<2018年一人アドベントカレンダー5日目>
はじめに
以下のようなコマンドのバインディングをXAMLではなく、コードビハインドに書きたいと思ったのです。
おなじみのInteraction
を使った方法です。
<Button Name="ButtonEventTest" Content="Button" HorizontalAlignment="Left" Margin="246,281,0,0" VerticalAlignment="Top" Width="75">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseEnter">
<i:InvokeCommandAction Command="{Binding MouseEnter}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
Button
であることに意味はありません。
できるようになったところでどうなのか、と言われたら返す言葉もありません。
重要なのはあるUI要素の任意のイベントに対してコマンドをバインディングしたいということです。
今回の例ではButton
要素にマウスオーバーしたときに、予めViewModel側に用意していたコマンドを呼び出して実行するようにしたいのです。
コードビハインドでどんなデータ構造になっているのか調べる
上記のXAMLを書いたときに、このButton
はどういう形でEventTriggerあたりを持っているのかを調べて、そのとおりにコードで構築してやればいいんじゃないかと思いました。
以下、MainWindow.xamlに上記のXAMLを書き、MainWindow.xaml.csにコードを書いていくという前提で話を進めます。
var buttonTriggers = Interaction.GetTriggers(ButtonEventTest);
UIにつけたEventTriggerは、このInteraction
クラスのGetTrigger
メソッドで取得することができます。
このコードをMainWindowのコンストラクタに書いて、ブレークポイントをつけて実行してみましょう。
ブレークポイントで止まったら「ローカル」タブや「ウォッチの追加」でbuttonTriggers
の中身を見てみましょう。

すると、以下のような構造になっていることがわかります。
TriggerCollection
└ EventTrigger
└ TriggerActionCollection(Actionsプロパティ)
└ InovokeCommandAction()
まずはEventTriggerをインスタンス化します。
System.Windows.Interactivity.EventTrigger trigger = new System.Windows.Interactivity.EventTrigger();
このEventTriggerオブジェクトのEventNameプロパティに対象とするイベントを指定します。
今回はMouseEnterイベントを指定します。
trigger.EventName = "MouseEnter";
このEventTriggerオブジェクトにActionsプロパティがあるので、ここにInvokeCommandActionオブジェクトを追加すれば良さそうです。
InvokeCommandAction action = new InvokeCommandAction();
このInvokeCommandActionオブジェクトはCommandプロパティを持っていて、ここに呼び出したいコマンドを指定してやればOKというわけですね!
ここで問題発生
どうやってコマンド指定するの(´・ω・`)
XAMLのほうではコマンド名で指定すれば、バインディングによって呼び出しはよろしくやってくれるのですが、XAMLでやる場合どうやったらバインディングできるのでしょうか。
例えばButton
要素であればSetBinding
メソッドを使って、コマンドをバインディングできます。
ButtonEventTest.SetBinding(Button.CommandProperty, new Binding("MouseEnter"));
こんな感じで他のUI要素でもコマンドのバインディングができたらいいのですが、Button以外だとこうも行かないのです。
誰か教えて!
解決策
さて、このままできないで終わっては私のこの数時間は一体何だったのということになるので、なんとか中途半端に解決してみました。
SetBindingみたいなやりかたがわからなかったので、じゃあこのInvokeCommandActionオブジェクトのCommandプロパティにViewModelで定義したコマンドを指定してやる、という、なんだか、ねぇ…。
DataContextにMainWindowViewModelが指定されているとして、こんな風にするととりあえずいけますが…。
ViewModelを明示しなければならないのがいけないです。
//ここが致命的。ViewがViewModelのことを知っていることになる。
var dc = this.DataContext as MainWindowViewModel;
InvokeCommandAction action = new InvokeCommandAction
{
Command = dc.MouseEnterCommand
};
じゃあなんとかしなければならないということで、dynamic
キーワードを使ってViewModelを明示せずに住むようにしてみました。
でも、コマンドの定義が無い場合は例外が発生するのでtry-catchで囲む必要が出てきますが。
dynamic dc = this.DataContext;
InvokeCommandAction action = new InvokeCommandAction
{
Command = dc.MouseEnterCommand
};
やっと話が戻りますが、後はこれをEventTriggerオブジェクトのActions
プロパティに追加して、更にEventTriggerオブジェクトをUI要素のTriggerCollectionに追加すると完了です。
var triggers = Interaction.GetTriggers(ButtonEventTest);
dynamic dc = this.DataContext;
//イベント1つぶんの記述。ここから
System.Windows.Interactivity.EventTrigger trigger = new System.Windows.Interactivity.EventTrigger();
trigger.EventName = "MouseEnter";
InvokeCommandAction action = new InvokeCommandAction
{
Command = dc.MouseEnterCommand
};
trigger.Actions.Add(action);
//ここまで
triggers.Add(trigger);
おわりに
やってみて当たり前のことを実感したことがあります。
それは、一つのイベントに対してこれだけ書く必要があって、それがXAMLだと5行ぐらいでわかりやすくかけて、改めてよくできた仕組みだな、ということですね。
それと、中途半端なことしかできないのであれば、素直にXAMLでバインディングしとけってことです。
追記(2018.12.06)
コメントでご指摘や情報提供をいただいた結果を反映させたいと思います。
ViewModel(MainWindowViewModel)側にICommand
型のコマンド(MouseEnterCommand)を定義してあるとします。
やりたいことは、XAML側で任意のイベントに対してコマンドをバインディングする以下の方法:
<Button Content="XAMLBinding" HorizontalAlignment="Left" Margin="172,281,0,0" VerticalAlignment="Top" Width="92">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseEnter">
<i:InvokeCommandAction Command="{Binding MouseEnterCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
を、C#で書くということです。
実用性はとりあえず考えないでいただくと嬉しいです。
ただ、XAMLは絶対書きたくないんだ!という方もいらっしゃるかもしれないので、もしかしたらお役に立てるかもしれません。
これについて、私は記事の中で最終的に以下のようにしました。
var triggers = Interaction.GetTriggers(ButtonEventTest);
dynamic dc = this.DataContext;
//イベント1つぶんの記述。ここから
System.Windows.Interactivity.EventTrigger trigger = new System.Windows.Interactivity.EventTrigger();
trigger.EventName = "MouseEnter";
InvokeCommandAction action = new InvokeCommandAction
{
Command = dc.MouseEnterCommand
};
trigger.Actions.Add(action);
//ここまで
triggers.Add(trigger);
可能な限りViewとViewModelとの依存関係をできるだけなくしたいと思い、dynamic
キーワードを使ってDataContext
の型によらず、実行時にその中の指定したICommand
型のプロパティを引っ張ってきてInvokeCommandAction
オブジェクトのCommand(ICommand型)
プロパティに入れることにしました。
これでコードからはMainWindowViewModel
が消えました。
しかし、記事でも触れましたが、これだとViewModel側に指定した名前のプロパティが含まれていないと例外になってしまうため、try-catchで囲む必要あります。
ということは、このコードはViewModel側の実装を正確に知っておかなければならないというわけで、これはViewModelに依存しているということになります。
(@KyoPeeee さんに指摘していただいた「意味もなくdyanmicで型情報を消している」というのは、こういう意味なのだと理解しました。)
その後、@shiganai_programmer1129 さんと@albireo さんから多くの情報をいただき、refrection
を使うと目的を達成できることを教えていただきました。
<Button Name="ButtonEventTest" Content="Button" HorizontalAlignment="Left" Margin="305,281,0,0" VerticalAlignment="Top" Width="76"/>
var triggers = Interaction.GetTriggers(ButtonEventTest);
System.Windows.Interactivity.EventTrigger trigger=new System.Windows.Interactivity.EventTrigger();
trigger.EventName = "MouseEnter";
InvokeCommandAction action=new InvokeCommandAction();
//action.Command = dc.MouseEnterCommand;//
var command= DataContext?.GetType().GetProperty("MouseEnterCommand")?.GetValue(DataContext) as ICommand;
action.Command = command;
trigger.Actions.Add(action);
triggers.Add(trigger);
DataContext
からICommand
型のプロパティを取得する箇所において、refrection
を使ってプロパティ名を文字列で指定して取得しています。
プロパティが存在しない場合はnullが返ってくるだけで、例外は発生しません。
今回の例で言えば、実際はDataContext
にMainWindowViewModel
のインスタンスを入れるコードがこのコードの外側にあるのですが、それ以外はViewがViewModelを参照する部分がなく、とても薄い依存になっていると思います。
しかし、それもPrismなどのMVVMフレームワークを使うとViewとViewModelの紐付けをMVVMフレームワーク側がやってくれるため、書く必要がなくなります。
そうなるとViewModelのファイルをまるごと削ってもアプリケーションは動き、気持ちいいくらいViewとViewModelの依存がなくなります。
おわりに
さて、いただいたアドバイスどおりの結果で、自分はほぼ何もしていないので大変恐縮なのですが、とはいえ今回の件でWPFのバインディングの仕組みについての理解がとても深まりました。
コメントをいただいた皆さんには深く感謝いたします。
まだ自分の知識が足りずTakeしてばかりですが、いつかGiveできるようになりたいと強く思いました。
それと、以下のことを感じました。
- アウトプットすると沢山教えてもらえて勉強になるのでアウトプットしよう。
- WPF偉大。
- フレームワーク偉大。
- コメントが来ると冷や汗かくけど気にせずどんどんレスしよう(笑)。
ありがとうございましたm(_ _)m