かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

C# 6.0時代の変更通知プロパティの書き方

2018/01/05 追記

こんなん書きました。

blog.okazuki.jp

本文

VS2015 RC時点の情報に基づいて書いています

XAML系のアプリを書くときに必須となるINotifyPropertyChangedインターフェースを実装した上での、変更通知機能を持ったプロパティがあります。こいつの実装がめんどくさい&文字列指定だとダサいみたいな理由から、INotifyPropertyChangedを実装した以下のようなクラスを準備して、こいつを継承して楽をするという手がよく使われています。

public class BindableBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual bool SetProperty<T>(ref T field, T value, [CallerMemberName]string propertyName = null)
    {
        if (Equals(field, value)) { return false; }
        field = value;
        var h = this.PropertyChanged;
        if (h != null) { h(this, new PropertyChangedEventArgs(propertyName)); }
        return true;
    }
}

こいつを継承するとプロパティの定義が以下のようにすっきりします。

private int age;
public int Age
{
    get { return this.age; }
    set { this.SetProperty(ref this.age, value); }
}

問題点

INotifyPropertyChangedというインターフェースを実装したいだけなのに1つしか出来ないクラスの継承をしてしまう。ほかのクラスを継承したいと思ってもできなくなります。まぁ実質そんなことは滅多に無いので困りはしないのですが、縛りは少ないほうがいいですよね。

C# 6.0の機能を使った変更通知プロパティ

C# 6.0ではいろんな機能が追加されています。

ufcpp.net

これらの機能を駆使してプロパティの定義について考えてみます。

プレーンなプロパティ変更通知

まず、今までの書き方でプレーンにプロパティを定義してみます。

public class MainPageViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string name;

    public string Name
    {
        get { return this.name; }
        set
        {
            if (this.name == value) { return; }
            this.name = value;
            var h = this.PropertyChanged;
            if (h != null)
            {
                h(this, new PropertyChangedEventArgs("Name"));
            }
        }
    }
}

まずNameを文字列で指定するのがいけてないのでnameof演算子を使っていい感じにします。

public class MainPageViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string name;

    public string Name
    {
        get { return this.name; }
        set
        {
            if (this.name == value) { return; }
            this.name = value;
            var h = this.PropertyChanged;
            if (h != null)
            {
                h(this, new PropertyChangedEventArgs(nameof(Name)));
            }
        }
    }
}

次にnull条件演算子を使ってPropertyChangedの呼び出しのためのif文を除去します。

public class MainPageViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string name;

    public string Name
    {
        get { return this.name; }
        set
        {
            if (this.name == value) { return; }
            this.name = value;
            this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
        }
    }
}

かなりスッキリしました。

PropertyChangedEventArgsインスタンスが大量生成されないようにする

プロパティの変更通知が大量に発生するとPropertyChangedEventArgsのインスタンスが大量に生成されてしまいます。これを防ぐためにstaticなところに避難させます。これはBindableBaseでは出来なかったこと。

public class MainPageViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private static readonly PropertyChangedEventArgs NamePropertyChangedEventArgs = new PropertyChangedEventArgs(nameof(Name));

    private string name;

    public string Name
    {
        get { return this.name; }
        set
        {
            if (this.name == value) { return; }
            this.name = value;
            this.PropertyChanged?.Invoke(this, NamePropertyChangedEventArgs);
        }
    }
}

これで理想的な状態になりました。

コードスニペット化

後は、これくらいならコードスニペットを作っておくと捗りそうです。

<?xml version="1.0" encoding="utf-8"?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
    <CodeSnippet Format="1.0.0">
        <Header>
            <Title>propn</Title>
            <Shortcut>propn</Shortcut>
            <Description>プロパティとバッキング フィールド用のコード スニペット</Description>
            <Author>okazuki</Author>
            <SnippetTypes>
                <SnippetType>Expansion</SnippetType>
            </SnippetTypes>
        </Header>
        <Snippet>
            <Declarations>
                <Literal>
                    <ID>type</ID>
                    <ToolTip>プロパティ型</ToolTip>
                    <Default>int</Default>
                </Literal>
                <Literal>
                    <ID>property</ID>
                    <ToolTip>プロパティ名</ToolTip>
                    <Default>MyProperty</Default>
                </Literal>
                <Literal>
                    <ID>field</ID>
                    <ToolTip>このプロパティのバッキング変数</ToolTip>
                    <Default>myVar</Default>
                </Literal>
            </Declarations>
            <Code Language="csharp"><![CDATA[private static readonly PropertyChangedEventArgs $property$PropertyChangedEventArgs = new PropertyChangedEventArgs(nameof($property$));
      
  private $type$ $field$;

  public $type$ $property$
  {
      get { return this.$field$;}
      set 
    {
      if (this.$field$ == value) { return; }
      this.$field$ = value;
      this.PropertyChanged?.Invoke(this, $property$PropertyChangedEventArgs);
    }
  }
  $end$]]>
            </Code>
        </Snippet>
    </CodeSnippet>
</CodeSnippets>

これをpropn.snippetという名前で保存して、コードスニペットマネージャーからインポートすればpropnで先ほどのプロパティの定義が生成されます。