6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-04-29

この記事

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

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

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

よく見かける実装例

以下はINotifyPropertyChangedのよく見かける実装例です。

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イベントを実行するように書き直せます。

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

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

以下はMainWindowのXAMLの例です。

<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側のバインドの設定で、ModeTwoWayにするとUI側の値の書き換えでセッターが呼ばれる
  • 上記で呼ばれたセッターは、呼び出し元のUIにPropertyChangedを発火することになる
  • 通知はバインドされているすべてのUIに対して行われる

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

イベントとリスナー

先ほど、

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

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

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

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

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

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

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

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

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

// リスナーは、以下のシグネチャを持つ必要がある
private void hogePlus(object? sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == "Address")
    {
        // この += は文字列を連結するもので、リスナー登録とは別物
        address += "ほげ";
    }
}

リスナーとして登録するイベントハンドラのシグネチャは、上述のhogePlusメソッドの様に決まっています。
ここで、senderはイベントを発生させたオブジェクト(MainWindowViewModelクラスのインスタンス)を参照します。
eは、イベントに関連する情報を含み、PropertyNameプロパティで変更されたプロパティ名を取得できます。

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

// 例えば、IDisposableを実装するなら...
public void Dispose()
{
    // リスナーの登録抹消(購読解除)
    PropertyChanged -= hogePlus;
}

当然ですが、PropertyChangedイベントにリスナーを登録&抹消しても、通知機能が上書きされたり無くなったりすることはありません。

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

SetPropertyメソッド

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

// フィールドを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;
}

これにより、セッターの記述が簡潔になります。

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

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

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

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

まとめ

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

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

6
4
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?