コマンド・オブジェクトを作ってコントロールにバインドする方法と、コントロールの任意のイベントにコマンド・オブジェクトをバインドするヒントを説明する。
powered by Insider.NET
このTIPSでは、データをUIのコントロールにバインドして表示する方法を紹介してきた。その主なメリットは、データ・バインディングによってUIとロジックを分離できることにある。だが、分離したいことはほかにもある。コントロールのイベントとコードビハインドのイベント・ハンドラは密に結合しているが、分離できないだろうか?
データ・バインディングのようにしてイベント・ハンドラを直接バインドできればよいのだが、残念ながらそれはできない。しかし、「コマンド・オブジェクト」をコントロールの「コマンド・プロパティ」にバインドすることで、ある程度は実現可能だ。本稿では、コマンド・オブジェクトを作ってコントロールにバインドする方法と、コントロールの任意のイベントにコマンド・オブジェクトをバインドするヒントを説明する。本稿のサンプルは「Windows Store app samples:MetroTips #47(Windows 8版)」からダウンロードできる。
Windows 8(以降、Win 8)向けのWindowsストア・アプリを開発するには、Win 8とVisual Studio 2012(以降、VS 2012)が必要である。これらを準備するには、第1回のTIPSを参考にしてほしい。本稿では64bit版Win 8 ProとVS 2012 Express for Windows 8を使用している。
「コマンド・オブジェクト」はSystem.Windows.Input名前空間のICommandインターフェイスを実装して作る。実装すべきメンバは次の3つである。
メンバ | 説明 |
---|---|
CanExecuteメソッド | 現在、このコマンドが実行可能かどうかを返す |
Executeメソッド | コマンドの実行時に呼び出されるメソッド |
CanExecuteChangedイベント | CanExecuteメソッドの返値が変わったときに、コマンド・オブジェクトから発生させるイベント |
ICommandインターフェイスで実装すべきメンバ |
コマンド・オブジェクトをバインドされたUIのコントロールは、CanExecuteメソッドを呼び出すことでコマンドを実行可能かどうか識別し、コントロールでイベントが発生したときにExecuteメソッドを呼び出す。コマンド・オブジェクトの内部で実行可能かどうかが変化するとCanExecuteChangedイベントが発生するので、コントロールはそれを受けてタイミングよくCanExecuteメソッドを呼び出すことで、コマンドの状態を確認できる。
UIを構築するためのコントロールには、「コマンド・プロパティ」を持っているものがある。コマンド・プロパティの型はICommandインターフェイスであり、そこには前述のコマンド・オブジェクトをバインドできる。
現在のところ、そのようなコントロールはまだ少ないのだが、例えばButtonコントロール(Windows.UI.Xaml.Controls名前空間)のCommandプロパティがある。これは、ボタンが押されたときにバインドされているコマンドが実行されるものだ。本稿では、これを使ってみよう。
前述のICommandインターフェイスを実装したクラスを作成すればよい。
Executeメソッドの処理内容は、直接記述してもよいし、汎用的に外部から与えてもよい。ここでは、オブジェクト生成時に外部から与えるようにしてみよう。また、CanExecuteメソッドは、本来はロジックがコマンドを受け付けられる状態にあるかどうかを返すのだが、ここではサンプルということで、コマンド・パラメータ(後述)だけを見て判定する。なお、このサンプル・コードでは、CanExecuteメソッドの返値はコマンド・パラメータごとに常に同じ結果になるので、CanExecuteChangedイベントを発火させることはない。
以上を実装すると、次のコードのようになる。
public class SampleCommand : ICommand
{
private Action<string> _execute;
// コンストラクト時に、コマンドで実行するActionデリゲートを渡す
// (これはICommandの条件ではない)
public SampleCommand(Action<string> execute)
{
this._execute = execute;
}
// コマンドを実行する(ICommandの実装)
public void Execute(object parameter)
{
var action = this._execute;
if (action != null)
action(parameter as string);
}
// コマンドが利用可能かどうかを答える(ICommandの実装)
public bool CanExecute(object parameter)
{
// parameterに"button3"という文字列が含まれていたら、そのコマンドは無効とする
var s = parameter as string;
if (s.Contains("button3"))
return false;
return true;
}
// CanExecute状態が変わったときに呼び出されるイベント・ハンドラ(ICommandの実装)
public event EventHandler CanExecuteChanged;
// CanExecute状態を変えたときに呼び出すメソッド (本サンプルでは未使用)
private void RaiseCanExecuteChanged()
{
var h = this.CanExecuteChanged;
if (h != null)
h(null, null);
}
}
Public Class SampleCommand
Implements ICommand
Private _execute As Action(Of String)
' コンストラクト時に、コマンドで実行するActionデリゲートを渡す
' (これはICommandの条件ではない)
Public Sub New(execute As Action(Of String))
Me._execute = execute
End Sub
' コマンドを実行する(ICommandの実装)
Public Sub Execute(parameter As Object) Implements ICommand.Execute
Dim action = Me._execute
If (action IsNot Nothing) Then
action(parameter)
End If
End Sub
' コマンドが利用可能かどうかを答える(ICommandの実装)
Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute
' parameterに"button3"という文字列が含まれていたら、そのコマンドは無効とする
Dim s = CStr(parameter)
If (s Is Nothing OrElse s.Contains("button3")) Then
Return False
End If
Return True
End Function
' CanExecute状態が変わったときに呼び出されるイベント・ハンドラ(ICommandの実装)
Public Event CanExecuteChanged(sender As Object, e As EventArgs) _
Implements ICommand.CanExecuteChanged
' CanExecute状態を変えたときに呼び出すメソッド (本サンプルでは未使用)
Private Sub RaiseCanExecuteChanged()
RaiseEvent CanExecuteChanged(Nothing, Nothing)
End Sub
End Class
コマンド・オブジェクトをバインドする画面を用意する。今回のサンプルはLayoutAwarePageクラスを継承したページにする必要がある*1。その手順は、「WinRT/Metro TIPS:複数のバインディング・ソースを画面にバインドするには?[Win 8]」の*5を参照してほしい。
*1 これは、後ほど、LayoutAwarePageクラスが提供するDefaultViewModelプロパティを利用するからだ。DefaultViewModelプロパティについては「WinRT/Metro TIPS 複数のバインディング・ソースを画面にバインドするには?[Win 8]」をお読みいただきたい。
LayoutAwarePageクラスを継承したページの用意ができたら、次のコードのように、Buttonコントロールを3つと、コマンドの処理結果を表示するためのTextBlockコントロール(Windows.UI.Xaml.Controls名前空間)を1つ配置する。
……省略……
<StackPanel ……省略……>
<!-- button1はコードでバインドする -->
<Button x:Name="button1" Content="button1" CommandParameter="button1 clicked" />
<!-- 残り3つのボタンは、XAMLでバインドする -->
<Button x:Name="button2" Content="button2" Command="{Binding Command}"
CommandParameter="button2 clicked" />
<Button x:Name="button3" Content="button3" Command="{Binding Command}"
CommandParameter="button3 clicked" />
<!-- 処理結果を表示するテキスト・ブロック -->
<TextBlock x:Name="textBlock1" ……省略…… />
</StackPanel>
……省略……
ここで、3つのButtonコントロール全てにCommandParameter属性が定義してある。そこに指定した値がコマンド・パラメータとして、コマンド・オブジェクトのExecuteメソッドとCanExecuteメソッドに渡されるのだ。
また、「button1」には、コードビハインドでコマンド・オブジェクトをバインドする(後述)。「button2」と「button3」には、Command属性に「{Binding Command}」と指定することで、データ・コンテキスト中の「Command」という名前のコマンド・オブジェクトをバインドしている。
なお、コントロールのイベントにイベント・ハンドラを指定する方法では、コードビハインドでイベント・ハンドラが定義されていないとビルド・エラーになる。ところが、上のコードのようにコマンド・オブジェクトをバインドする方法では、指定したコマンド・オブジェクトが存在しなくても、ビルドして実行できる。画面のデザインとロジックの実装を分けて作業する場合、それはメリットになるだろう。
コマンド・オブジェクトを生成し、次いで、それをSourceプロパティに指定したBindingオブジェクトを生成する。コントロールのSetBindingメソッドを使って、そのBindingオブジェクトを結び付ければよい。
ただし、今回のコマンド・オブジェクトは、インスタンス化するときにコマンドで実行する処理を与えるように作ってある。その処理は、コマンド・パラメータをそのままテキスト・ブロックに表示することにしよう(次のコード)。
private void CommandAction(string parameter)
{
textBlock1.Text = parameter;
}
Private Sub CommandAction(parameter As String)
textBlock1.Text = parameter
End Sub
実際にバインドするコードは、次のようになる。
// コマンド・オブジェクトを生成する。実行されるコマンドはCommandActionメソッド。
var cmd = new SampleCommand(s => CommandAction(s));
// button1には、コードでBindingオブジェクトを作ってバインドしてみる
this.button1.SetBinding(Button.CommandProperty, new Binding() { Source = cmd, });
' コマンド・オブジェクトを生成する。実行されるコマンドはCommandActionメソッド。
Dim cmd = New SampleCommand(Sub(s) CommandAction(s))
' button1には、コードでBindingオブジェクトを作ってバインドしてみる
Me.button1.SetBinding(Button.CommandProperty, New Binding() With {.Source = cmd})
これで実行してみよう。「button1」をタップすると、最終的にCommandParameter属性に指定した値を引数としてCommandActionメソッドが呼び出され、テキスト・ブロックに「button1 clicked」と表示されるはずだ。
まずXAMLコードで、コントロールのCommand属性にバインドを指定し、CommandParameter属性にコマンド・パラメータを指定する。そして、コードビハインドでコントロールのデータ・コンテキストにコマンド・オブジェクトを設定する。
XAMLコードの記述については、前述した「サンプル画面」の項で説明した。あとは、コードビハインドでデータ・コンテキストの「Command」という名前の場所にコマンド・オブジェクトを設定すればよい。先ほどの「コードからコマンド・オブジェクトをバインドする」コードに続けて、次のコードを記述する。
//残りのボタンには、XAMLでバインドする
this.DefaultViewModel["Command"] = cmd;
'残りのボタンには、XAMLでバインドする
Me.DefaultViewModel("Command") = cmd
これで実行してみると、「button2」のタップでテキスト・ブロックに「button2 clicked」と表示されるはずだ。
さて、「コマンド・オブジェクトを作るには?」の項で作成したコマンド・オブジェクトのコードには、CanExecuteメソッドがあった。その中身は、コマンド・パラメータに「button3」という文字列が含まれていたらfalse/Falseを返すようになっている。これはどのような効果を発揮するのだろうか?
実行時の画面は、次の画像のようになる。
「button3」が操作できない状態であることが見て取れる。これがCanExecuteメソッドの効果だ。
画面のロード時、コントロールはコマンド・オブジェクトのCanExecuteメソッドを呼び出す。CanExecuteメソッドがfalse/Falseを返した場合、コントロールはそのコマンドが利用できない状態であると認識する。そのときにコントロールの外観をどうするかの決まりはないが、Buttonコントロールではタップ/クリックできない状態を表す外観になるのだ。
そして、コマンド・オブジェクトの状態が変わったときに、コマンド・オブジェクトはCanExecuteChangedイベントを発火させる。バインドされているコントロールは、それを受けて再びCanExecuteメソッドを呼び出し、コントロールの状態を更新するのだ。
以上でコマンド・オブジェクトを作って利用する方法の解説は終わった。Buttonコントロールのタップ/クリック・イベントだけがロジックと分離できても、そのほかさまざまなイベントが分離できないのでは、面倒なコマンド・オブジェクトを作成する意義は薄いのではないかと思われたかもしれない。
さまざまなコントロールのイベントをロジックから分離するには、それらのイベントをそのコントロールの新しいコマンド・プロパティに変換するコードを追加すればよい。例えば、ユーザー・コントロールを作って、新しいコマンド・プロパティを実装するのだ。とはいっても、それも面倒な作業になる。
そこで検討したいのが、MVVMフレームワークだ。「MVVM Light Toolkit」や「Prism for the Windows Runtime」など、いくつかのフレームワークが公開されている。それらのフレームワークでは、V(ビュー)とVM(ビューモデル)を分離するための仕掛けの中に、イベントとイベント・ハンドラを分離する仕組みも持っている。例えば、MVVM Light Toolkitを利用すると、次のようなXAMLコードを記述できる。
<Border ……省略……
xmlns:b="using:Win8Utils.Behaviors"
b:EventToCommand.Event="Tapped"
b:EventToCommand.Command="{Binding Main.NavigateToArticleCommand,
Mode=OneWay,
Source={StaticResourceLocator}}"
b:EventToCommand.CommandParameter="{Binding}">
……省略……
</Border>
ICommandインターフェイスを実装したコマンド・オブジェクトを作成すれば、原理的にはコントロールのイベントとコードビハインドのイベント・ハンドラを分離してコマンド・オブジェクトをバインドできる。ただし、全てを自前で実装するのは手間が掛かるため、MVVMフレームワークの利用を検討するとよい。
ICommandインターフェイスの実装例は、次のドキュメントも参考になる。
Copyright© Digital Advantage Corp. All Rights Reserved.