SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

XAMLからViewModelのメソッドにバインドする~RelayCommand~

RelayCommand/DelegateCommand

WPFなどのXAMLを用いたフレームワークでは、Commandを使ってXAMLからViewModelのメソッドを呼び出すためには、DelegateCommandとかRelayCommandと呼ばれるクラスを作ります。で、VMにこのコマンドのインスタンスを作成してXAML側からバインドします。

この辺の説明は、以下のサイトに詳しく説明されてます。
http://www.atmarkit.co.jp/ait/articles/1011/09/news102_3.html


話は変わりますが、Windows8.1のストアアプリ用のプロジェクトのテンプレートには、
RelayCommandというものが追加されてました。
ということで、このRelayCommandをWPFでも使用してみたいと思います。
というか、このクラス。そのままWPFのプロジェクトにコピペして使用できます。

Windows8.1ストアアプリのRelayCommandをWPFで使ってみる

まずは、↓の画像のようなプロジェクトを用意します。
f:id:minami_SC:20140113193320p:plain
MainWindow一つと、このウィンドウ用のVMとなるMainWindowViewModelクラスを追加。
あと、Commonフォルダには、Win8.1ストア用のRelayCommandクラスとBindableBaseクラスを追加します。
BindableBaseは、↓の記事で書いたWin8ストアアプリ用のコードを流用したものです。
http://sourcechord.hatenablog.com/entry/20130303/1362315081

サンプルコード

BindableBaseは前述の記事のコードのまま。
RelayCommandはWin8.1のコードをそのままコピペなので省略します。

ボタンを押したら、VMのコマンドを実行してTextBlockの文字列を変更するだけのサンプルです。
f:id:minami_SC:20140113193335p:plain:w250

MainWindow.xaml
<Window x:Class="RelayCommandTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        Width="300"
        Height="200">
    <Grid>
        <TextBlock Margin="10,10,0,0"
                   HorizontalAlignment="Left"
                   VerticalAlignment="Top"
                   Text="{Binding Message}"
                   TextWrapping="Wrap" />
        <Button Width="75"
                Margin="10,30,0,0"
                HorizontalAlignment="Left"
                VerticalAlignment="Top"
                Command="{Binding ChangeMessageCommand}"
                Content="Button" />
    </Grid>
</Window>
MainWindow.xaml.cs
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new MainWindowViewModel();
        }
    }
MainWindowViewModel.cs
    class MainWindowViewModel : BindableBase
    {
        private string message;

        public string Message
        {
            get { return message; }
            set { this.SetProperty(ref this.message, value); }
        }

        #region コマンドの実装
        private RelayCommand changeMessageCommand;
        public RelayCommand ChangeMessageCommand
        {
            get { return changeMessageCommand = changeMessageCommand ?? new RelayCommand(ChangeMessage); }
        }

        private void ChangeMessage()
        {
            this.Message = "コマンドを実行しました。";
        }
        #endregion


        public MainWindowViewModel()
        {
            this.Message = "初期値です。";
        }
    }

任意の型の引数を受け付けるようにする

Win8.1のRelayCommandは、引数なしのAction型のメソッドを実行するのみで、CommandParameterとして渡される引数を使用することはできません。
そこで、↓の記事を参考に、任意の型の引数を受け付けるRelayCommandを作成しました。
http://okazuki.hatenablog.com/entry/20100223/1266897125

RelayCommand.cs

通常のRelayCommandの後に、RelayCommandクラスを追加しました。

    /// <summary>
    /// その機能を中継することのみを目的とするコマンド
    /// デリゲートを呼び出すことにより、他のオブジェクトに対して呼び出します。
    ///CanExecute メソッドの既定の戻り値は 'true' です。
    /// <see cref="RaiseCanExecuteChanged"/> は、次の場合は必ず呼び出す必要があります。
    /// <see cref="CanExecute"/> は、別の値を返すことが予期されます。
    /// </summary>
    public class RelayCommand : ICommand
    {
        private readonly Action _execute;
        private readonly Func<bool> _canExecute;

        /// <summary>
        /// RaiseCanExecuteChanged が呼び出されたときに生成されます。
        /// </summary>
        public event EventHandler CanExecuteChanged;

        /// <summary>
        /// 常に実行可能な新しいコマンドを作成します。
        /// </summary>
        /// <param name="execute">実行ロジック。</param>
        public RelayCommand(Action execute)
            : this(execute, null)
        {
        }

        /// <summary>
        /// 新しいコマンドを作成します。
        /// </summary>
        /// <param name="execute">実行ロジック。</param>
        /// <param name="canExecute">実行ステータス ロジック。</param>
        public RelayCommand(Action execute, Func<bool> canExecute)
        {
            if (execute == null)
                throw new ArgumentNullException("execute");
            _execute = execute;
            _canExecute = canExecute;
        }

        /// <summary>
        /// 現在の状態でこの <see cref="RelayCommand"/> が実行できるかどうかを判定します。
        /// </summary>
        /// <param name="parameter">
        /// コマンドによって使用されるデータ。コマンドが、データの引き渡しを必要としない場合、このオブジェクトを null に設定できます。
        /// </param>
        /// <returns>このコマンドが実行可能な場合は true、それ以外の場合は false。</returns>
        public bool CanExecute(object parameter)
        {
            return _canExecute == null ? true : _canExecute();
        }

        /// <summary>
        /// 現在のコマンド ターゲットに対して <see cref="RelayCommand"/> を実行します。
        /// </summary>
        /// <param name="parameter">
        /// コマンドによって使用されるデータ。コマンドが、データの引き渡しを必要としない場合、このオブジェクトを null に設定できます。
        /// </param>
        public void Execute(object parameter)
        {
            _execute();
        }

        /// <summary>
        /// <see cref="CanExecuteChanged"/> イベントを発生させるために使用されるメソッド
        /// <see cref="CanExecute"/> の戻り値を表すために
        /// メソッドが変更されました。
        /// </summary>
        public void RaiseCanExecuteChanged()
        {
            var handler = CanExecuteChanged;
            if (handler != null)
            {
                handler(this, EventArgs.Empty);
            }
        }
    }


    /// <summary>
    /// 任意の型の引数を1つ受け付けるRelayCommand
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class RelayCommand<T> : ICommand
    {
        private readonly Action<T> _execute;
        private readonly Func<T, bool> _canExecute;

        /// <summary>
        /// RaiseCanExecuteChanged が呼び出されたときに生成されます。
        /// </summary>
        public event EventHandler CanExecuteChanged;

        /// <summary>
        /// 常に実行可能な新しいコマンドを作成します。
        /// </summary>
        /// <param name="execute">実行ロジック。</param>
        public RelayCommand(Action<T> execute)
            : this(execute, null)
        {
        }

        /// <summary>
        /// 新しいコマンドを作成します。
        /// </summary>
        /// <param name="execute">実行ロジック。</param>
        /// <param name="canExecute">実行ステータス ロジック。</param>
        public RelayCommand(Action<T> execute, Func<T, bool> canExecute)
        {
            if (execute == null)
                throw new ArgumentNullException("execute");
            _execute = execute;
            _canExecute = canExecute;
        }

        /// <summary>
        /// 現在の状態でこの <see cref="RelayCommand"/> が実行できるかどうかを判定します。
        /// </summary>
        /// <param name="parameter">
        /// コマンドによって使用されるデータ。コマンドが、データの引き渡しを必要としない場合、このオブジェクトを null に設定できます。
        /// </param>
        /// <returns>このコマンドが実行可能な場合は true、それ以外の場合は false。</returns>
        public bool CanExecute(object parameter)
        {
            return _canExecute == null ? true : _canExecute((T)parameter);
        }

        /// <summary>
        /// 現在のコマンド ターゲットに対して <see cref="RelayCommand"/> を実行します。
        /// </summary>
        /// <param name="parameter">
        /// コマンドによって使用されるデータ。コマンドが、データの引き渡しを必要としない場合、このオブジェクトを null に設定できます。
        /// </param>
        public void Execute(object parameter)
        {
            _execute((T)parameter);
        }

        /// <summary>
        /// <see cref="CanExecuteChanged"/> イベントを発生させるために使用されるメソッド
        /// <see cref="CanExecute"/> の戻り値を表すために
        /// メソッドが変更されました。
        /// </summary>
        public void RaiseCanExecuteChanged()
        {
            var handler = CanExecuteChanged;
            if (handler != null)
            {
                handler(this, EventArgs.Empty);
            }
        }
    }

RelayCommand+任意の型の引数のサンプル

f:id:minami_SC:20140113195832p:plain:w250
同一のコマンドにバインドしたButtonを三つ配置し、それぞれのボタンに別々のCommandParameterを設定しています。
で、コマンドを実行したら、CommandParameterの内容を表示するサンプルです。

MainWindow.xaml
<Window x:Class="RelayCommandTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        Width="300"
        Height="200">
    <Grid>
        <TextBlock Margin="10,10,0,0"
                   HorizontalAlignment="Left"
                   VerticalAlignment="Top"
                   Text="{Binding Message}"
                   TextWrapping="Wrap" />
        <Button Width="75"
                Margin="10,30,0,0"
                HorizontalAlignment="Left"
                VerticalAlignment="Top"
                Command="{Binding ChangeMessageCommand}"
                CommandParameter="ボタン1"
                Content="Button1" />
        <Button Width="75"
                Margin="10,54,0,0"
                HorizontalAlignment="Left"
                VerticalAlignment="Top"
                Command="{Binding ChangeMessageCommand}"
                CommandParameter="ボタン2"
                Content="Button2" />
        <Button Width="75"
                Margin="10,78,0,0"
                HorizontalAlignment="Left"
                VerticalAlignment="Top"
                Command="{Binding ChangeMessageCommand}"
                CommandParameter="ボタン3"
                Content="Button3" />
    </Grid>
</Window>
MainWindowViewModel.cs
    class MainWindowViewModel : BindableBase
    {
        private string message;

        public string Message
        {
            get { return message; }
            set { this.SetProperty(ref this.message, value); }
        }

        #region コマンドの実装
        private RelayCommand<string> changeMessageCommand;
        public RelayCommand<string> ChangeMessageCommand
        {
            get { return changeMessageCommand = changeMessageCommand ?? new RelayCommand<string>(ChangeMessage); }
        }

        private void ChangeMessage(string parameter)
        {
            this.Message = "CommandParameter: " + parameter ;
        }
        #endregion

        public MainWindowViewModel()
        {
            this.Message = "初期値です。";
        }
    }