この記事
以下、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
により、PropertyChanged
がnull
でなければPropertyChanged
イベントが実行されます。if
によるnull
チェックを省略する書き方です。
名前が一緒で紛らわしいですが、Dispatcher
のInvoke
とは別物です。
以下は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側のバインドの設定で、
Mode
をTwoWay
にするとUI側の値の書き換えでセッターが呼ばれる - 上記で呼ばれたセッターは、呼び出し元のUIに
PropertyChanged
を発火することになる - 通知はバインドされているすべてのUIに対して行われる
単純化するためにOnPropertyChanged
を省略してみましたが、OnPropertyChanged
を書くことでCallerMemberName
属性によるプロパティ名の記載省略ができる様になります。
プロパティの数が多くなるとOnPropertyChanged
を敢えて書くことが生きてきます。プロパティごとにプロパティ名の文字列をタイプする必要がなくなります。プロパティ名を変更することになっても影響なしです。
また、OnPropertyChanged
内で各プロパティ共通のロジックを一箇所に記載することができます。(OnPropertyChanged
の名前は、実装者の好みで変えられます。)
イベントとリスナー
先ほど、
OnPropertyChanged
内で各プロパティ共通のロジックを一箇所に記載する
と書きましたが、共通ロジックはイベントハンドラに記載してリスナーとしてPropertyChanged
イベントに登録することも可能です。
ここで、更に理解を深めるために、イベントとリスナーという言葉の整理をしてみましょう。
イベントと言えば、マウスのクリックやキー入力などのユーザー操作により引き起こされるものを想像するかも知れません。ユーザー操作によるイベントの発火はプログラマがコーディングするものではなく、その操作そのものとなります。一方、PropertyChanged
イベントはプログラマがコーディングして発火させるものです。いずれにしても、何かが発火される瞬間のこととなります。
リスナーとは、イベントの発火を監視して発火に反応するものです。例えば、マウスのクリックやキー入力などのユーザー操作をリスニングする場合、プログラマが対象のイベント用にイベントハンドラを登録して、そこに必要な処理を記述します。このイベントハンドラがリスナーとなります。
では、上述のよく見かける実装例において、PropertyChanged
イベントのリスナーはどこにあるのでしょうか
ここにはリスナーとなるイベントハンドラは書かれていません。ここでのリスナーは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パターンでの基本となるものです。
今どきは、いきなりラムダ式の実装を見かけると思いますが、そこで何が行われているかは解っていた方が良いと思います。上述のよく見かける実装例は基本的な形となりますので、空で覚えておきたいものです。
初めて目にした時は複雑に見えたこれらの実装も、言語化することでモヤモヤが晴れて簡単に見えてきませんか