Bitmani Redmineのバックアップとリストア
昨日書いた以下の続きから
nomoredeathmarch.hatenablog.com
バックアップとリストアの手段を確認したい
プロジェクトとかチケットとか細かい運用方法を考える前になにかあったときのためにバックアップとリストアの手順を確認しておく。また社内に普及する時にPC内のRedmineのチケットならなにやらをサーバーにスムーズに移したいしね。
とりあえずRedmineのバックアップって何をバックアップすれば良いのか。
以下参照
つまり
・Redmineインストールフォルダ内の「files」フォルダに入っているもの一式
・Redmineのデータが登録されているデータベースのフルバックアップ
この二つがバックアップされていれば良いということらしい。filesフォルダ内にはチケットやWikiに添付されたファイル類の保存先とのこと、それ以外のプロジェクトやチケットの情報はデータベース(Bitmaniのオールインワンパッケージを使ったならMySql)に保存されている様子。
データベース名、ユーザー名、パスワード等の確認
以下のファイルから確認出来た。
D:\Bitnami\redmine-4.0.3-3\apps\redmine\htdocs\config\database.yml
MySQLに触れるのがかなり久しぶりなので不安なのだけど概ね他のデータベース製品と同じだろう。
大体
・インスタンス名
・ユーザー名
・パスワード
・ポート
・エンコード
・ホスト名
このあたりを抑えとけばどうにでもなるだろう。
fileのバックアップ
こちらはなんてことないだろう。とりあえずフォルダ毎ごっそりコピーして避けてみた。
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へ導入する方法を検索したら最初に以下の記事がヒットした。
どうやらオールインワンパッケージなるものが公開されているらしく、これなら自分のPCで検証出来そうと思いさっそくインストーラーをダウンロード
最初の最初にやったこと
とりあえずはどういう機能がありそうか見た目で想像しながら色んなタブを表示してみる。唯一既定の設定が認証を不要とし匿名のアクセスを受け入れる形になっているようなので認証は必須とした。
ある程度ページの構成が分かったところでプロジェクトを作っていくつかチケットを登録・編集しながらガントチャートとにらめっこしていた。
少し触ってみての感想
率直に「いいなこれ」と思った。トラッカーやロール、カスタムフィールド等、プラグインを全く入れていない状態でもかなりのカスタマイズが効くし、プラグインをある程度使いこなせば市販のグループウェア相当の働きは十分にしてくれると思う。いやいや、市販製品だって使いこなせば・・・というのは事実だと思うが正直あれやこれや出来たとしても日常的に利用される機能ってのは極々シンプルなものになりがち。
懸念事項
本題はここ、業務で取り入れていくうえで不安になった点があるのでいくつか書いていく。今後使いながら落としどころを探求していきたい。
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のページは以下
確認用のプログラム
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のイベント発生順位がまとめられてた。