Windows Forms で MVVM 2 (ReactiveProperty 編)


前書き

前回の Windows Forms で MVVM では PropertySetter, Command, Binder の三つのヘルパークラスを作って Windows Forms で MVVM を実装する方法をお伝えしました。
PropertySetterINotifyPropertyChanged を簡単に実装するためのクラス、Command は UI からのアクションを実装するクラス、Binder はコントロールとデータを簡単にバインドするためのクラスでした。

しかし、ReactiveProperty を使用すればもっとシンプルにできます。
ReactiveProperty を使うとプロパティ自体が IObservable<T> を実装するので、ViewModelINotifyPropertyChanged を実装する必要がありません。また ReactiveCommand が実装されているので、Command を作る必要がありません。
したがって、三つのヘルパークラスのうち、Binder を除いて二つが不要になるということです。

ReactiveProperty のインストール

NuGet パッケージマネージャーを開いて、Install-Package ReactiveProperty と実行してください。

Binder

Binder.cs

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using System.Windows.Forms;
using Reactive.Bindings;

namespace FormsMvvm
{
    public static class Binder
    {
        public static void Bind<T, U>(Expression<Func<T>> item1, Expression<Func<U>> item2)
        {
            Tuple<object, string> ResolveLambda<V>(Expression<Func<V>> expression)
            {
                var lambda = expression as LambdaExpression;
                if (lambda == null) throw new ArgumentException();
                var property = lambda.Body as MemberExpression;
                if (property == null) throw new ArgumentException();
                var members = new List<MemberInfo>();
                var parent = property.Expression;
                return new Tuple<object, string>(Expression.Lambda(parent).Compile().DynamicInvoke(), property.Member.Name);
            }
            var tuple1 = ResolveLambda(item1);
            var tuple2 = ResolveLambda(item2);
            var control = tuple1.Item1 as Control;
            if (control == null) throw new ArgumentException();
            control.DataBindings.Add(new Binding(tuple1.Item2, tuple2.Item1, tuple2.Item2));
        }

        public static void Bind<T>(this Label label, Expression<Func<T>> expression)
        {
            Bind(() => label.Text, expression);
        }

        public static void Bind(this Button button, ReactiveCommand command)
        {
            command.CanExecuteChanged += (sender, args) => button.Enabled = command.CanExecute();
            button.Enabled = command.CanExecute();
            button.Click += (sender, args) => command.Execute();
        }
    }
}

Binder.cs は上記のようになります。
具体的にはボタンに対するバインド部分が変更されています。

ViewModel

ViewModel.cs

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

namespace WindowsFormsApp1
{
    public class ViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public ReactiveProperty<int> Counter { get; } = new ReactiveProperty<int>();

        public ReactiveCommand UpCommand { get; private set; }
        public ReactiveCommand DownCommand { get; private set; }

        public ViewModel()
        {
            UpCommand = Counter.Select(_ => Counter.Value < 10).ToReactiveCommand();
            UpCommand.Subscribe(() => Counter.Value++);
            DownCommand = Counter.Select(_ => Counter.Value > 0).ToReactiveCommand();
            DownCommand.Subscribe(() => Counter.Value--);
        }
    }
}

ReactiveProperty全然分からねぇ!って人向けのFAQ集【修正済】 を読んで初めて知りましたが、ViewModelINotifyPropertyChanged を実装していないとメモリリークを起こすそうです。そこで形だけ実装します。

Form1

Form1.cs

using FormsMvvm;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        protected ViewModel ViewModel { get; private set; } = new ViewModel();

        public Form1()
        {
            InitializeComponent();
            label1.Bind(() => ViewModel.Counter.Value);
            button1.Bind(ViewModel.UpCommand);
            button2.Bind(ViewModel.DownCommand);
        }
    }
}

Form1 はほとんど変更ありませんが、ViewModel.CounterViewModel.Counter.Value になりました。