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; }; } } } }