ささいなことですが。

Windowsアプリテスト自動化ライブラリFriendly開発者の日記です。

ViewModelをシンプルに書きたい! - ③コマンド接続編 -

コマンドはReactiveCommand使えばこんな感じに書けます。シンプルですね。
ishikawa-tatsuya.hatenablog.com

でも、Commandも直接メソッドにつなぎたいんですよ。

前回はそれをイベントでつなぎました。今回はコマンドでつなげてみます。
常にCanExecuteがEnableな時はこんな感じ。コマンドパラメータを受け取る場合はobjectを受け取るという仕様

using Reactive.Bindings;
using System.Reactive.Linq;

namespace WpfApp
{
    public class MainWindowVM
    {
        public ReactiveProperty<string> Number { get; } = new ReactiveProperty<string>("0");
        public void Increment()
        {
            Number.Value = (int.Parse(Number.Value) + 1).ToString();
        }
        public void SetNumber(object param)
        {
            Number.Value = (string)param;
        }
    }
}

Xamlです。{c:Command }というXaml拡張で実現しています。

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:l="clr-namespace:WpfApp"
        xmlns:c="clr-namespace:VVMConnection;assembly=VVMConnection"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <l:MainWindowVM/>
    </Window.DataContext>

    <StackPanel>
        <StackPanel Orientation="Horizontal">
            <TextBox Width="100" Text="{Binding Number.Value, UpdateSourceTrigger=PropertyChanged}"/>
            <Button Content="インクリメント" Command="{c:Command Increment }"/>
            <Button Content="5をパラメータで渡す" Command="{c:Command SetNumber }" CommandParameter="5"/>
        </StackPanel>
    </StackPanel>
</Window>

CanExecuteを使いたい場合はReactivPropertyを指定するという仕様にしました。

using Reactive.Bindings;
using System.Reactive.Linq;

namespace WpfApp
{
    public class MainWindowVM
    {
        public ReactiveProperty<string> Number { get; } = new ReactiveProperty<string>("0");
        public ReactiveProperty<bool> CanIncrement { get; }

        public MainWindowVM()
        {
            int num = 0;
            CanIncrement = Number.Select(e => int.TryParse(e, out num)).ToReactiveProperty();
        }

        public void Increment()
        {
            Number.Value = (int.Parse(Number.Value) + 1).ToString();
        }
    }
}
<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:l="clr-namespace:WpfApp"
        xmlns:c="clr-namespace:VVMConnection;assembly=VVMConnection"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <l:MainWindowVM/>
    </Window.DataContext>

    <StackPanel>
        <StackPanel Orientation="Horizontal">
            <TextBox Width="100" Text="{Binding Number.Value, UpdateSourceTrigger=PropertyChanged}"/>
            <Button Content="インクリメント" Command="{c:Command Increment, CanIncrement }"/>
        </StackPanel>
    </StackPanel>
</Window>

{c:Command}の実装

これもXaml拡張で実現してます。MarkupExtension楽しいですねw

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
using System.Windows.Markup;

namespace VVMConnection
{
    public class CommandExtension : MarkupExtension
    {
        string _method;
        string _enableProperty;

        public CommandExtension(string method, string enableProperty)
        {
            _method = method;
            _enableProperty = enableProperty;
        }

        public CommandExtension(string method)
        {
            _method = method;
        }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            var targetProvider = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
            if (targetProvider == null)
            {
                return null;
            }
            var target = targetProvider.TargetObject as FrameworkElement;
            if (target == null)
            {
                return null;
            }
            //コマンドを生成
            return new CommandBridge(target, _method, _enableProperty);
        }

        class CommandBridge : ICommand
        {
            public event EventHandler CanExecuteChanged = (_,__)=> { };

            Action _invoke;
            Action<object> _invokeParam;
            dynamic _enable;

            internal CommandBridge(FrameworkElement target, string method, string enableProperty)
            {
                var connect = MakeConnectAction(target, method, enableProperty);
                connect();
                target.DataContextChanged += (_, __) => connect();
            }

            public bool CanExecute(object parameter)=> _enable == null ? true : _enable.Value;

            public void Execute(object parameter)
            {
                if (_invoke != null) _invoke();
                else _invokeParam(parameter);
            }

            Action MakeConnectAction(FrameworkElement target, string method, string enableProperty)
            {
                return () =>
                {
                    var vm = target.DataContext;
                    if (vm == null)
                    {
                        return;
                    }

                    //Executeで呼び出す実行メソッド取得
                    var vmType = vm.GetType();
                    var methodInfo = vmType.GetMethod(method);
                    if (methodInfo == null)
                    {
                        return;
                    }
                    var args = methodInfo.GetParameters();
                    if (args.Length == 0)
                    {
                        //引数なし
                        _invoke = () => methodInfo.Invoke(vm, new object[0]);
                    }
                    else if (args.Length == 1 && args[0].ParameterType == typeof(object))
                    {
                        //パラメータを受け取る
                        _invokeParam = p => methodInfo.Invoke(vm, new object[] { p });
                    }
                    else
                    {
                        return;
                    }
                    if (string.IsNullOrEmpty(enableProperty))
                    {
                        return;
                    }

                    //CanExecuteは
                    //INotifyPropertyChangedを実装していて
                    //bool Valueというプロパティーがあるオブジェクトで指定
                    //ReactivePropertyでもいいし、そんな感じのクラスならOKってことにしました。
                    var enablePropertyInfo = vmType.GetProperty(enableProperty);
                    if (enablePropertyInfo == null)
                    {
                        return;
                    }

                    var enable = enablePropertyInfo.GetValue(vm) as INotifyPropertyChanged;
                    if (enable == null)
                    {
                        return;
                    }
                    Type enableType = enable.GetType();
                    var valueProperty = enableType.GetProperty("Value");
                    if (valueProperty == null || valueProperty.PropertyType != typeof(bool))
                    {
                        return;
                    }
                    enable.PropertyChanged += (_, __) => CanExecuteChanged(this, EventArgs.Empty);
                    _enable = enable;
                };
            }
        }
    }
}

連載で使っているMarkup拡張のコードはこちらにおいてます。

github.com