ReactiveUI WinFormsをかじる

WPFと言えばMVVMですが実際の現場でWPFの浸透度はどうでしょうか?WinXPのサポートが切れたことで 『.NET3.0のインストールは30分も掛かってユーザに迷惑だから.NET2.0で開発する』 などと古臭い主張をする保守派への反論材料が揃いだした昨今ではまだ、WindowsFormsで新規開発しているほうがまだ多いと感じます。

Windows Formsで『MVC(おそらく正確にはMVP)で開発する』と意気込んだプロジェクトの何%が目標通りの設計、メンテナンス性が実現できているのか疑問です。Frameworkサポートとしては弱いWindows FormsでMVVMを実現させるライブラリの紹介です。

 

ReactiveUI WinForms

まずWindowsFormsアプリのプロジェクトを作成した状態で、Nugetパッケージマネージャで

PM> install-package reactiveui-winforms

としてReactiveUI WinFormsをインストールしておきます。VS2010ではダメだったのですが、VS2013ではokでした、VS2012以降などの制限があるかもしれません。

サンプルFormは以下の構成とします。

form

ReactiveUI-WinFormsではFormをただのViewとして扱い、ViewModelは以下のように別クラスに定義します。

 SampleViewModel.cs

namespace ReactiveUiForWinFormsTest
{
    using System;
    using ReactiveUI;

    public class SampleViewModel : ReactiveObject
    {
        private string inputString1;
        public string InputString1 {
            get { return inputString1; }
            set { this.RaiseAndSetIfChanged(ref inputString1, value); }
        }

        private string inputString2 = "input2の初期値です";
        public string InputString2 {
            get { return inputString2; }
            set { this.RaiseAndSetIfChanged(ref inputString2, value); }
        }

        private DateTime currentDateTime = DateTime.Now;
        public DateTime CurrentDateTime {
            get { return currentDateTime; }
            set { this.RaiseAndSetIfChanged(ref currentDateTime, value); }
        }

        // 初期化コマンド
        public ReactiveCommand<object> Initialize{ get; set;}

        public SampleViewModel()
        {
            // 初期化コマンドの作成
            Initialize = ReactiveCommand.Create(this.WhenAny(vm => vm.InputString1, str => true)); // =常時有効
            Initialize.Subscribe(_ =>
            {
                InputString1 = "初期値1";
                InputString2 = "初期値2";
                CurrentDateTime = new DateTime(2020, 1, 1);
            }
            );
        }

        public override string ToString(){
            return "1:" + InputString1 + Environment.NewLine +
                   "2:" + InputString2 + Environment.NewLine +
                   CurrentDateTime.ToString();
        }
    }
}

MVVMに従い、Viewに対する操作は全てViewModelに定義します。ViewModelの基本クラスとしてReactiveUI.ReactiveObjectクラスから派生させます。

データ更新は通常INotifyPropertyChangedを実装しますが、ReactiveObjectクラスが実装するRaiseAndSetIfChangedメソッドの呼び出しでフィールドへの値のセット及びINotifyPropertyChangedのイベント発砲の処理を行います。thisを明示しないとこのメソッドが出てこず苦労しました;

データ操作はReactiveCommandとして公開します。ReactiveExtensionsを使い、いつ、どんな処理を行うかを定義します。上の例では常時有効にしていますが、これをプロパティ状態に応じてtrue/falseが切り替わるようにするとこの後でバインドするボタンの有効状態が変わります。WPFではおなじみのICommandを実装しているのでCanExecute/Executeを実装しているのでWindowsFormsであっても有効状態も同期する利点がでています。

これをバインドする先のViewはこんな感じになります。

 SampleViewForm.cs

namespace ReactiveUiForWinFormsTest
{
    using System.Windows.Forms;
    using ReactiveUI;

    public partial class SampleViewForm : Form, IViewFor<SampleViewModel>
    {
        public SampleViewForm(SampleViewModel viewModel)
        {
            InitializeComponent();

            ViewModel = viewModel;
            this.Bind(ViewModel, vm => vm.InputString1, v => v.input1TextBox.Text);
            this.Bind(ViewModel, vm => vm.InputString2, v => v.input2TextBox.Text);
            this.Bind(ViewModel, vm => vm.CurrentDateTime, v => v.currentDateTimePicker.Value);
            this.BindCommand(ViewModel, vm => vm.Initialize, v => v.executeButton);
            this.closeButton.Click += (_, __) => this.Close();
        }

        public SampleViewModel ViewModel{
            get;set;
        }
        object IViewFor.ViewModel{
            get { return ViewModel; }
            set { ViewModel = value as SampleViewModel; }
        }
    }
}

ViewはViewModelに依存するので、依存先のViewModelを指定したIViewFor<TViewModel>から派生させます。CloseだけはViewで処理していますが、他は全てViewCommandの処理とバインドさせておりViewが処理を担うことをなくしています。ここでのFormは前述の通りただのViewなのでViewModelはConstructor Injectionで渡してもらうのが妥当でしょう。

蛇足ですがこれを呼び出すのはこんな感じ。

Program.cs

namespace ReactiveUiForWinFormsTest
{
    using System;
    using System.Windows.Forms;
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            var viewModel = new SampleViewModel();
            Application.Run(new SampleViewForm(viewModel));
            MessageBox.Show(viewModel.ToString());
        }
    }
}

実行結果はご想像の通り、ちゃんとViewModelとViewのバインドはとれています。

binding__

上の例では単純にBindでバインドしただけですが、WPFでのバインド似でOneWayBindがあったり、バインド時の引数にIValueConverterみたいなものがあったり、かなり使えそうな印象です。