SourceChordスマートフォン用の表示で見るSourceChordC#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。前置き先日のConnect();では、久しぶりにWPFの今後のロードマップの話がありましたねwWPFの次バージョンが気になる今日この頃ですが、今日は現時点での最新版WPF4.5で追加されていた新機能についてのお話をしたいと思います。WPF4.5の新機能WPF4.5で追加された新機能、ちゃんと使ってみると、地味に便利な機能が多々あるのです。新機能一覧は以下のページにまとまっています。新機能サンプルコードもないし、説明も微妙にわかりにくいので、ちょっと残念な感じですが。この記事で作る物この記事では、WPF4.5の新機能の一番最後に書かれている、「イベントのマークアップ拡張」についてご紹介します。イベントのマークアップ拡張を用いると、「イベント発生時にVMのコマンド呼び出しを行う」という処理をXAMLでシンプルに書けるようになります。同様の事はBlendSDKのInvokeCommandActionを用いても書くことができます。比較してみるとXAMLのスッキリ具合は一目瞭然。この記事ではデータバインディングやMVVMなどについての予備知識が必要になるかと思います。Scent「イベントのマークアップ拡張」の作成は、上記記事の最後で出てきているCommandの呼び出し方の延長のような内容になるかと思います。少々長くなりますがお付き合いくださいませ。それでは、さっそく本題に入っていきたいと思います。イベントのマークアップ拡張とは前述の新機能についてのリンクでは、以下のような一文で説明されています。イベントのマークアップ拡張をサポートします。定義されていないイベントのマークアップ拡張は、誰でも作成できます。なんのこっちゃ??って感じですね。要はXAMLのイベントのプロパティに対しても、マークアップ拡張を適用することができるようになったということのようです。マークアップ拡張まずはマークアップ拡張とはなんぞや、という説明です。マークアップ拡張というのは、XAMLを書いてて時々出てくる、中かっこ{}で括られた部分。BindingとかStaticResourceとか、そういったものを指します。MarkupExtensionを継承するクラスを作ることで、独自のマークアップ拡張を作ることもできます。イベントのマークアップ拡張でできること今まで、XAMLで以下のように普通にイベントの設定をすると、コードビハインドのイベントハンドラ呼び出しを指定することしかできませんでした。VMでICommandを作っておくと、View側のButtonやMenuItemからバインドして実行できるようになりますが、VMで作成したコマンドをバインドすることができるのは、ButtonBase派生の各種ボタン系クラスやMenuItemなど、ICommandSourceを実装している一部のコントロールのみです。それ以外のコントロールや任意のイベントでからVMのコマンドを実行したい、という場合には、イベントハンドラを使うか、BlendSDKの中にあるInvokeCommandActionなどを用いる、というのが一般的な方法だったかと思います。WPF4.5では、このマークアップ拡張をイベントのプロパティにも適用することができるようになりました。独自のマークアップ拡張を作って、VMのコマンド呼び出しを設定することもできるようになります。要はBlendSDKのInvokeCommandActionやCallMethodActionのようなものが、マークアップ拡張を使ってスッキリ書けるようになります。InvokeCommandActionなどと比較してみると、以下のようなメリット/デメリットがあります。ビヘイビアなどとは異なり、BlendのUI上からの設定はできませんが、XAMLの記述は非常にシンプル&インテリセンスも効くので、XAML手書き派の人にはとても便利なのではと思います。実装してみるということで、さっそくイベントのマークアップ拡張を書いてみます。マークアップ拡張の書き方自体についての詳細は、Web上の数多のページにお任せするとして今回は割愛します。MarkupExtensionから派生するクラスを作ります。ここでは、InvokeCommandExtensionというクラスを作成します。独自のMarkupExtensionを作るには、以下二つの手順を行えばOKです。MarkupExtensionを継承するクラスを作成クラス名には、サフィックスとしてExtensionを付けます。マークアップ拡張のクラス名は、XAMLで記述する際に末尾のExtensionという部分は省略して書くことができるため。作成したクラスで、ProvideValueメソッドをオーバーライドする。ProvideValueメソッドProvideValueメソッドでは、イベントハンドラとして実行されるデリゲートを返す必要があります。リフレクションなどを駆使して、イベント発生時に実行すべき内容のデリゲートを作っていきます。対象のイベントの解析まずは、ProvideValueメソッドの引数として渡されるserviceProviderから、マークアップ拡張が書かれた要素についての情報を取得します。つづいて、GetServiceメソッドを呼び出し、IProvideValueTargetを取得します。ここで注意が必要なのですが、この取得した結果のTargetPropertyでは、MethodInfoが渡される場合と、EventInfoが渡される場合とがあります。↓のページによると、対象のイベントが添付イベントか通常のイベントかによって挙動が違うようです。https://connect.microsoft.com/VisualStudio/feedback/details/695888MouseEnterなどのような添付プロパティとして作られているイベントではMethodinfoが渡され、Clickなどのようなコントロール自身が持つイベントの場合はEventInfoが渡されるようです。どちらの場合にも対応できるように、EventInfoとMethodInfo両方でキャストしてみて、変換できた方を使うようにコードを書いています。以下の部分がそのコードです。(これって、C#6のnull伝搬演算子が使えるようになれば、もうちょいスッキリ記述できそうですね。null;コマンドを呼び出すイベントハンドラの作成この部分はリフレクションを多用していて少々複雑ですが、さらっとだけ解説します。イベントハンドラは、一般的に第一引数にobject型のsenderが渡され、第二引数でEventArgs派生の任意の型のパラメータが渡されます。この第二引数の実際の型はイベントごとに異なるため、ジェネリックなメソッドを用意しておきます。前述のEventInfo/MethodInfoから取得したパラメータの型を用いて、MakeGenericMethodメソッドでジェネリックの型引数を固定したイベントハンドラを作成して返してます。genericMethod);コード今回作成したマークアップ拡張は以下の通りです。イベント発生時の呼び出すコマンドのパスを取得または設定します。target引数で渡されたオブジェクトに対し、pathで示されたプロパティをリフレクションを用いて取得します。以下のようにイベントのプロパティで、VMの呼び出したいコマンドを指定するだけです。/>こうすると、MouseEnterイベント発生時に、VMのShowMessageCommandが呼び出されます。イベントの引数を使うCommandParameterを受け付けるようにICommandを実装した場合には、イベントのパラメータを受け取ることもできます。対象イベントのイベントハンドラのEventArgsと同様の型がコマンド実行時に引数としてわたってきます。ファイルをドロップした時に発生するイベント、Dropイベント発生時にコマンドを実行するように以下のようにXAMLを書いたとします。このとき、以下のように作ったコマンドのインスタンスをVMのDropFileプロパティに設定しておくと、Dropイベントの引数をコマンドで使用することができます。}※ここでは説明のためにICommandを直接実装したコマンドのクラスを作りましたが、RelayCommandやDelegateCommandなどと呼ばれる、ICommandの汎用的な実装と一緒に使った方がよいかと思います。サンプルせっかくなので、このInvokeCommandマークアップ拡張を使い、XAMLから各種イベント発生時にてVMのコマンドを呼び出すサンプルを作ってみました。VMの実装にはBindableBase/RelayCommandというヘルパークラスを併用しています。BindableBaseクラスは、INotifyPropertyChangedを実装するためのベースクラス、RelayCommandはICommandの汎用的な実装です。prismやMvvmLightToolkitなどでも同じようなコードがあるかと思いますが、そういうよくある実装と同じものを使っていただければ問題ありません。ここでは以下二つの記事で書いたBindableBase/RelayCommandというクラスを使っています。Win8/Win8.1のストアアプリのテンプレートから拝借してきたコードをちょこっと改変したものです。SourceChordプロジェクト一式を見たい場合は、この記事の最後の方に書いたCodeplexのページを見てください。実行すると以下のような画面が表示されます。以下二つのイベント発生時に、VMのShowMessageCommandを実行します。このコマンドが実行されると、メッセージボックスの表示が行われます。・TextBoxのGotFocusイベント・RectangleのMouseEnterイベント「コマンドが実行された」ということを分かりやすく示すために、VMからメッセージボックスの表示を行ってます。MVVM的にはお行儀悪いですね。画面右側では、ファイルドロップ時のイベントを受け付けるサンプルとなっています。Imageコントロールにエクスプローラなどから画像ファイルをドロップすると、表示する画像が切り替わります。Imageコントロールには以下3か所の設定をしています。True・・・こいつをTrueにしておくことで、コントロールがファイルドロップを受け付けるようになります。VMのImagePathプロパティにバインド。※ImagePathがnullの場合には、デフォルトの画像を表示するようにTargetNullValueの設定を行っています。(Imageコントロールが何も表示してないと、Width/Heightが0になってしまって、ファイルドロップを受け付けるUIの領域がなくなってしまうので。今回作成したInvokeCommandマークアップ拡張でVMのDropFileCommandを呼び出します。DropFileCommandコマンドは、DropEventArgs型の引数を受け付けるコマンドとして実装しています。コマンドが実行されると、ドロップされたファイルのパスをImagePathに設定します。Text="GotFocusイベントが発生すると、ShowMessageCommandを呼び出します。Text="MouseEnterイベントが発生すると、ShowMessageCommandを呼び出します。表示する画像のパスを取得または設定します。今回実装したInvokeCommandExtensionクラスにはいくつかの制限があります。DataContextの変更に追従しないProvideValueが呼ばれた時点でのDataContextから呼び出すコマンドの設定などをしています。そのため、実行時にDataContextを入れ替えたりした場合に、その変更に追従せず、元のDataContextのコマンドを呼び出してしまいます。対策としては、ProvideValueメソッド内で、DataContextChangedイベントのイベントハンドラを設定しておき、DataContextChangedが起きたら、呼び出すコマンドなどをの再設定を行うようにすればよいかな、と思います。この辺はまた近い内に対処してアップデートしていきたいと思います。コレクション走査パスでのバインディングには未対応ListBoxのItemsSourceにコレクションをバインドした場合などによく使う、選択した項目の要素を取得する以下の構文には対応していません。(/を付けて、リストで選択中の要素を指定する構文)EventArgsの使用には注意これは制限というより、注意すべき点ですが、、、ここで作成したInvokeCommandExtensionでは、イベントのEventArgsをCommand呼び出し時のパラメータとして渡しています。そのため、VM側のコマンドでEventArgsを使うことができます。RoutedEventArgsなどではメンバにSourceプロパティを持つので、View側の要素にいとも容易く触れることができてしまいます。気を付けないと、Vにベッタリと依存したVMになってしまうので、使う際には意識しておいた方がよいかと思います。サンプルコードここで作ったマークアップ拡張と、それを利用したサンプルコードを、Codeplexに上げておきました。興味のある方は是非ご覧になってください。書いてた不具合も、以下の通り直しています。DataContextの変更に追従するようにしました。コレクション走査パスでのバインディングに対応しました。参考にしたページこの記事を書くにあたって、以下のページを参考にさせていただきました。Blog上記のページを見て、WPFでこんなことできるのか!?と随分驚きました。今回の記事は基本的には、この辺の海外のサイトの記事を見て、自分なりに解釈した結果をまとめたものです。かずきのBlog@hatenaSilverlight5では、実は同じようなことができてたみたいですね。ProvideValueの実装方法などを参考にさせてもらってます。CalendarはP3PPPさんです。Basicな構成のプロジェクトを見ていきます。アプリ開発Windows開発者はてなブログをはじめよう!minami_SCさんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。