INotifyPropertyChangedインターフェイスは簡単だ！

この記事
---
以下、WPFなどで用いる`INotifyPropertyChanged`インターフェイスについて簡単にまとめてみました。

`INotifyPropertyChanged`の定義は、dotnet/runtimeリポジトリにあります。
以下がその定義になります。

```c#
namespace System.ComponentModel
{
    public interface INotifyPropertyChanged
    {
        event PropertyChangedEventHandler? PropertyChanged;
    }
}
```

https://github.com/dotnet/runtime/blob/f5ea326fff2f89ab8188950531c0290d2ae7a827/src/libraries/System.ObjectModel/src/System/ComponentModel/INotifyPropertyChanged.cs#L6

よく見かける実装例
---
以下は`INotifyPropertyChanged`のよく見かける実装例です。
```c#
public class MainWindowViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    // [CallerMemberName]属性で、このメソッドを呼び出したプロパティの名前が
    // 自動的に引数として渡される
    private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private string address;
    public string Address
    {
        get
        {
            return this.address;
        }
        set
        {
            if (value != this.address)
            {
                this.address = value;
                OnPropertyChanged();
            }
        }
    }
}
```
ここで、わざわざ`OnPropertyChanged`を書かずにセッター内で直接`PropertyChanged`イベントを実行するように書き直せます。

```c#
set
{
    if (value != this.address)
    {
        this.address = value;
        // 直接、PropertyChangedイベントを実行してもよい
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Address"));
    }
}
```

`Invoke`により、`PropertyChanged`が`null`でなければ`PropertyChanged`イベントが実行されます。`if`による`null`チェックを省略する書き方です。
名前が一緒で紛らわしいですが、`Dispatcher`の`Invoke`とは別物です。

以下は`MainWindow`のXAMLの例です。

```xml
<Window x:Class="Hoge.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Hoge.ViewModels"
        mc:Ignorable="d"
        Title="MainWindow" Height="300" Width="300">
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <Grid>
        <StackPanel Orientation="Vertical" VerticalAlignment="Center">
            <TextBox Text="{Binding Address,Mode=TwoWay}" Width="150"/>
        </StackPanel>
    </Grid>
</Window>
```

### INotifyPropertyChanged まとめ

+ WPFのUI（`TextBox`など）でプロパティをバインドする
+ `PropertyChanged`の名前は、**PropertyChanged**でなければならない
+ `PropertyChanged`をプロパティのセッター内で発火（実行）する
+ `PropertyChanged`を発火することでUI側に通知が行われる
+ 通知を発火するのはプログラマの仕事
+ `INotifyPropertyChanged`を実装しないクラスで、単に`PropertyChanged`をメンバとして持たせて実行しても通知は伝わらない
+ UI側のバインドの設定で、`Mode`を`TwoWay`にするとUI側の値の書き換えでセッターが呼ばれる
+ 上記で呼ばれたセッターは、呼び出し元のUIに`PropertyChanged`を発火することになる
+ 通知はバインドされているすべてのUIに対して行われる

単純化するために`OnPropertyChanged`を省略してみましたが、`OnPropertyChanged`を書くことで`CallerMemberName`属性によるプロパティ名の記載省略ができる様になります。
プロパティの数が多くなると`OnPropertyChanged`を敢えて書くことが生きてきます。プロパティごとにプロパティ名の文字列をタイプする必要がなくなります。プロパティ名を変更することになっても影響なしです。
また、`OnPropertyChanged`内で各プロパティ共通のロジックを一箇所に記載することができます。（`OnPropertyChanged`の名前は、実装者の好みで変えられます。）

イベントとリスナー
---
先ほど、

> `OnPropertyChanged`内で各プロパティ共通のロジックを一箇所に記載する

と書きましたが、共通ロジックはイベントハンドラに記載してリスナーとして`PropertyChanged`イベントに登録することも可能です。

ここで、更に理解を深めるために、**イベント**と**リスナー**という言葉の整理をしてみましょう。

**イベント**と言えば、マウスのクリックやキー入力などのユーザー操作により引き起こされるものを想像するかも知れません。ユーザー操作によるイベントの発火はプログラマがコーディングするものではなく、その操作そのものとなります。一方、`PropertyChanged`イベントはプログラマがコーディングして発火させるものです。いずれにしても、何かが発火される瞬間のこととなります。

**リスナー**とは、イベントの発火を監視して発火に反応するものです。例えば、マウスのクリックやキー入力などのユーザー操作をリスニングする場合、プログラマが対象のイベント用にイベントハンドラを登録して、そこに必要な処理を記述します。このイベントハンドラがリスナーとなります。

では、上述の**よく見かける実装例**において、`PropertyChanged`イベントのリスナーはどこにあるのでしょうか:question::thinking:

ここにはリスナーとなるイベントハンドラは書かれていません。ここでのリスナーはUIコンポーネントであり、イベントハンドラを登録せずともUIがバインドされていれば、そのUIはイベントに反応してくれます。その内部処理はUIコンポーネント任せで、プログラマの知るところではありません。


以下は、このUIコンポーネント任せの`PropertyChanged`イベントに対する処理をカスタマイズするためのリスナー登録（購読開始）の例です。

```C#
// 例えば、INotifyPropertyChangedを実装するクラスのコンストラクタで...
public MainWindowViewModel()
{
    // 色々な初期化処理...
    
    // リスナーの登録（購読開始）
    PropertyChanged += hogePlus;
}

// リスナーは、以下のシグネチャを持つ必要がある
private void hogePlus(object? sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == "Address")
    {
        // この += は文字列を連結するもので、リスナー登録とは別物
        address += "ほげ";
    }
}
```
リスナーとして登録するイベントハンドラのシグネチャは、上述の`hogePlus`メソッドの様に決まっています。
ここで、`sender`はイベントを発生させたオブジェクト（`MainWindowViewModel`クラスのインスタンス）を参照します。
`e`は、イベントに関連する情報を含み、`PropertyName`プロパティで変更されたプロパティ名を取得できます。

リスナーが不要になったら登録を抹消（購読解除）します。

```C#
// 例えば、IDisposableを実装するなら...
public void Dispose()
{
    // リスナーの登録抹消（購読解除）
    PropertyChanged -= hogePlus;
}
```
当然ですが、`PropertyChanged`イベントにリスナーを登録＆抹消しても、通知機能が上書きされたり無くなったりすることはありません。

:::note warn
`PropertyChanged`イベントのこのような使い方は、コードが複雑になったりメモリーリークの原因になる可能性があり、注意が必要です。
:::

SetPropertyメソッド
-
以下はセッターの記述を簡潔にするための、よく見かける追加の実装例です。

```C#
// フィールドをrefで参照渡しする
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
{
    if (EqualityComparer<T>.Default.Equals(field, value))
    {
        // 戻り値で、値が変更されたか否かの判定ができる
        return false;
    }
    
    field = value;
    OnPropertyChanged(propertyName);
    return true;
}
```
これにより、セッターの記述が簡潔になります。

```C#
public string Address
{
    get 
    { 
        return address; 
    }
    set 
    { 
        SetProperty(ref address, value); 
    }
}
```

更にラムダ式で簡潔に書けます。

```C#
public string Address 
{ 
    get => address; 
    set => SetProperty(ref address, value); 
}
```

`SetProperty<T>`はジェネリックメソッドです。ジェネリックメソッドについては以下を参照してください。

https://qiita.com/nekohei/items/b424ba3171b7a0630c44#%E3%82%B8%E3%82%A7%E3%83%8D%E3%83%AA%E3%83%83%E3%82%AF%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89

まとめ
---
`INotifyPropertyChanged`インターフェイスはMVVMパターンでの基本となるものです。
今どきは、いきなりラムダ式の実装を見かけると思いますが、そこで何が行われているかは解っていた方が良いと思います。上述の**よく見かける実装例**は基本的な形となりますので、空で覚えておきたいものです。

初めて目にした時は複雑に見えたこれらの実装も、言語化することでモヤモヤが晴れて簡単に見えてきませんか:question::laughing:

https://learn.microsoft.com/en-us/dotnet/desktop/wpf/data/how-to-implement-property-change-notification?view=netframeworkdesktop-4.8&viewFallbackFrom=netdesktop-8.0