Windows Forms で MVVM 2 (ReactiveProperty 編)
前書き
前回の Windows Forms で MVVM では PropertySetter
, Command
, Binder
の三つのヘルパークラスを作って Windows Forms で MVVM を実装する方法をお伝えしました。
PropertySetter
は INotifyPropertyChanged
を簡単に実装するためのクラス、Command
は UI からのアクションを実装するクラス、Binder
はコントロールとデータを簡単にバインドするためのクラスでした。
しかし、ReactiveProperty
を使用すればもっとシンプルにできます。
ReactiveProperty
を使うとプロパティ自体が IObservable<T>
を実装するので、ViewModel
が INotifyPropertyChanged
を実装する必要がありません。また 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集【修正済】 を読んで初めて知りましたが、ViewModel
は INotifyPropertyChanged
を実装していないとメモリリークを起こすそうです。そこで形だけ実装します。
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.Counter
が ViewModel.Counter.Value
になりました。
執筆日: 2017/12/19
Discussion