今回は、ICommandインターフェースを使用して、ビューとビューモデル間のコマンド通知の方法について見ていきます。
従来のWindowsアプリケーションでは、ビュー側(画面)の状態の通知はイベントによって行われています(例:ボタンをクリックしたらクリックイベントが発生する)。
Silverlight でもイベントによる状態の通知は行うことができるので、ボタンがクリックされたらクリックイベントを発生させてその中でデータ処理を行うことは可能です。
MVVMモデルの場合は、モデル、ビュー、ビューモデルに分離して考え、状態の通知はイベントではなくコマンドをバインディングするという手法で行うのが一般的です(イベントを使用することも当然可能です)。
モデル、ビューモデル間のバインディングの関係を図に表すと以下のようになります。
INotifyPropertyChangedインターデースを使用したデータのバインドに関してはクラスのインスタンスをバインドする3 ~データを更新する~ の記事を参照してください。
サンプルプロジェクトを作成する
今回作成するサンプルプログラムは、「ボタンがクリックされたら挨拶を表示する」というものです。
「ボタンがクリックされたら」という部分をコマンドのバインディングによって行います。
最初に今回のサンプルプログラムの実行例を以下に示します。
Visual Studioを起動し、Silverlightアプリケーションを作成します。
プロジェクト名は、C#はCommandSampleCS,、VB.NETはCommandSampleVBとします。
画面のデザイン
MainPage.xaml のデザインは以下のようにします。
XAMLのコード
<Grid x:Name="LayoutRoot" Background="White"> <TextBox HorizontalAlignment="Left" Height="23" Margin="10,10,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120" Text="{Binding Name, Mode=TwoWay}"/> <Button Content="挨拶する" HorizontalAlignment="Left" Margin="10,38,0,0" VerticalAlignment="Top" Width="75""/> </Grid>
まずはデータバインディング
最初にビューモデルを作成します。
クラス名を ViewModel とし、以下のようにコードを記述します。
VB.NET
Imports System.ComponentModel Public Class ViewModel Implements INotifyPropertyChanged Private _name As String Public Property Name() As String Get Return _name End Get Set(ByVal value As String) '値に変更があったかどうか If _name <> value Then _name = value '値に変更があったので、変更のあったプロパティ名を通知 RaisePropertyChanged("Name") End If End Set End Property ''' <summary> ''' プロパティ変更用イベント ''' </summary> ''' <param name="sender"></param> ''' <param name="e"></param> ''' <remarks></remarks> Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged ''' <summary> ''' プロパティ変更通知 ''' </summary> ''' <param name="propertyName"></param> ''' <remarks></remarks> Private Sub RaisePropertyChanged(propertyName As String) RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName)) End Sub End Class
C#
using System.Windows; using System.Windows.Input; using System.ComponentModel; namespace CommandSampleCS { public class ViewModel : INotifyPropertyChanged { private string _name; /// <summary> /// Nameプロパティ /// XAMLページの TextBox へバインドする値 /// </summary> public string Name { get { return _name; } set { // 値に変更があったかどうか if (_name != value) { _name = value; // 値に変更があったので、変更のあったプロパティ名を通知 RaisePropertyChanged("Name"); } } } /// <summary> /// プロパティ変更通知用イベント /// </summary> public event PropertyChangedEventHandler PropertyChanged; /// <summary> /// プロパティ変更通知処理 /// </summary> /// <param name="propertyName"></param> protected void RaisePropertyChanged(string propertyName) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } }
ViewModel クラスは INotifyPropertyChangedインターフェースを実装します
クラスには以下を実装しています。
- Nameプロパティ
XAMLページに配置した TextBoxのTextプロパティ用 - PropertyChangedイベント
プロパティの変更があった場合に通知を行います。このクラスではNameプロパティの編個通知用として使用します。 - RaisePropertyChangedメソッド
Nameプロパティに変更があった場合にこのメソッドを呼び出し、PropertyChangedイベントを実行します。
以上ができたら、MainPage の DataContext にバンディングします。
VB.NET(MainPage.xaml.vb)
Partial Public Class MainPage Inherits UserControl Public Sub New() InitializeComponent() Me.DataContext = New ViewModel() End Sub End Class
C#(MainPage.xaml.cs)
using System.Windows.Controls; namespace CommandSampleCS { public partial class MainPage : UserControl { public MainPage() { InitializeComponent(); this.DataContext = new ViewModel(); } } }
コマンドバインディング
ビューからビューモデルにコマンドを通知するために、ICommandインターフェースを実装した DelegateCommandという名前のクラスを作成します。
VB.NET(DelegateCommand.vb)
Public Class DelegateCommand Implements ICommand Public Property ExecuteHandler As Action(Of Object) Public Property CanExecuteHandler As Func(Of Object, Boolean) Public Event CanExecuteChanged(sender As Object, e As EventArgs) Implements ICommand.CanExecuteChanged ''' <summary> ''' 実行できるかどうかの判定 ''' </summary> ''' <param name="parameter"></param> ''' <returns></returns> ''' <remarks></remarks> Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute Dim d = CanExecuteHandler ' d(parameter) は GreetingCommandExecuteメソッドの実行 ' GreetingCommandCanExecuteメソッドは TextBoxが空の場合は falseを返してくる Return IIf(IsNothing(d), True, d(parameter)) End Function ''' <summary> ''' コマンドの実行 ''' </summary> ''' <param name="parameter"></param> ''' <remarks></remarks> Public Sub Execute(parameter As Object) Implements ICommand.Execute Dim d = ExecuteHandler If Not IsNothing(d) Then ' ViewModel の GreetingCommandExecute を実行 d(parameter) End If End Sub ''' <summary> ''' 実行確認メソッドの起動 ''' </summary> ''' <remarks></remarks> Public Sub RaiseCanExecuteChanged() ' CanExecuteメソッドの起動 RaiseEvent CanExecuteChanged(Me, Nothing) End Sub End Class
C#(DelegateCommand.cs)
using System; using System.Windows.Input; namespace CommandSampleCS { public class DelegateCommand : ICommand { public Action<object> ExecuteHandler { get; set; } public Func<object, bool> CanExecuteHandler { get; set; } public event EventHandler CanExecuteChanged; /// <summary> /// 実行できるかどうかの判定 /// </summary> /// <param name="parameter"></param> /// <returns></returns> public bool CanExecute(object parameter) { var d = CanExecuteHandler; // d(parameter) は GreetingCommandExecuteメソッドの実行 // GreetingCommandCanExecuteメソッドは TextBoxが空の場合は falseを返してくる return d == null ? true : d(parameter); } /// <summary> /// コマンドの実行 /// </summary> /// <param name="parameter"></param> public void Execute(object parameter) { var d = ExecuteHandler; if (d != null) // ViewModel の GreetingCommandExecute を実行 d(parameter); } /// <summary> /// 実行確認メソッドの起動 /// </summary> public void RaiseCanExecuteChanged() { // CanExecuteメソッドの起動 CanExecuteChanged(this, null); } } }
DelegateCommandクラスでは、コマンドを実行できるかどうかの判定と、コマンドの実行を担当するクラスです。
詳しい解説は後回しにして、今作成した DelegateCommand クラスを ViewModel で使用するよう改造をします。
ViewModelクラスの改造
DelegateCommandクラスができたので、最初に作成したViewModelクラスを改造します。
今回のサンプルプログラムでは、
- テキストボックスに1文字も入力されていない場合はボタンをクリックできないようにする
- ボタンがクリックされたときにメッセージボックスを表示する
という機能を持たせることとします。
ソースコードは以下の通りです。
VB.NET(ViewModel.vb 改造後完全ソース)
Imports System.ComponentModel Public Class ViewModel Implements INotifyPropertyChanged Private _name As String Public Property Name() As String Get Return _name End Get Set(ByVal value As String) '値に変更があったかどうか If _name <> value Then _name = value '値に変更があったので、変更のあったプロパティ名を通知 RaisePropertyChanged("Name") 'コマンドを実行できるかどうかをチェックする CType(GreetingCommand, DelegateCommand).RaiseCanExecuteChanged() End If End Set End Property ''' <summary> ''' プロパティ変更用イベント ''' </summary> ''' <param name="sender"></param> ''' <param name="e"></param> ''' <remarks></remarks> Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged ''' <summary> ''' プロパティ変更通知 ''' </summary> ''' <param name="propertyName"></param> ''' <remarks></remarks> Private Sub RaisePropertyChanged(propertyName As String) RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName)) End Sub ''' <summary> ''' コマンドを実行 ''' ボタンがクリックされたときに実行する ''' </summary> ''' <param name="parameter"></param> ''' <remarks></remarks> Private Sub GreetingCommandExecute(parameter As Object) MessageBox.Show("こんにちは" + _name + "さん") End Sub Private Function GreetingCommandCanExecute(parameter As Object) As Boolean '_nameプロパティが 「null以外」または「空以外」であれば true を返し実行可能とする Return Not String.IsNullOrEmpty(_name) End Function Private _greetingCommand As ICommand ''' <summary> ''' 挨拶実行コマンドプロパティ ''' 挨拶を実行するコマンド(GreetingCommandExecute)と ''' コマンドを実行できるかどうかのチェック(GreetingCommandCanExecute) ''' を_greetingCommandにセット ''' </summary> ''' <value></value> ''' <returns></returns> ''' <remarks></remarks> Public ReadOnly Property GreetingCommand() As ICommand Get If _greetingCommand Is Nothing Then _greetingCommand = New DelegateCommand() With { .ExecuteHandler = AddressOf GreetingCommandExecute, .CanExecuteHandler = AddressOf GreetingCommandCanExecute } End If Return _greetingCommand End Get End Property End Class
C#(ViewModel.vb 改造後完全ソース)
using System.Windows; using System.Windows.Input; using System.ComponentModel; namespace CommandSampleCS { public class ViewModel : INotifyPropertyChanged { private string _name; /// <summary> /// Nameプロパティ /// XAMLページの TextBox へバインドする値 /// </summary> public string Name { get { return _name; } set { // 値に変更があったかどうか if (_name != value) { _name = value; // 値に変更があったので、変更のあったプロパティ名を通知 RaisePropertyChanged("Name"); // コマンドを実行できるかどうかをチェックする ((DelegateCommand)GreetingCommand).RaiseCanExecuteChanged(); } } } /// <summary> /// プロパティ変更通知用イベント /// </summary> public event PropertyChangedEventHandler PropertyChanged; /// <summary> /// プロパティ変更通知処理 /// </summary> /// <param name="propertyName"></param> protected void RaisePropertyChanged(string propertyName) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } /// <summary> /// コマンドを実行 /// ボタンがクリックされたときに実行する /// </summary> /// <param name="parameter"></param> private void GreetingCommandExecute(object parameter) { MessageBox.Show("こんにちは" + _name + "さん"); } /// <summary> /// コマンドを実行できるかどうかをチェック /// </summary> /// <param name="parameter"></param> /// <returns></returns> private bool GreetingCommandCanExecute(object parameter) { // _nameプロパティが 「null以外」または「空以外」であれば true を返し実行可能とする return !string.IsNullOrEmpty(_name); } private ICommand _greetingCommand; /// <summary> /// 挨拶実行コマンドプロパティ /// 挨拶を実行するコマンド(GreetingCommandExecute)と /// コマンドを実行できるかどうかのチェック(GreetingCommandCanExecute) /// を_greetingCommandにセット /// </summary> public ICommand GreetingCommand { get { if (_greetingCommand == null) _greetingCommand = new DelegateCommand { ExecuteHandler = GreetingCommandExecute, CanExecuteHandler = GreetingCommandCanExecute, }; return _greetingCommand; } } } }
DelegateCommandクラスの解説
まずは DelegateCommandクラスから見ていきます。
DelegateCommandクラスは、コマンドを実行できるかどうかを判断し、コマンドを実行するという機能を持っています。
CanExecuteメソッドは、コマンドを実行できるかどうかを True/Falseで返します。return 文にある d(parameter)は ViewModelクラスの GreetingCommandCanExecuteメソッドを呼び出してします。これにより TextBoxが空であれば Falseを返し、空以外の場合は True を返します。MainPageに貼り付けられているButtonコントロールには、コマンドが実行できないが伝わるとクリックできない状態に自動で変化します。
Executeメソッドは、コマンドを実行します。ここでいうコマンドとは ViewModelクラスにある GreetingCommandメソッドです。よってメッセージボックスが表示され、挨拶を表示します。
RaiseCanExecuteChanged メソッドは CanExecuteChnangedイベントを発行し、CanExecuteメソッドキックします。
改造後のViewModelクラスの解説
今度は改造後の ViewModelクラスについて見ていきます。
Nameプロパティ では、Nameプロパティに変更があった場合に(要するにTextBoxの入力に変更があった場合)に、コマンドが実行可能かをチェックするよう、DelegateCommandクラスのRaiseCanExecuteChangedを呼び出す処理を加えています(VB.NETは19行目、C#は30行目)。
GreetingCommandExecuteメソッド(VB.NETは49行目、C#は54行目)を新たに追加しています。このメソッドは、コマンドが実行可能な場合に呼び出され、メッセージボックスに挨拶を表示します。
GreetingCommandCanExecuteメソッド(VB.NETは53行目、C#は64行目)を新たに追加しています。このメソッドは、Nameプロパティ(要するにTextBox)が空かどうかを判断し、空の場合には Trueを返します。
GreetingCommandメソッド(VB.NETは69行目、C#は78行目)を新たに追加しています。このメソッドでは プライベート変数 \greetingCommandに DelegateCommandクラスのインス端をセットしています。ExecuteHandlerには、ViewModelクラスの GreetingCommandExecuteメソッドをセットし、 CanExecuteHandlerには、同じくViewModlクラスの GreetingCOmmangCanExecuteメソッドをセットしています。
XAMLページのボタンにコマンドをバインドする
最後に、ここまでに作成したコマンドを XAMLページに配置したボタンにバインドします。
XAMLのコードは以下の通りです。
<Grid x:Name="LayoutRoot" Background="White"> <TextBox HorizontalAlignment="Left" Height="23" Margin="10,10,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120" Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <Button Content="挨拶する" HorizontalAlignment="Left" Margin="10,38,0,0" VerticalAlignment="Top" Width="75" Command="{Binding GreetingCommand}"/> </Grid>
まず Button の XAMLから見ていきます。Command属性に GreetingCommandをバインディングしています。たったこれだけです。クリックイベントは必要ないことがわかります。
そして TextBoxですが、TextプロパティのBindingのところが一部変更になっています。UpdateSourceTrigerプロパティにPropertyChangedをセットしています。これはバインディングソースの更新タイミングを指示するものです。PropertyChangedとしているので、Textプロパティが変更されたときに更新されるようになります。
だいぶ長い記事となってしまいましたが、MVVMではこの基礎を踏まえてコマンドを作成します。
コメント