No more Death March

あるSEのチラシの裏 C# WPF

Bitmani Redmineのバックアップとリストア

昨日書いた以下の続きから

nomoredeathmarch.hatenablog.com

バックアップとリストアの手段を確認したい

 プロジェクトとかチケットとか細かい運用方法を考える前になにかあったときのためにバックアップとリストアの手順を確認しておく。また社内に普及する時にPC内のRedmineのチケットならなにやらをサーバーにスムーズに移したいしね。

とりあえずRedmineのバックアップって何をバックアップすれば良いのか。

 以下参照

qiita.com

つまり

Redmineインストールフォルダ内の「files」フォルダに入っているもの一式
Redmineのデータが登録されているデータベースのフルバックアップ

 この二つがバックアップされていれば良いということらしい。filesフォルダ内にはチケットやWikiに添付されたファイル類の保存先とのこと、それ以外のプロジェクトやチケットの情報はデータベース(Bitmaniのオールインワンパッケージを使ったならMySql)に保存されている様子。

filesフォルダの場所を確認

 自宅PCの環境だと以下のフォルダと思われる。

D:\Bitnami\redmine-4.0.3-3\apps\redmine\htdocs\files

データベース名、ユーザー名、パスワード等の確認

 以下のファイルから確認出来た。

D:\Bitnami\redmine-4.0.3-3\apps\redmine\htdocs\config\database.yml

 MySQLに触れるのがかなり久しぶりなので不安なのだけど概ね他のデータベース製品と同じだろう。
大体
インスタンス
・ユーザー名
・パスワード
・ポート
エンコード
・ホスト名
このあたりを抑えとけばどうにでもなるだろう。

fileのバックアップ

 こちらはなんてことないだろう。とりあえずフォルダ毎ごっそりコピーして避けてみた。

MySQLのパスの確認

 バックアップ前にパスが通っているか確認してみたところ通っておらず。大体のデータベース製品てインストールした後に自分でパス通すしね。というわけでパスを通す場所を探してみたら以下のbinフォルダ内にそれらしいもの一式があったのでパスへ登録。と思ったけどコマンドファイルでやればいいかと思い登録せず。

D:\Bitnami\redmine-4.0.3-3\mysql\bin

dump取得用のコマンド作成

 以下のようなコマンドを作って実行してみると実行直後に警告文が表示されるけどダンプ自体はとれた模様。binフォルダ内にファイルが出力されている。ちなみに警告の内容は「コマンドラインインターフェースでパスワードべた書きするな、セキュアじゃないだろアホか。」という旨の内容。はい、ごもっともです。

pause
d:
cd D:\Bitnami\redmine-4.0.3-3\mysql\bin
mysqldump -u [username] -p[password] [databasename] > bitnami_redmine_backup
pause

※[]部分は各環境に合わせて読みかえる。

リストアコマンドの作成

 同じ要領でリストアコマンドを以下のように作った。で、実行するとバックアップの時と同じように警告が出るが処理出来た模様。

pause
d:
cd D:\Bitnami\redmine-4.0.3-3\mysql\bin
mysql -u [username] -p[password] [databasename] < bitnami_redmine_backup
pause

バックアップの結果を確認

 filesを復元する手順は兎も角として、MySqlの方がこれで復元できているか不安なのでRedmineに適当にチケットやプロジェクトを登録して再度復元を実行してみたところ無事復元出来た。悪意を持ってログインしっぱなしでリストアしてみるとリストア直後に画面を移動するとセッションが失効した旨のエラーが出てログイン画面に戻された。

最後に

 とりあえずバックアップとリストアは簡単に出来そうなので安心した。社内でサーバ立てることになってもこれなら簡単にローカルで抱えてたプロジェクトとかチケットを移せそう。

Excel職人が初めてRedmineを使って思ったことのメモ

今更ながらRedmineを使い始めたので色々とメモ

Redmineを使い始めた動機

 Redmineについては名前を聞いたことがある程度で最初からRedmine自体を覚えようとかそういった意識があったわけではない。所謂Excel職人なエンジニアなので今までタスク管理とかプロジェクトの進捗管理Excelでやっていたのだけどいい加減意識して改善しないとまずい状況になってきたという動機。

Redmineを見つけるまで

 手始めにガントチャートを手軽に作れる方法はないかと検索していて見つけたのがRedmine、他にも有名所のグループウェアの紹介サイトは山ほど出てきたけども今の社内でもある程度運用されているものがあるので可能な限り安価で手軽に人柱になる方法を考慮した結果Remineを試そうという結論に至った。

環境構築

 自分のスキル的にWindowsズブズブで0から学習してWebサービスを立ち上げるのは現実的ではない(ほんとは勉強したいところだけど・・・)、そこでWindowsへ導入する方法を検索したら最初に以下の記事がヒットした。

qiita.com

 どうやらオールインワンパッケージなるものが公開されているらしく、これなら自分のPCで検証出来そうと思いさっそくインストーラーをダウンロード

bitnami.com

 インストーラーを起動してぷちぷちするだけでRedmineにアクセスするところまでたどり着けた。

最初の最初にやったこと

 とりあえずはどういう機能がありそうか見た目で想像しながら色んなタブを表示してみる。唯一既定の設定が認証を不要とし匿名のアクセスを受け入れる形になっているようなので認証は必須とした。

 ある程度ページの構成が分かったところでプロジェクトを作っていくつかチケットを登録・編集しながらガントチャートとにらめっこしていた。

少し触ってみての感想

 率直に「いいなこれ」と思った。トラッカーやロール、カスタムフィールド等、プラグインを全く入れていない状態でもかなりのカスタマイズが効くし、プラグインをある程度使いこなせば市販のグループウェア相当の働きは十分にしてくれると思う。いやいや、市販製品だって使いこなせば・・・というのは事実だと思うが正直あれやこれや出来たとしても日常的に利用される機能ってのは極々シンプルなものになりがち。

懸念事項

 本題はここ、業務で取り入れていくうえで不安になった点があるのでいくつか書いていく。今後使いながら落としどころを探求していきたい。

UIが使いにくい

 自分がWebサービスをあまり使わないので慣れていないせいか入力のUIはかなり使いにくいな、と思った。慣れれば大丈夫だろうとも思ったけども人に勧める上で多少なりとも反発が起きそうな感じがした。しかしプロジェクトを管理する側の立場で当初の計画を立てるにあたって大量にチケットを入れるというのには不向きというだけで、日常的に小さいタスクの粒度で向き合う分には気にならないのかもしれない。

プラグイン入れるか否か

 魅力的なプラグインがたくさん公開されていて大変有難いことだけどどうしても互換性に関しては気になるところ、個人で使っている分にはがんがん入れちゃいましょうというところだけど組織で使っている以上プラグインにどっぷりつかっていたがある日を境にプラグインの更新が止まり、ハードや周辺ソフトのライフサイクルの事情からプラグインに依存していた部分は全て捨てなければなりません。となると辛い。
 標準の状態で多少の工夫で解決出来そうな問題であればその方が良いと思う。新しくメンバーが入ってきた時にもなるべく標準に近い形の方が習得や慣れにかかるストレスが少なくなるだろう。

祝日管理

 前項に付随して使い始めのこと誰もが最初に「あれっ?」ってなるところなんじゃないかと思う。祝日や会社の設立記念日等加味したうえでガントチャートの線が伸びてくれるかなぁ・・・くれないよなぁ・・・・
 とりあえず祝日周りのプラグインくらいは入れた方がいいだろうなぁ・・・と思いつつ祝日や有給をチケットという形で入れてみることとした。

プロジェクトの粒度

 これも悩ましいところ、ぱっと浮かぶのは部門毎にプロジェクトを分けるというところ。営業、開発、総務という大まかな区分があって、営業なら新規開拓か既存の顧客か、開発なら製品毎だったり製品毎でも1次リリース2次リリースみたいな区切りが想像出来る。
 これは組織の人数や事業所の具合なんかに大きく左右されるところと思われる、正解なんてないが注意しなきゃいけないのは考え無しにプロジェクトを乱立させるっていうのは悪手だろうなということ。他のグループウェアと同じように自分達の組織形態に合わせたノウハウの蓄積が必要になるところなんだろうな。とにかく管理管理と固執して最初っからガチガチに決めてさぁみんなでルール通りやろうぜは大体得るもの以上に失うものが多い失敗パターンの典型。
 まずは今自分が管理しているプロジェクトのそこそこ規模の大きいマイルストーンを一つのプロジェクトにして行くことにした。(所謂〇次開発単位程度の粒度)

小さくチケットを切るという癖とつけたい

 自分が開発する時は小さなミスであってもこまめにチケットを切って行こうと思っている。例えば〇〇画面作ってたらこういう部分でつっかかって〇時間かかっちゃったので気を付けようとか、体裁とか気にせずメモ帳でもエクセルでもなんでも良いから書き留めて置く、チームでの管理っていうのはなかなか難しいところがあるんだけども個人単位でもこういうナレッジを溜めておくと命拾いすることが本当に多い。
 こういう気づきをとりあえずチケットで残す癖をつけ、隙を見てWikiに落とし込んでいく、という風にすればメンバー同士でもノウハウを共有しやすくなるんじゃないだろうか。

小さい視点と大きい視点の明確な切り分け

 前項の話も大事なのだけど当然プロジェクトの状況を俯瞰して見たい。普段は個人レベルのナレッジとして、上長や経営者と共有する部分は大まかなレベルで。ガントチャートをスムーズに切り替えれるようにするにはどうするか、標準の状態で出来そうなことで思いついたのは二つ、まず、どちらも親チケットでフェーズを大まかに区分することには変わらない、空のプロジェクトにチケットを登録する時にExcel職人してた時と同じように報告の単位でチケットの木構造を作っていけば良い。
 二つの手段で違うのはここから、一つ目はトラッカーでチケットに「報告用」という目印をつけること。報告の単位に全て同じトラッカーを付けてガントチャートでトラッカー単位でフィルタリングしてやれば良い。が、ここで一つ踏みとどまった。「報告用」と一口にいっても大まかな分類でも今後その性質に基づいて切り分けをしたい場合が出てくるのではないか?ということ。 例えば同じプロジェクトでもある程度進行して行くと業務の性質毎にチームの作業を細分化する場合がある。そうなるとどうやってチケットに目印をつけるか、自然に思ったのはそりゃトラッカーで「〇〇業務」とかつけるだろうということ。「報告用」と目印をつける手段とはトラッカーというリソースが競合してしまう。
 そこで二つ目の手段として思いついたのはユーザーを使う方法、「報告者」というユーザーをプロジェクトに参加させて俯瞰するための粒度のチケットの担当者に割り当ててやる。そうすればトラッカーは作業内容な業務を分割するための目印として利用出来る。

カスタマイズは極力避けたい

 標準の状態でトラッカーを増やしたりカスタムフィールドを増やしたり工夫の余地は色々あるようだけどカスタマイズはなるべく避けてシンプルに使いたいなと感じた。エクセルの資料でも他のグループウェアでも安易にあれを追加これを追加とすると管理コストや入力コストがどんどん増えていく割に後からやっぱりいらなかったとかそもそもこれなんのためにやってるの?という状況になりがち。

さいごに

 大分だらだら書いてしまったけどまとめるとExcelであれやこれやファイルを管理するよりは断然良い感じ、しばらく使ってみてノウハウが出来たら社内に布教するかな。

C# ListをラップしたObservableCollectionのイベント

ObservableCollectionはコンストラクタにListを渡して生成することが出来る。が、生成した後にListクラスを通じてコレクションを編集してもコレクション変更イベントは発生しない。

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            var list = new List<int>();
            var observable = new ObservableCollection<int>(list);
            observable.CollectionChanged += Observable_CollectionChanged;

            list.Add(1);
            list.Add(2);
            list.Add(3);
            Console.ReadKey();
        }

        private static void Observable_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            Console.WriteLine("Observable_CollectionChanged Called!!");
        }
    }
}
|cs|<

WPF TextBoxの不思議な挙動

忘れる前にメモしておく。上位からみてScrollViewer→StackPanel→Grid→TextBoxの順にビジュアルーツリーを構成し、横スクロール可能な状態でGridの列をStar指定で分割、TextBoxにテキストを入力しつづけて割り当てられたサイズを超えると指定した幅を無視してTextBoxの幅が大きくなってしまう。ScrollViewerのHorizontalScrollBarVIsibilityをHiddenにしてやれば解消可能。

これ仕様・・・?そこそこ複雑な画面で原因が分からずかなり焦った・・・

C# ObservableCollectionに対する操作とCollectionChangedイベントの内容

INotifyCollectionChangedインターフェースのCollectionChangedイベントについてMSDNの記述だけだとどういう通知が来るのかわからなかったのでプログラムを書いて確認してみた。

MSDNのページは以下

docs.microsoft.com

確認用のプログラム

Visual Studioで新規のコンソールアプリケーション作ってProgram.csにべたっとペーストすれば動く

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;

namespace CollectionChangedTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var col = new ObservableCollection<int>();
            col.CollectionChanged += Col_CollectionChanged;
            Debug.WriteLine("var col = new ObservableCollection<int>()");

            Debug.WriteLine("==========================================");
            Debug.WriteLine("col.Add(1)");
            Debug.WriteLine("==========================================");
            col.Add(1);
            Debug.WriteLine(string.Format("col = {0}", string.Join(",", col.Select(x => x.ToString()))));

            Debug.WriteLine("==========================================");
            Debug.WriteLine("col.Add(2)");
            Debug.WriteLine("==========================================");
            col.Add(2);
            Debug.WriteLine(string.Format("col = {0}", string.Join(",", col.Select(x => x.ToString()))));

            Debug.WriteLine("==========================================");
            Debug.WriteLine("col.Add(3)");
            Debug.WriteLine("==========================================");
            col.Add(3);
            Debug.WriteLine(string.Format("col = {0}", string.Join(",", col.Select(x => x.ToString()))));

            Debug.WriteLine("==========================================");
            Debug.WriteLine("col.Add(4)");
            Debug.WriteLine("==========================================");
            col.Add(4);
            Debug.WriteLine(string.Format("col = {0}", string.Join(",",col.Select(x => x.ToString()))));

            Debug.WriteLine("==========================================");
            Debug.WriteLine("col.Remove(3)");
            Debug.WriteLine("==========================================");
            col.Remove(3);
            Debug.WriteLine(string.Format("col = {0}", string.Join(",", col.Select(x => x.ToString()))));

            Debug.WriteLine("==========================================");
            Debug.WriteLine("col.RemoveAt(1)");
            Debug.WriteLine("==========================================");
            col.RemoveAt(1);
            Debug.WriteLine(string.Format("col = {0}", string.Join(",", col.Select(x => x.ToString()))));

            Debug.WriteLine("==========================================");
            Debug.WriteLine("col.Move(0,1)");
            Debug.WriteLine("==========================================");
            col.Move(0,1);
            Debug.WriteLine(string.Format("col = {0}", string.Join(",", col.Select(x => x.ToString()))));

            Debug.WriteLine("==========================================");
            Debug.WriteLine("col.Insert(1,5)");
            Debug.WriteLine("==========================================");
            col.Insert(1, 5);
            Debug.WriteLine(string.Format("col = {0}", string.Join(",", col.Select(x => x.ToString()))));

            Debug.WriteLine("==========================================");
            Debug.WriteLine("col.OrderBy(x => x)");
            Debug.WriteLine("==========================================");
            Debug.WriteLine("※異なる参照が返るのでイベントは発生しない。");
            col.OrderBy(x => x);
            Debug.WriteLine(string.Format("col = {0}", string.Join(",", col.Select(x => x.ToString()))));

            Debug.WriteLine("==========================================");
            Debug.WriteLine("col.Reverse()");
            Debug.WriteLine("==========================================");
            Debug.WriteLine("※異なる参照が返るのでイベントは発生しない。");
            col.Reverse();
            Debug.WriteLine(string.Format("col = {0}", string.Join(",", col.Select(x => x.ToString()))));

            Debug.WriteLine("==========================================");
            Debug.WriteLine("col.Clear()");
            Debug.WriteLine("==========================================");
            col.Clear();
            Debug.WriteLine(string.Format("col = {0}", string.Join(",", col.Select(x => x.ToString()))));

            Console.ReadKey();
        }

        private static void Col_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            var sb = new StringBuilder();


            sb.AppendLine("↓↓↓NotifyCollectionChangedEventArgsの中身↓↓↓");
            sb.AppendLine(string.Format("e.Action = {0}",e.Action.ToString()));

            if(e.NewItems == null)
            {
                sb.AppendLine("e.NewItems == null");
            }
            if(e.NewItems != null)
            {
                sb.AppendLine(string.Format("e.NewItems.Count = {0}", e.NewItems.Count.ToString()));
                sb.AppendLine(string.Format("e.NewItems = {0}", string.Join(",", e.NewItems.Cast<int>().ToArray().Select(x => x.ToString()))));
            }
            sb.AppendLine(string.Format("e.NewStartingIndex = {0}",e.NewStartingIndex.ToString()));

            if (e.OldItems == null)
            {
                sb.AppendLine("e.Oldtems == null");
            }
            if (e.OldItems != null)
            {
                sb.AppendLine(string.Format("e.OldItems.Count = {0}", e.OldItems.Count.ToString()));
                sb.AppendLine(string.Format("e.OldItems = {0}", string.Join(",", e.OldItems.Cast<int>().ToArray().Select(x => x.ToString()))));
            }
            

            sb.AppendLine(string.Format("e.OldStartingIndex = {0}", e.OldStartingIndex.ToString()));

            Debug.Write(sb.ToString());
    }
    }
}

実行結果

実行すると以下の通りデバッグ出力が得られた。

var col = new ObservableCollection<int>()
==========================================
col.Add(1)
==========================================
↓↓↓NotifyCollectionChangedEventArgsの中身↓↓↓
e.Action = Add
e.NewItems.Count = 1
e.NewItems = 1
e.NewStartingIndex = 0
e.Oldtems == null
e.OldStartingIndex = -1
col = 1
==========================================
col.Add(2)
==========================================
↓↓↓NotifyCollectionChangedEventArgsの中身↓↓↓
e.Action = Add
e.NewItems.Count = 1
e.NewItems = 2
e.NewStartingIndex = 1
e.Oldtems == null
e.OldStartingIndex = -1
col = 1,2
==========================================
col.Add(3)
==========================================
↓↓↓NotifyCollectionChangedEventArgsの中身↓↓↓
e.Action = Add
e.NewItems.Count = 1
e.NewItems = 3
e.NewStartingIndex = 2
e.Oldtems == null
e.OldStartingIndex = -1
col = 1,2,3
==========================================
col.Add(4)
==========================================
↓↓↓NotifyCollectionChangedEventArgsの中身↓↓↓
e.Action = Add
e.NewItems.Count = 1
e.NewItems = 4
e.NewStartingIndex = 3
e.Oldtems == null
e.OldStartingIndex = -1
col = 1,2,3,4
==========================================
col.Remove(3)
==========================================
↓↓↓NotifyCollectionChangedEventArgsの中身↓↓↓
e.Action = Remove
e.NewItems == null
e.NewStartingIndex = -1
e.OldItems.Count = 1
e.OldItems = 3
e.OldStartingIndex = 2
col = 1,2,4
==========================================
col.RemoveAt(1)
==========================================
↓↓↓NotifyCollectionChangedEventArgsの中身↓↓↓
e.Action = Remove
e.NewItems == null
e.NewStartingIndex = -1
e.OldItems.Count = 1
e.OldItems = 2
e.OldStartingIndex = 1
col = 1,4
==========================================
col.Move(0,1)
==========================================
↓↓↓NotifyCollectionChangedEventArgsの中身↓↓↓
e.Action = Move
e.NewItems.Count = 1
e.NewItems = 1
e.NewStartingIndex = 1
e.OldItems.Count = 1
e.OldItems = 1
e.OldStartingIndex = 0
col = 4,1
==========================================
col.Insert(1,5)
==========================================
↓↓↓NotifyCollectionChangedEventArgsの中身↓↓↓
e.Action = Add
e.NewItems.Count = 1
e.NewItems = 5
e.NewStartingIndex = 1
e.Oldtems == null
e.OldStartingIndex = -1
col = 4,5,1
==========================================
col.OrderBy(x => x)
==========================================
※異なる参照が返るのでイベントは発生しない。
col = 4,5,1
==========================================
col.Reverse()
==========================================
※異なる参照が返るのでイベントは発生しない。
col = 4,5,1
==========================================
col.Clear()
==========================================
↓↓↓NotifyCollectionChangedEventArgsの中身↓↓↓
e.Action = Reset
e.NewItems == null
e.NewStartingIndex = -1
e.Oldtems == null
e.OldStartingIndex = -1
col = 

イベント発生時に通知されるNotifyCollectionChangedEventArgsの各メンバーについてメモ

Action

・操作の内容によって値が変わる。
・Add→Add
・Insert→Add
・Move→Move
・Remove→Remove
・RemoveAt→Remove
・Clear→Reset
・SetItem→Replace(?) ※SetItemはprotected、今回は確認していない。

NewItems

・Addメソッド、Insertメソッド、Moveメソッドで対象要素のインスタンスが格納される。
・これ以外のメソッドはnullとなる。
・たぶんSetItemもここに入るだろう。

NewStartingIndex

・NewItemsの開始位置を示す
・NewItemsにインスタンスが格納されていない場合-1を取る

OldItems

・Removeメソッド、RemoveAtメソッド、Moveメソッドで削除、移動前の要素のインスタンスが格納される
・これ以外のメソッドはnullとなる
・SetItemもはいる??

OldStartingIndex

・OldItemsの開始位置を示す。
・つまり「削除・移動前に要素が入っていた位置」
・OldItemsにインスタンスが格納されていない場合-1となる

Clearメソッドについて補足

・ActionメンバーがResetを取る。
・NewItemとOldItemはnull
・NewStartingIndexとOldStartingIndexはともに-1

OrderByメソッドやReverseメソッドについて補足

・異なる参照が返るので元のコレクションの内容は変わらない
・したがってイベントも発生しない

WPF MultiBindingのコールバックの挙動について

WPFのMultiBindingを初めて使った時に若干嵌ったのでメモしておく。

テスト用ビヘイビア

コードビハインドでMultiBindingクラスを生成しバインディングする。マルチと言いつつTextプロパティしかバインディングしていないが確認するのには十分、コンバーター内で呼び出された旨をデバッグ出力し、ターゲットの添付プロパティもコールバック関数内で同様にデバッグ出力する。

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace CodeBehindBindingSample
{
    public static class TextBoxBehavior
    {


        public static bool GetIsAttached(DependencyObject obj)
        {
            return (bool)obj.GetValue(IsAttachedProperty);
        }

        public static void SetIsAttached(DependencyObject obj, bool value)
        {
            obj.SetValue(IsAttachedProperty, value);
        }

        public static readonly DependencyProperty IsAttachedProperty = DependencyProperty.RegisterAttached("IsAttached", typeof(bool), typeof(TextBoxBehavior), new PropertyMetadata(false,OnIsAttachedChanged));

        private static void OnIsAttachedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var multiBinding =
                new MultiBinding()
                {
                    Mode = BindingMode.OneWay,
                    Converter = new MultiBindingConverter()
                };
            multiBinding.Bindings.Add(
                new Binding()
                {
                    Source = d,
                    Path = new PropertyPath(TextBox.TextProperty),
                }
                );

            BindingOperations.SetBinding(d, TextBoxBehavior.MultiBindingTargetProperty, multiBinding);
        }



        public static object[] GetMultiBindingTarget(DependencyObject obj)
        {
            return (object[])obj.GetValue(MultiBindingTargetProperty);
        }

        public static void SetMultiBindingTarget(DependencyObject obj, object[] value)
        {
            obj.SetValue(MultiBindingTargetProperty, value);
        }

        public static readonly DependencyProperty MultiBindingTargetProperty = DependencyProperty.RegisterAttached("MultiBindingTarget", typeof(object[]), typeof(TextBoxBehavior), new PropertyMetadata(new object[] { },OnMultiBindingTargetChanged));

        private static void OnMultiBindingTargetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            System.Diagnostics.Debug.WriteLine("OnMultiBindingTargetChanged Called");
        }

        private class MultiBindingConverter
            : IMultiValueConverter
        {
            public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
            {
                System.Diagnostics.Debug.WriteLine("Convert Called");
                return values;
            }

            public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
            {
                throw new InvalidOperationException();
            }
        }
    }
}

画面

先のビヘイビアを有効にしたテキストボックスを画面におく。

<Window x:Class="CodeBehindBindingSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:CodeBehindBindingSample"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <StackPanel>
        <TextBox local:TextBoxBehavior.IsAttached="True"/>
    </StackPanel>
</Window>

何が問題なのか

この画面を起動すると起動直後に以下のように出力される。

Convert Called
OnMultiBindingTargetChanged Called


ここまでは良い。SetBindingを実行したタイミングでバインディング先の添付プロパティに値が書き込まれることがわかる。問題はここからで、以降ソースとなっているTextBoxをいくら書き換えても以下の通りコンバーターのみ動作し、コールバック関数が呼ばれなくなる。

Convert Called
Convert Called
Convert Called

解決方法

解消するには以下の通り、コンバーター内で新しいインスタンスが戻り値として返るようにすればよい。

        private class MultiBindingConverter
            : IMultiValueConverter
        {
            public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
            {
                System.Diagnostics.Debug.WriteLine("Convert Called");
                // ToArrayメソッドで常に参照が変わるように
                return values.ToArray();
            }

            public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
            {
                throw new InvalidOperationException();
            }
        }


再度実行するとTextBoxのTextプロパティが変わる度にコールバック関数が呼ばれるようになる。

Convert Called
OnMultiBindingTargetChanged Called
Convert Called
OnMultiBindingTargetChanged Called
Convert Called
OnMultiBindingTargetChanged Called
Convert Called
OnMultiBindingTargetChanged Called

おそらくだが、MultiBindingは内部で同じ参照の配列を保持しソースの値が変わったらインスタンスを変えずに配列を編集しているのだろう。今回のケースだと初回のバインディング時に参照が渡されて以降参照が変わっていないため、コールバックが呼ばれなくなっていたと思われる。MultiBinding自体あまり使う機会もないだろうし、使ったとしてもなんらかの加工を加えインスタンスが生成された結果を利用すると思われるのであまり気にすることはないのだけど、運良く(?)こういう事象を踏み抜く場合もありますよと・・・・

WPF Window起動時のイベント発生順位をメモ

余り複雑な制御を行っているコントロールを作るのは好ましくないが、どうしてもやらなければならない場合が結構あったりする。そんな時に毎回イベントの発生順位ってどうだっけ?と忘れてしまうことがあるのでメモしておく。後述の通りサンプル画面を作ってビヘイビアで各イベントが着火したタイミングで発生したイベントをデバッグプリントしてやる。

<Window x:Class="WpfEventTracking.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfEventTracking"
        mc:Ignorable="d"
        local:WindowBehavior.IsAttached="True"
        Title="MainWindow" Height="450" Width="800"
        >
    <GroupBox Header="GroupBox" local:ControlBehavior.IsAttached="True">
        <StackPanel>
            <TextBox Text="TextBox" local:ControlBehavior.IsAttached="True"/>
            <Button Content="Button" local:ControlBehavior.IsAttached="True"/>
        </StackPanel>
    </GroupBox>
</Window>

全てのイベントを網羅すると大変なので今回はUIElement、FrameworkElement、Control、Windowのイベントのみ出力する。

Window起動時

TextBox Initialized
Button Initialized
GroupBox Initialized
MainWindow Initialized
MainWindow IsVisibleChanged
GroupBox IsVisibleChanged
TextBox IsVisibleChanged
Button IsVisibleChanged
MainWindow SizeChanged
GroupBox SizeChanged
Button SizeChanged
TextBox SizeChanged
Button LayoutUpdated
TextBox LayoutUpdated
GroupBox LayoutUpdated
MainWindow LayoutUpdated
MainWindow Activated
MainWindow PreviewGotKeyboardFocus
MainWindow IsKeyboardFocusWithinChanged
MainWindow IsKeyboardFocusedChanged
MainWindow GotKeyboardFocus
Button LayoutUpdated
TextBox LayoutUpdated
GroupBox LayoutUpdated
MainWindow LayoutUpdated
MainWindow Loaded
GroupBox Loaded
TextBox Loaded
Button Loaded
MainWindow ContentRendered
Button LayoutUpdated
TextBox LayoutUpdated
GroupBox LayoutUpdated
MainWindow LayoutUpdated
Button LayoutUpdated
TextBox LayoutUpdated
GroupBox LayoutUpdated
MainWindow LayoutUpdated
Button LayoutUpdated
TextBox LayoutUpdated
GroupBox LayoutUpdated
MainWindow LayoutUpdated
Button LayoutUpdated
TextBox LayoutUpdated
GroupBox LayoutUpdated
MainWindow LayoutUpdated
Button LayoutUpdated
TextBox LayoutUpdated
GroupBox LayoutUpdated
MainWindow LayoutUpdated
Button LayoutUpdated
TextBox LayoutUpdated
GroupBox LayoutUpdated
MainWindow LayoutUpdated

LayoutUpdatedイベントまで出すと見難い・・・

Window起動時(LayoutUpdate無しver)

TextBox Initialized
Button Initialized
GroupBox Initialized
MainWindow Initialized
MainWindow IsVisibleChanged
GroupBox IsVisibleChanged
TextBox IsVisibleChanged
Button IsVisibleChanged
MainWindow SizeChanged
GroupBox SizeChanged
Button SizeChanged
TextBox SizeChanged
MainWindow Activated
MainWindow PreviewGotKeyboardFocus
MainWindow IsKeyboardFocusWithinChanged
MainWindow IsKeyboardFocusedChanged
MainWindow GotKeyboardFocus
MainWindow Loaded
GroupBox Loaded
TextBox Loaded
Button Loaded
MainWindow ContentRendered

LayoutUpdatedイベントを非表示にした再度確認

Window終了時

MainWindow Closing
MainWindow IsVisibleChanged
GroupBox IsVisibleChanged
TextBox IsVisibleChanged
Button IsVisibleChanged
MainWindow Deactivated
MainWindow IsKeyboardFocusWithinChanged
MainWindow IsKeyboardFocusedChanged
MainWindow LostKeyboardFocus
MainWindow Closed

ついでなのでWindow終了時の結果もメモ

まとめ

フォーカス取得系を省いて順番を整理すると
Initialize→IsVisibleChanged→SizeChanged→Activated→Loaded→ContentRendered→(画面表示完了)→Closing→IsVisibleChanged→Deactivec→Closed

バブルイベントとトンネルイベントを分けると

バブルイベント
Initialized

トンネルイベント
IsVisibleChanged
SizeChanged
Loaded

といった具合になる。

少し検索してみるとここで確認した通り、Windowのイベント発生順位がまとめられてた。

stackoverflow.com