[Silverlight][バインディング] コマンドバインディングの基礎

今回は、ICommandインターフェースを使用して、ビューとビューモデル間のコマンド通知の方法について見ていきます。


従来のWindowsアプリケーションでは、ビュー側(画面)の状態の通知はイベントによって行われています(例:ボタンをクリックしたらクリックイベントが発生する)。

Silverlight でもイベントによる状態の通知は行うことができるので、ボタンがクリックされたらクリックイベントを発生させてその中でデータ処理を行うことは可能です。

MVVMモデルの場合は、モデル、ビュー、ビューモデルに分離して考え、状態の通知はイベントではなくコマンドをバインディングするという手法で行うのが一般的です(イベントを使用することも当然可能です)。

モデル、ビューモデル間のバインディングの関係を図に表すと以下のようになります。

モデル、ビューモデル間のバインディング

INotifyPropertyChangedインターデースを使用したデータのバインドに関してはクラスのインスタンスをバインドする3 ~データを更新する~ の記事を参照してください。


サンプルプロジェクトを作成する

今回作成するサンプルプログラムは、「ボタンがクリックされたら挨拶を表示する」というものです。

「ボタンがクリックされたら」という部分をコマンドのバインディングによって行います。

最初に今回のサンプルプログラムの実行例を以下に示します。

サンプルプログラムの実行例

Visual Studioを起動し、Silverlightアプリケーションを作成します。

プロジェクト名は、C#はCommandSampleCS,、VB.NETはCommandSampleVBとします。


 画面のデザイン

MainPage.xaml のデザインは以下のようにします。

XAMのデザイン

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ではこの基礎を踏まえてコマンドを作成します。

 

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください