MvvmGenでメンドクサいINotifyPropertyChangedの実装を楽にする(VisualStudio)


メンドクサいMVVM

XAMLアプリではMVVMパターン、モデル・ビュー・ビューモデルパターンで実装するのがセオリーとされています。ビューが描画を、モデルがデータとロジックを担当し、ビューモデルがその間を取り持ちます。

なるほど、理屈は分かります。

<TextBox Text="{Binding ID}" />
<TextBox Text="{Binding CustomerName}" />
<TextBox Text="{Binding PhoneNumber}" />

XAML側でこんな定義をして…。

public class DemoCustomer
{
    public Guid ID { get; set; } = Guid.NewGuid();
    public string CustomerName { get; set; }
    public string PhoneNumber { get; set; }

    private DemoCustomer()
    {
        CustomerName = "Customer";
        PhoneNumber = "(312)555-0100";
    }
}

プロパティとバインドすれば、プロパティの変更が自動的に反映されるわけです。

…というのは嘘で、その反映は手動でやらないといけません。

マイクロソフトの例に従えば、こうしないといけません。


public class DemoCustomer : INotifyPropertyChanged
{
    private Guid idValue = Guid.NewGuid();
    private string customerNameValue = String.Empty;
    private string phoneNumberValue = String.Empty;

    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    private DemoCustomer()
    {
        customerNameValue = "Customer";
        phoneNumberValue = "(312)555-0100";
    }

    public static DemoCustomer CreateNewCustomer()
    {
        return new DemoCustomer();
    }

    public Guid ID
    {
        get
        {
            return this.idValue;
        }
    }

    public string CustomerName
    {
        get
        {
            return this.customerNameValue;
        }

        set
        {
            if (value != this.customerNameValue)
            {
                this.customerNameValue = value;
                NotifyPropertyChanged();
            }
        }
    }

    public string PhoneNumber
    {
        get
        {
            return this.phoneNumberValue;
        }

        set
        {
            if (value != this.phoneNumberValue)
            {
                this.phoneNumberValue = value;
                NotifyPropertyChanged();
            }
        }
    }
}

たった3つしかプロパティがないのに、猛烈に見通しが悪くなっています。MFCの暗黒時代に戻ったかのようです。

メンドクサくないMVVM

MvvmGenはこの面倒を解決するための手段の1つです。MvvmGenでDemoCustomerはこうなります。

[ViewModel]
public partial class DemoCustomer
{
    [Property]
    private Guid id = Guid.NewGuid();

    [Property]
    private string customerName;

    [Property]
    private string phoneNumber;

    partial void OnInitialize()
    {
        CustomerName = "Customer";
        PhoneNumber = "(312)555-0100";
    }
}

最初にやりたかったコードとほとんど変わりません。変数に対応するプロパティはMvvmGenが裏で勝手に作ってくれています。プロパティは名前の先頭が大文字になっています。

この裏でこっそり作られているプロパティは、ソリューションエクスプローラーから参照できます。

// <auto-generated>
//   This code was generated for you by
//   ⚡ MvvmGen, a tool created by Thomas Claudius Huber (https://www.thomasclaudiushuber.com)
//   Generator version: 1.1.2
// </auto-generated>
using MvvmGen.Commands;
using MvvmGen.Events;
using MvvmGen.ViewModels;

namespace Sample
{
    partial class DemoCustomer : global::MvvmGen.ViewModels.ViewModelBase
    {
        public DemoCustomer()
        {
            this.OnInitialize();
        }

        partial void OnInitialize();

        public System.Guid Id
        {
            get => id;
            set
            {
                if (id != value)
                {
                    id = value;
                    OnPropertyChanged("Id");
                }
            }
        }

        public string CustomerName
        {
            get => customerName;
            set
            {
                if (customerName != value)
                {
                    customerName = value;
                    OnPropertyChanged("CustomerName");
                }
            }
        }

        public string PhoneNumber
        {
            get => phoneNumber;
            set
            {
                if (phoneNumber != value)
                {
                    phoneNumber = value;
                    OnPropertyChanged("PhoneNumber");
                }
            }
        }
    }
}

この面倒から解放されるのはありがたいですね。ていうか最初からこうしてくれよマイクロソフト。

プロパティの変更を検知する

MvvmGenはプロパティを自動生成するため、そのままではsetterで何か処理をするということができません。PropertyCallMethod属性を追加することで、プロパティがセットされたときにそのメソッドを呼んでくれます。

[ViewModel]
public partial class DemoCustomer
{
    [PropertyCallMethod("OnUpdateCustomerName")]
    [Property]
    private string customerName;

    private void OnUpdateCustomerName()
    {
        System.Diagnostics.Debug.WriteLine($"customerName has been changed to {customerName}");
    }
}

コマンド

PropertyはViewModelからViewへの通信でしたが、この逆のViewからViewModelへの通信としてコマンドという仕組みがあります。MvvmGenはこのコマンドにも対応しています。

[ViewModel]
public partial class DemoCustomer
{
    [Property]
    private Guid id = Guid.NewGuid();

    [Property]
    private string customerName;

    [Property]
    private string phoneNumber;

    partial void OnInitialize()
    {
        CustomerName = "Trump";
        PhoneNumber = "(312)555-0100";
    }

    [Command]
    private void ClickFire()
    {
        CustomerName = "Biden";
    }
}

CustomerNameをTrumpにしておき、ボタンがクリックされたらBidenに書き換えます。ClickFireメソッドにCommand属性が追加されています。

<TextBlock Name="SampleText" FontSize="100" Text="{Binding CustomerName}"/>
<Button Content="Fire!" Command="{Binding ClickFireCommand}"/>

ClickFireCommandがMvvmGenにより自動生成されていますので、これをXAML側でボタンにバインドします。作業はこれだけで完了です。

これでも.NET FrameworkのWinFormに比べればまだ煩雑ですが、だいぶ許容範囲になりました。