2011年10月31日月曜日

MVVMでApplicationCommandsのコマンドを使うには?

WPFの定義済みコマンドを使用する方法

WPFには、一般的に使えそうなコマンドを定義したApplicationCommandsクラスがあります。ここにCopyPasteOpenみたいなコマンドが揃っています(同じようなクラスにMediaCommands、NavigationCommands、ComponentCommands、EditingCommandsってのがあります。詳しくは[MSDN]コマンド実行の概要参照)。

ここに定義されているのはRoutedCommandなので、コマンドを実行するコントロール(CommandSource)と処理するコントロール(CommandTarget)を別にすることができるみたい。具体的には、処理するコントロールにCommandBindingを設定すればいいみたい。以下はApplicationCommands.OpenButtonで実行してWindowで処理するイメージとXAML例。

 
<Window>
  (中略)
  <Window.CommandBinding>
    <CommandBinding Command="Open" Executed="DoOpen" />
  </Window.CommandBinding>
  <Grid>
    <Border>
      <Button Command="Open" Content="Open" />
    </Border>
  </Grid>
</Window>
(検証してないので動くかどうかは不明・・・)

DoOpenはViewModel出定義したコマンドのExecutedイベントハンドラ。つまりViewModelが処理用のメソッドを公開すれば、ViewModelでコマンドの処理を実装できるってこと。実際にコマンドが実行されると↓みたいな流れでDoOpenが呼ばれる(多分)。

 

これでApplicationCommandsの定義済みコマンドを使うことが出来ました。

MVVMにおけるCommandBindingの問題点

でもこれMVVMでやっちゃダメだよね。

なんでって、CommandBindingExecutedイベントのイベントハンドラ(ExecutedRoutedEventHandler)の定義って↓でしょ。

public delegate void ExecutedRoutedEventHandler (
 Object sender,
 ExecutedRoutedEventArgs e
)

これのsenderって、コマンドを実行したコントロール(CommandSource)が入るんですよね。そんなものViewModelに渡しちゃダメですよね? ViewModelがコントロールなんて受け取った日には、ViewとViewModel分けた意味がなくなってしまうので。

そこで問題だ。CommandBindingを使わずにどうやってApplicationCommandsのコマンドを使うか?

3択―ひとつだけ選びなさい

  • 答え①ハンサムな私は突如CommandBindingを使わないアイデアをひらめく
  • 答え②Microsoftが新しいクラスを提供してくれる
  • 答え③使えない。現実は非情である。

答え②に○を付けたいが、MicrosoftはMVVM関連ではBehaviorが最近入ったばかり、新たに追加クラスを提供してくれることは期待できない。

やはり答えは……………①しかねえようだ!

全部独自コマンドにすれば問題ないけど、やっぱりOpenとかNewとかは標準で定義されているコマンドを使いたいよね。Trigger&ActionBehavior使って、パラメータ渡さないメソッドにバインディングするような仕組みを作るか。あ、でもドラッグアンドドロップの時どうしよう。。。Object型のパラメータは有りにするか。でもそもそもViewModelが公開していいのはプロパティとコマンドだけだよね。。。メソッドを公開するという仕組み自体がダメか。。。イベントハンドラ付きのプロパティ定義する?でもそれって独自コマンド定義するのとあんまり変わらないような。。。。。。。。。。。

「答え-③ 答え③ 答え③」

どうしたもんかなー

参考

Posted by Picasa

2011年10月22日土曜日

MVVMを図にしてみた

MVVMに則ったクラスの機能分担
ユーザ入力によるイベント発生時の動作
Modelの処理中にプロパティ変更イベントが発生した場合の動作

最近真面目にプログラム書いてるので久々の更新。

WPFで書くにはMVVMというデザインパターン(?)で書くのが定説らしい。で、色々調べてみましたが、MVVM人気ないんですかね。要る要らないとかの宗教論争が多いです。要るか要らないかは使う側が考えるから、提供側はバシッと情報提供頼むよ(To マイクロソフト)。提供元がフラフラしてるからMVVMの考え方もフラフラしてるみたい。その辺は追々書いてみます(やる気の神が降りてきたら…)。

で、勉強も兼ねてMVVMの役割分担を図にしてみました。合ってるかどうかは不明。合ってなくてもいいんだ。まだ考え方がフラフラしてるんだし。MVVMは基本的に3つのクラスで構成されます。VMを複数のクラスに分けたりVを入れ替えたりするかもしれませんが、考え方的にはM-V-VMの3つ。適当に解釈してこれらの役割を書くと以下のとおり。

  • Model: 目に見えない処理をするクラス
  • View: 目に見える処理するクラス
  • ViewModel: Modelを目に見える形に変換するクラス

語弊がありすぎる気がしますが、細かく書くときりがないのでこの程度に。なおViewは目に見える処理しか行わないので、基本的にはXAMLだけ記述することになります。だけどXAMLだけでは書き切れない処理はコードビハインドと言ってC#で書いてもいいそうです。もっとも、コードビハインドの是非こそがMVVMの最大の争点な気もしますけど。その辺はおいおい(やる気の神がry)

図はModel、ViewModel、Viewが横並びになっていますが、ModelはViewModelやViewを気にする必要はありません。ViewModelもViewを意識する必要はありません。「全く意識しないわけではない」と説明されることもありますが、ありません。あってはならないのです。多分ないです。その説明は追々(やる気のry)。MVVMは各クラスで役割分担を明確にするために用いられるため、不要な参照はしません。ViewModelがViewを全く知らなくても成り立ってしまうなんて。マイクロソフトに、そしてデータバインディングに感謝。

ViewModelはModelのインスタンスを、ViewはViewModelのインスタンスを持ちます。なお、図には書いていませんがViewModelが持つModelのインスタンスは非公開ですのでお間違いなく。ViewがModelを触れるなんてことはありません。また、ViewModelもプロパティとコマンドしか公開しないので、ViewがViewModelを操作することもできません。Viewは飽くまでも表示だけです。

で、MVVMにすると何がいいのかってことですが、言わなきゃわからん奴は使わなくていいよ。データバインドとコマンドバインドのおかげで表示部(View)と演算部(ModelとひょっとしたらViewModel)を完全に分離できてるんだぞ。View取り替えるだけで表示部を変えられるなんてステキじゃないか。

VはMやVMと切り離されている(灰色は壁です)ので、V1をV2に取り替えてもOK(V2も同じVMに対応している前提)

「そんなのデータテンプレートでもできるだろ?」って思ったあなた、Viewを完全に独立させるには↓みたいにイベントハンドラ内でViewを参照してはダメなんですが大丈夫でしょうか。

public void button1_Click(object sender, EventArgs e)
{
  // ボタンが押されたらTextBox1の文字列で検索実行
  string word = TextBox1.Text; // ←TextBox1というViewの名前を使用しているのでMVVM的にはNG
  this.search(word);
}

「イベントハンドラでViewの値取れないとか無理ゲーじゃね?」と思ったあなた、そもそもその発想がMVVM的ではないのですよ。MVVMではModelが演算データの原本で、ViewModelが表示データの原本なんですよ。Viewはそれを表示してるだけなんだから、ViewModel(=コマンド規定側≒イベントハンドラ側)にはコマンド(≒イベント)を処理するためのデータは全てあるのですよ。なきゃおかしいのです。

「じゃあウィンドウの状態とかドラッグアンドドロップされたファイルのパスみたいにView操作契機で生じるデータ知りたいときどうするの?」と思ったあなた、長くなるので続きはまた今度(やる気がry)。端的に言うとTriggerやらActionやらBehaviorやらMessengerやらを使うとなんでもできるらしいです。

で、このTrigger、Action、Behavior、MessengerがMVVMの気に食わないところなんですよね。せっかくの美しいMVVMの概念をややこしくしている気がして。

久々の長文だったから疲れた。

Posted by Picasa