Windows Forms で MVVM 2 (ReactiveProperty 編)

2021/09/02に公開

前書き

前回の 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 になりました。

執筆日: 2017/12/19

GitHubで編集を提案

Discussion