C# PropertyChangedEventHandlerとは?INotifyPropertyChangedで画面更新されない原因と実装方法を解説

はじめに

C#でWPF、Xamarin.Forms、.NET MAUI、WinUIなどの画面開発をしていると、PropertyChangedEventHandlerINotifyPropertyChangedという名前をよく見かけます。

これらは、ViewModelのプロパティ値が変わったことを画面側へ通知し、データバインディングされた表示を更新するために使われます。たとえば、ボタンを押してNameプロパティの値を変更したのに画面のLabelTextBlockが更新されない場合、PropertyChangedイベントが正しく発火していない可能性があります。

この記事では、C#のPropertyChangedEventHandlerとは何か、INotifyPropertyChangedとの関係、基本的な実装方法、画面更新されない原因、実務で使いやすい書き方までを解説します。

1. C# PropertyChangedEventHandlerとは?

1-1. PropertyChangedEventHandlerの役割

PropertyChangedEventHandlerは、プロパティの値が変更されたことを通知するPropertyChangedイベントで使われるデリゲート型です。

C#では、イベントを定義するときに「どのような引数を持つメソッドをイベントハンドラとして受け取るか」をデリゲートで表します。PropertyChangedEventHandlerは、プロパティ変更通知用に用意されているデリゲートです。

INotifyPropertyChangedインターフェースは、プロパティ値が変更されたことをクライアントへ通知するための仕組みで、PropertyChangedイベントを持ちます。このPropertyChangedイベントの型として使われるのがPropertyChangedEventHandlerです。Microsoft Learn+1

基本形は次のようになります。

C#
using System.ComponentModel;

public class SampleViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
}

このイベントを発火すると、バインディングしている画面側に「指定したプロパティが変わった」と伝えられます。

1-2. PropertyChangedイベントが使われる場面

PropertyChangedイベントは、主にデータバインディングを使う画面アプリケーションで利用されます。

代表的な場面は次のとおりです。

C#
private string _message = string.Empty;

public string Message
{
get => _message;
set
{
_message = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Message)));
}
}

このようにMessageプロパティの値が変わったタイミングでPropertyChangedイベントを発火すると、Messageにバインドされている画面部品が更新されます。

たとえば、WPFで次のようにバインディングしている場合です。

XML
<TextBlock Text="{Binding Message}" />

ViewModel側でMessageを変更してPropertyChangedを発火すれば、TextBlockの表示も更新されます。

1-3. PropertyChangedEventArgsとの違い

PropertyChangedEventHandlerと似た名前にPropertyChangedEventArgsがあります。

この2つの役割は異なります。

PropertyChangedEventHandlerはイベントハンドラの型です。一方、PropertyChangedEventArgsは、どのプロパティが変更されたのかをイベントに渡すための引数クラスです。

実装では、次のように一緒に使います。

C#
PropertyChanged?.Invoke(
this,
new PropertyChangedEventArgs(nameof(Name))
);

ここでの意味は次のとおりです。

C#
PropertyChanged

これは変更通知イベントです。

C#
PropertyChangedEventHandler

これはPropertyChangedイベントの型です。

C#
PropertyChangedEventArgs

これは変更されたプロパティ名を渡すためのイベント引数です。

つまり、PropertyChangedEventHandlerは「通知を受け取るためのメソッドの形」を表し、PropertyChangedEventArgsは「通知内容」を表します。

1-4. 値の変更通知が画面更新につながる仕組み

C#の画面アプリでは、画面部品がViewModelのプロパティを参照する形でデータバインディングを行います。

たとえば、画面のTextBlockがViewModelのTitleプロパティにバインドされているとします。

XML
<TextBlock Text="{Binding Title}" />

この状態でViewModelのTitleを変更しても、ただフィールドの値を変えただけでは画面側は変更に気づけません。

そこで、PropertyChangedイベントを発火します。

C#
OnPropertyChanged(nameof(Title));

すると、バインディング機構が「Titleが変わった」と認識し、画面部品が新しい値を取得して表示を更新します。

つまり、画面更新に必要なのは単なる値の変更ではなく、「値が変わった」という通知です。

2. INotifyPropertyChangedとの関係

2-1. INotifyPropertyChangedインターフェースとは

INotifyPropertyChangedは、プロパティ値の変更を通知するためのインターフェースです。

このインターフェースには、PropertyChangedイベントが定義されています。

C#
public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler? PropertyChanged;
}

実際には.NET側で定義されているため、自分でこのインターフェースを作る必要はありません。ViewModelクラスにINotifyPropertyChangedを実装することで、画面側のバインディング機構に「このクラスはプロパティ変更通知に対応している」と伝えられます。Microsoft Learn+1

2-2. PropertyChangedEventHandlerを宣言する理由

ViewModelに次のようなイベントを定義するのは、INotifyPropertyChangedの契約を満たすためです。

C#
public event PropertyChangedEventHandler? PropertyChanged;

INotifyPropertyChangedを実装するクラスは、PropertyChangedイベントを公開する必要があります。

このイベントがあることで、画面側のバインディング機構はViewModelを監視できます。プロパティのsetterでPropertyChangedイベントを発火すると、バインディング先の画面部品へ変更が伝わります。

2-3. データバインディングで変更通知が必要になる理由

データバインディングでは、画面とデータが自動的に同期されるように見えます。しかし、ViewModelのプロパティが変更されたことを画面側が自動で検知できるわけではありません。

たとえば、次のようなViewModelでは画面更新されないことがあります。

C#
public class MainViewModel
{
public string Message { get; set; } = "Hello";
}

このクラスはINotifyPropertyChangedを実装していないため、Messageの値を変更しても画面側へ通知されません。

画面を更新したい場合は、次のように変更通知を実装します。

C#
public class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;

private string _message = "Hello";

public string Message
{
get => _message;
set
{
if (_message == value)
{
return;
}

_message = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Message)));
}
}
}

このように、データバインディングで画面更新を正しく行うには、値の変更と変更通知をセットで考える必要があります。

2-4. WPF・Xamarin・MAUI・WinUIでの利用例

INotifyPropertyChangedPropertyChangedEventHandlerは、複数のC# UIフレームワークで使われます。

WPFではDataContextにViewModelを設定し、XAMLのBindingでプロパティを参照します。

C#
DataContext = new MainViewModel();

Xamarin.Formsや.NET MAUIでは、一般的にBindingContextにViewModelを設定します。

C#
BindingContext = new MainViewModel();

WinUIでも、ViewModelをバインディング元として使う場合にINotifyPropertyChangedを実装します。

フレームワークによってDataContextBindingContextなどの名前は違いますが、「プロパティが変わったことを通知し、バインディング先を更新する」という考え方は共通です。

3. PropertyChangedEventHandlerの基本的な実装方法

3-1. ViewModelにINotifyPropertyChangedを実装する

まず、ViewModelクラスにINotifyPropertyChangedを実装します。

C#
using System.ComponentModel;

public class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
}

これで、ViewModelがプロパティ変更通知に対応する準備ができます。

ただし、この時点ではイベントを宣言しただけです。実際に画面を更新するには、プロパティ変更時にイベントを発火する必要があります。

3-2. PropertyChangedイベントを定義する

INotifyPropertyChangedを実装すると、次のイベントを定義する必要があります。

C#
public event PropertyChangedEventHandler? PropertyChanged;

?はnullable参照型を有効にしている場合の書き方です。イベントに購読者がいない場合、PropertyChangednullになります。

そのため、イベントを発火するときは次のようにnull条件演算子を使うのが一般的です。

C#
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Message)));

?.Invokeを使うことで、購読者がいない場合でも例外を出さずに安全に呼び出せます。

3-3. OnPropertyChangedメソッドを作成する

毎回PropertyChanged?.Invoke(...)を書くとコードが重複します。

そこで、ViewModel内にOnPropertyChangedメソッドを用意します。

C#
protected void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(
this,
new PropertyChangedEventArgs(propertyName)
);
}

これにより、各プロパティのsetterでは次のように書けます。

C#
OnPropertyChanged(nameof(Message));

変更通知の処理を1か所にまとめられるため、保守しやすくなります。

3-4. プロパティのsetterで変更通知を呼び出す

画面更新を行うには、プロパティのsetterでOnPropertyChangedを呼び出します。

C#
private string _message = string.Empty;

public string Message
{
get => _message;
set
{
if (_message == value)
{
return;
}

_message = value;
OnPropertyChanged(nameof(Message));
}
}

ポイントは、フィールドに値を代入したあとにOnPropertyChangedを呼ぶことです。

先に通知してしまうと、画面側が値を取りに来た時点で古い値が返る可能性があります。

3-5. CallerMemberNameを使ってプロパティ名の指定を省略する

nameof(Message)を毎回書く方法でも問題ありませんが、CallerMemberNameを使うとプロパティ名の指定を省略できます。

CallerMemberName属性を使うと、呼び出し元メンバーの名前を文字列引数として明示的に渡す必要を避けられます。これにより、文字列のハードコーディングやリファクタリング時の修正漏れを減らせます。Microsoft Learn

C#
using System.ComponentModel;
using System.Runtime.CompilerServices;

public class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;

protected void OnPropertyChanged(
[CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(
this,
new PropertyChangedEventArgs(propertyName)
);
}

private string _message = string.Empty;

public string Message
{
get => _message;
set
{
if (_message == value)
{
return;
}

_message = value;
OnPropertyChanged();
}
}
}

この場合、Messageプロパティのsetter内でOnPropertyChanged()を呼ぶだけで、プロパティ名"Message"が自動的に渡されます。

4. 画面更新されない主な原因

4-1. INotifyPropertyChangedを実装していない

画面更新されない原因として最も多いのが、ViewModelにINotifyPropertyChangedを実装していないケースです。

次のようなクラスでは、プロパティ変更通知がありません。

C#
public class MainViewModel
{
public string Message { get; set; } = "Hello";
}

この場合、初期表示はできても、後からMessageを変更したときに画面が更新されないことがあります。

対処法は、ViewModelにINotifyPropertyChangedを実装し、プロパティ変更時にPropertyChangedを発火することです。

4-2. PropertyChangedイベントを発火していない

INotifyPropertyChangedを実装していても、PropertyChangedイベントを発火していなければ画面は更新されません。

悪い例は次のとおりです。

C#
private string _message = string.Empty;

public string Message
{
get => _message;
set
{
_message = value;
}
}

これでは値を変更しているだけで、画面側に通知していません。

正しくは次のようにします。

C#
public string Message
{
get => _message;
set
{
if (_message == value)
{
return;
}

_message = value;
OnPropertyChanged();
}
}

4-3. 通知するプロパティ名が間違っている

PropertyChangedEventArgsに渡すプロパティ名が間違っていると、バインディング先が更新されません。

たとえば、次のコードは誤りです。

C#
OnPropertyChanged("Mesage");

MessageではなくMesageになっているため、画面側はMessageが変更されたと認識できません。

文字列で直接指定するのではなく、nameofを使いましょう。

C#
OnPropertyChanged(nameof(Message));

さらに、CallerMemberNameを使えばプロパティ名の指定自体を省略できます。

C#
OnPropertyChanged();

4-4. setterを経由せずフィールドを直接変更している

画面更新されない原因として、プロパティではなくフィールドを直接変更しているケースもあります。

C#
_message = "Changed";

このようにフィールドを直接変更すると、setter内のOnPropertyChangedが呼ばれません。

正しくは、プロパティを通して値を変更します。

C#
Message = "Changed";

プロパティのsetterに変更通知を集約しておけば、どこから変更しても通知処理が実行されます。

4-5. DataContextやBindingContextの設定が間違っている

ViewModel側の実装が正しくても、画面側のDataContextBindingContextが設定されていなければバインディングは機能しません。

WPFでは次のように設定します。

C#
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}

.NET MAUIでは次のように設定します。

C#
public MainPage()
{
InitializeComponent();
BindingContext = new MainViewModel();
}

画面更新されない場合は、まず画面が参照しているViewModelが本当に意図したインスタンスかを確認しましょう。

ありがちなミスは、画面に設定したViewModelとは別のViewModelインスタンスをコード側で変更しているケースです。

4-6. バインディングのModeが適切でない

バインディングのModeが意図と合っていない場合も、画面更新や入力値の反映が期待どおりになりません。

たとえば、ViewModelから画面へ表示するだけならOneWayで十分です。

XML
<TextBlock Text="{Binding Message, Mode=OneWay}" />

画面の入力値をViewModelへ戻したい場合は、TwoWayが必要です。

XML
<TextBox Text="{Binding Message, Mode=TwoWay}" />

TextBoxの入力内容がViewModelへ反映されない場合は、Modeや更新タイミングを確認しましょう。

WPFでは、入力中に即時反映したい場合にUpdateSourceTrigger=PropertyChangedを指定することがあります。

XML
<TextBox Text="{Binding Message, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />

4-7. コレクション更新にListを使っている

一覧表示を更新したい場合、List<T>を使っていると追加や削除が画面に反映されないことがあります。

List<T>は要素の追加や削除を画面へ通知する仕組みを持っていません。

一覧の追加・削除を画面に反映したい場合は、ObservableCollection<T>を使います。ObservableCollection<T>は、項目の追加、削除、リスト全体の更新が発生したときに通知を提供する動的コレクションです。Microsoft Learn+1

C#
using System.Collections.ObjectModel;

public ObservableCollection<string> Items { get; } = new();

public void AddItem()
{
Items.Add("New Item");
}

ただし、ObservableCollection<T>が通知するのは主にコレクション自体の追加・削除です。コレクション内の各要素のプロパティ変更を反映したい場合は、要素クラス側にもINotifyPropertyChangedを実装する必要があります。

5. 正しく画面更新するための実装例

5-1. 最小構成のViewModelサンプル

まずは、最小構成のViewModelを確認しましょう。

C#
using System.ComponentModel;
using System.Runtime.CompilerServices;

public class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;

private string _message = "初期値";

public string Message
{
get => _message;
set
{
if (_message == value)
{
return;
}

_message = value;
OnPropertyChanged();
}
}

protected void OnPropertyChanged(
[CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(
this,
new PropertyChangedEventArgs(propertyName)
);
}
}

このViewModelでは、Messageプロパティの値が変わるたびにPropertyChangedイベントが発火します。

5-2. XAML側のBinding設定例

WPFの場合、XAMLでは次のようにバインディングします。

XML
<StackPanel>
<TextBlock Text="{Binding Message}" />
</StackPanel>

コードビハインドでDataContextを設定します。

C#
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
}

.NET MAUIの場合は、次のようにBindingContextを設定します。

C#
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
BindingContext = new MainViewModel();
}
}

XAMLでは次のように書けます。

XML
<Label Text="{Binding Message}" />

5-3. TextBoxやLabelを更新するサンプル

WPFでTextBoxTextBlockを同じプロパティにバインディングすると、入力値と表示を連動できます。

XML
<StackPanel>
<TextBox Text="{Binding Message, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="{Binding Message}" />
</StackPanel>

ViewModelは次のようになります。

C#
private string _message = string.Empty;

public string Message
{
get => _message;
set
{
if (_message == value)
{
return;
}

_message = value;
OnPropertyChanged();
}
}

TextBoxに入力するとMessageが更新され、TextBlockの表示も変わります。

5-4. ボタンクリックで値を変更して画面更新する例

ボタンクリックでプロパティを変更する場合も、setterを通して値を更新することが重要です。

C#
public void ChangeMessage()
{
Message = "ボタンがクリックされました";
}

WPFのコードビハインドで簡単に試す場合は、次のように書けます。

C#
private void Button_Click(object sender, RoutedEventArgs e)
{
if (DataContext is MainViewModel vm)
{
vm.Message = "ボタンがクリックされました";
}
}

XAMLは次のようになります。

XML
<StackPanel>
<TextBlock Text="{Binding Message}" />
<Button Content="変更" Click="Button_Click" />
</StackPanel>

本格的なMVVMでは、コードビハインドのクリックイベントではなくICommandを使うことが多いですが、画面更新の基本は同じです。プロパティを変更し、PropertyChangedを発火することが重要です。

5-5. ObservableCollectionを使った一覧更新の例

一覧表示では、ObservableCollection<T>を使うと要素の追加・削除を画面に反映しやすくなります。

C#
using System.Collections.ObjectModel;

public class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;

public ObservableCollection<string> Items { get; } = new();

public void AddItem()
{
Items.Add($"Item {Items.Count + 1}");
}
}

WPFのXAML例です。

XML
<StackPanel>
<ListBox ItemsSource="{Binding Items}" />
<Button Content="追加" Click="Button_Click" />
</StackPanel>

ボタンクリックで追加します。

C#
private void Button_Click(object sender, RoutedEventArgs e)
{
if (DataContext is MainViewModel vm)
{
vm.AddItem();
}
}

ObservableCollection<T>はコレクション変更通知に対応しているため、Items.Add(...)を呼ぶと一覧表示が更新されます。Microsoft Learn+1

6. よくあるエラーと対処法

6-1. PropertyChangedがnullで発火しない

PropertyChangedはイベントの購読者がいない場合、nullになります。

そのため、次のように直接呼び出すと例外になる可能性があります。

C#
PropertyChanged(this, new PropertyChangedEventArgs(nameof(Message)));

安全に呼び出すには、null条件演算子を使います。

C#
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Message)));

ただし、PropertyChangednullということは、まだ画面側がそのViewModelを監視していない可能性もあります。

DataContextBindingContextが正しく設定されているか、別インスタンスを変更していないかを確認しましょう。

6-2. UIスレッド以外から更新して例外が出る

非同期処理やバックグラウンドスレッドからViewModelを更新すると、UIスレッド以外から画面に関係するコレクションやプロパティを更新して例外になることがあります。

特にObservableCollection<T>をバックグラウンドスレッドから変更すると、UIフレームワークによってはスレッド関連の例外が発生します。

対処法は、UIスレッドに処理を戻してから更新することです。

WPFではDispatcherを使います。

C#
Application.Current.Dispatcher.Invoke(() =>
{
Items.Add("New Item");
});

.NET MAUIではMainThreadを使うことがあります。

C#
MainThread.BeginInvokeOnMainThread(() =>
{
Items.Add("New Item");
});

非同期処理後に画面を更新する場合は、「どのスレッドでプロパティやコレクションを変更しているか」を確認しましょう。

6-3. ネストしたプロパティの変更が反映されない

次のようなネストしたプロパティでは注意が必要です。

C#
public User User { get; set; } = new();

public string UserName => User.Name;

User.Nameが変わっても、ViewModel側でUserNameの変更通知を出さなければ、UserNameにバインドしている画面は更新されません。

対処法の1つは、UserクラスにもINotifyPropertyChangedを実装し、必要に応じて親ViewModel側で関連プロパティの通知を出すことです。

C#
OnPropertyChanged(nameof(UserName));

ネストしたオブジェクトを扱う場合は、「どのプロパティにバインドしているか」と「どのプロパティ名で通知しているか」を確認しましょう。

6-4. 計算プロパティが更新されない

計算プロパティは、値を保持せず他のプロパティから算出されるプロパティです。

C#
public string FirstName { get; set; } = string.Empty;

public string LastName { get; set; } = string.Empty;

public string FullName => $"{LastName} {FirstName}";

この場合、FirstNameLastNameが変わっても、FullNameの変更通知を出さなければ、FullNameにバインドしている画面は更新されません。

正しくは、関連するプロパティのsetterで計算プロパティも通知します。

C#
private string _firstName = string.Empty;

public string FirstName
{
get => _firstName;
set
{
if (_firstName == value)
{
return;
}

_firstName = value;
OnPropertyChanged();
OnPropertyChanged(nameof(FullName));
}
}

private string _lastName = string.Empty;

public string LastName
{
get => _lastName;
set
{
if (_lastName == value)
{
return;
}

_lastName = value;
OnPropertyChanged();
OnPropertyChanged(nameof(FullName));
}
}

public string FullName => $"{LastName} {FirstName}";

計算プロパティそのものにsetterがない場合でも、依存元のプロパティが変わったタイミングで通知する必要があります。

6-5. async処理後に画面が更新されない

async処理でデータ取得後に画面が更新されない場合も、原因は変更通知の不足であることが多いです。

悪い例です。

C#
public async Task LoadAsync()
{
var result = await GetMessageAsync();
_message = result;
}

このコードではフィールドを直接変更しているため、PropertyChangedが発火しません。

正しくは、プロパティを通して変更します。

C#
public async Task LoadAsync()
{
var result = await GetMessageAsync();
Message = result;
}

また、非同期処理の完了後にコレクションを更新する場合は、UIスレッド上で更新されているかも確認しましょう。

7. 実装を簡潔にする書き方

7-1. SetPropertyメソッドで重複コードを減らす

複数のプロパティで毎回同じsetter処理を書くと、コードが長くなります。

そこで、共通のSetPropertyメソッドを作ると便利です。

C#
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;

public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;

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;
}

protected void OnPropertyChanged(string? propertyName)
{
PropertyChanged?.Invoke(
this,
new PropertyChangedEventArgs(propertyName)
);
}
}

継承先のViewModelでは次のように書けます。

C#
public class MainViewModel : ViewModelBase
{
private string _message = string.Empty;

public string Message
{
get => _message;
set => SetProperty(ref _message, value);
}
}

この形にすると、値が変わったときだけ通知する処理を共通化できます。

7-2. CommunityToolkit.Mvvmを使う方法

手書き実装を減らしたい場合は、CommunityToolkit.Mvvmを使う方法もあります。

CommunityToolkit.MvvmObservableObjectは、INotifyPropertyChangedINotifyPropertyChangingを実装する監視可能なオブジェクトの基底クラスです。プロパティ変更通知が必要なクラスのベースとして利用できます。Microsoft Learn+1

C#
using CommunityToolkit.Mvvm.ComponentModel;

public class MainViewModel : ObservableObject
{
private string _message = string.Empty;

public string Message
{
get => _message;
set => SetProperty(ref _message, value);
}
}

ObservableObjectにはSetPropertyが用意されているため、自分でINotifyPropertyChangedOnPropertyChangedを実装するコードを減らせます。

7-3. ObservableObjectとObservablePropertyの使い方

CommunityToolkit.Mvvmでは、ObservableProperty属性を使ってさらに簡潔に書けます。

ObservablePropertyは、注釈を付けたフィールドから監視可能なプロパティを生成するための属性です。プロパティ変更通知の定型コードを減らす目的で使われます。Microsoft Learn

C#
using CommunityToolkit.Mvvm.ComponentModel;

public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
private string message = string.Empty;
}

このコードから、ツールキットのソースジェネレーターによってMessageプロパティが生成されます。

実際には、次のようなプロパティを手書きした場合に近い動きをします。

C#
public string Message
{
get => message;
set => SetProperty(ref message, value);
}

プロパティが多いViewModelでは、ObservablePropertyを使うことでコード量を大きく減らせます。

7-4. 手書き実装とライブラリ利用の比較

手書き実装のメリットは、仕組みを理解しやすく、外部ライブラリに依存しないことです。

小規模なアプリや学習目的であれば、まずは手書きでINotifyPropertyChangedPropertyChangedEventHandlerOnPropertyChangedの関係を理解するのがおすすめです。

一方、実務ではViewModelのプロパティ数が多くなりやすいため、SetPropertyを持つ基底クラスやCommunityToolkit.Mvvmを使うと保守しやすくなります。

比較すると、次のようになります。

方法特徴向いている場面
完全手書き仕組みを理解しやすい学習、小規模アプリ
ViewModelBaseを自作共通処理をまとめられる中規模アプリ
CommunityToolkit.Mvvm定型コードを大幅に削減できる実務、大規模アプリ

重要なのは、どの方法を使っても最終的にはPropertyChangedイベントが正しく発火することです。

8. PropertyChangedEventHandlerを使うときの注意点

8-1. 不要な通知を避ける

PropertyChangedイベントは便利ですが、むやみに発火すればよいわけではありません。

値が変わっていないのに通知すると、画面側の再評価が不要に発生します。

悪い例です。

C#
set
{
_message = value;
OnPropertyChanged();
}

この場合、同じ値を代入しても通知されます。

値が変わったときだけ通知しましょう。

C#
set
{
if (_message == value)
{
return;
}

_message = value;
OnPropertyChanged();
}

8-2. 値が変わったときだけ通知する

値が変わったときだけ通知するには、setter内で現在値と新しい値を比較します。

C#
if (_message == value)
{
return;
}

汎用的に書くなら、EqualityComparer<T>.Default.Equalsを使います。

C#
if (EqualityComparer<T>.Default.Equals(field, value))
{
return false;
}

このようにしておくと、文字列、数値、参照型などに対して共通の比較処理を書きやすくなります。

8-3. プロパティ名のハードコーディングを避ける

次のような文字列指定は避けた方が安全です。

C#
OnPropertyChanged("Message");

プロパティ名を変更したときに文字列が自動で変更されないため、バグの原因になります。

代わりにnameofを使います。

C#
OnPropertyChanged(nameof(Message));

さらに、CallerMemberNameを使うとsetter内では次のように書けます。

C#
OnPropertyChanged();

リファクタリングに強いコードにするため、プロパティ名のハードコーディングはできるだけ避けましょう。

8-4. ViewModelとModelの責務を分ける

INotifyPropertyChangedはViewModelに実装することが多いですが、Modelに実装してはいけないわけではありません。

ただし、責務の分離は重要です。

ViewModelは、画面に表示するデータや画面操作に関する状態を持ちます。Modelは、業務データやドメインロジックを持ちます。

画面更新のためだけのプロパティや表示用の加工プロパティは、ViewModelに置く方が自然です。

たとえば、FirstNameLastNameを結合したFullNameを画面表示用に作る場合、ViewModelに置くと画面都合の変更に対応しやすくなります。

C#
public string FullName => $"{LastName} {FirstName}";

一方、Model自体が変更通知を必要とする設計であれば、ModelにINotifyPropertyChangedを実装することもあります。

8-5. メモリリークを防ぐイベント購読の考え方

PropertyChangedイベントを手動で購読する場合は、解除漏れに注意が必要です。

C#
viewModel.PropertyChanged += ViewModel_PropertyChanged;

長寿命のオブジェクトが短寿命のオブジェクトをイベント購読し続けると、参照が残り、ガベージコレクションされない原因になることがあります。

不要になったら購読解除します。

C#
viewModel.PropertyChanged -= ViewModel_PropertyChanged;

XAMLの通常のバインディングではフレームワーク側が面倒を見てくれる部分もありますが、自分でイベント購読を書く場合は、ライフサイクルを意識しましょう。

9. PropertyChangedEventHandlerに関するFAQ

9-1. PropertyChangedEventHandlerとEventHandlerの違いは?

EventHandlerは一般的なイベントで使われるデリゲートです。

C#
public event EventHandler? Clicked;

一方、PropertyChangedEventHandlerはプロパティ変更通知専用のデリゲートです。

C#
public event PropertyChangedEventHandler? PropertyChanged;

大きな違いは、イベント引数です。

EventHandlerでは通常EventArgsを使います。

C#
void Handler(object? sender, EventArgs e)

PropertyChangedEventHandlerではPropertyChangedEventArgsを使います。

C#
void Handler(object? sender, PropertyChangedEventArgs e)

PropertyChangedEventArgsには、変更されたプロパティ名が含まれます。

9-2. INotifyPropertyChangedは必ず必要?

常に必要というわけではありません。

画面に一度だけ値を表示し、その後変更しない場合は、INotifyPropertyChangedがなくても表示できることがあります。

しかし、実行中にViewModelのプロパティを変更し、その変更を画面に反映したい場合は、基本的にINotifyPropertyChangedが必要です。

特にMVVMパターンで画面更新を行う場合、ViewModelにINotifyPropertyChangedを実装するのが一般的です。

9-3. Listでは画面更新されないのはなぜ?

List<T>は、要素が追加・削除されたことを画面へ通知する仕組みを持っていないためです。

次のようにList<T>へ要素を追加しても、画面側が変更を検知できないことがあります。

C#
Items.Add("New Item");

一覧の変更を画面へ反映したい場合は、ObservableCollection<T>を使います。

C#
public ObservableCollection<string> Items { get; } = new();

ObservableCollection<T>は、項目の追加や削除などの変更通知を提供するコレクションです。Microsoft Learn+1

9-4. OnPropertyChangedはどこで呼ぶべき?

基本的には、プロパティのsetterで呼びます。

C#
public string Message
{
get => _message;
set
{
if (_message == value)
{
return;
}

_message = value;
OnPropertyChanged();
}
}

計算プロパティがある場合は、依存元のプロパティのsetterで一緒に通知します。

C#
OnPropertyChanged(nameof(FullName));

また、プロパティではなくメソッド内で状態を変更する場合も、最終的にはsetterを通すのがおすすめです。

C#
public void Clear()
{
Message = string.Empty;
}

9-5. MVVMではどのクラスに実装するべき?

MVVMでは、基本的にViewModelにINotifyPropertyChangedを実装します。

ViewModelは画面に表示する状態を持ち、Viewはその状態にバインディングします。ViewModelのプロパティが変わったときにPropertyChangedを発火することで、Viewが更新されます。

典型的な構成は次のとおりです。

C#
public class MainViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;

private string _title = string.Empty;

public string Title
{
get => _title;
set
{
if (_title == value)
{
return;
}

_title = value;
PropertyChanged?.Invoke(
this,
new PropertyChangedEventArgs(nameof(Title))
);
}
}
}

Modelにも変更通知が必要な場合は、Model側にINotifyPropertyChangedを実装することもあります。ただし、画面表示のための状態や加工済みプロパティはViewModelに置くと管理しやすくなります。

まとめ

C#のPropertyChangedEventHandlerは、プロパティ変更通知で使われるイベントハンドラの型です。INotifyPropertyChangedPropertyChangedイベントに使われ、ViewModelのプロパティが変わったことを画面側へ伝える役割を持ちます。

画面更新されない場合は、まず次の点を確認しましょう。

INotifyPropertyChangedを実装しているか。

PropertyChangedイベントを発火しているか。

通知するプロパティ名が正しいか。

フィールドを直接変更せず、プロパティのsetterを経由しているか。

DataContextBindingContextが正しいインスタンスを参照しているか。

一覧更新にはList<T>ではなくObservableCollection<T>を使っているか。

基本実装では、INotifyPropertyChangedを実装し、PropertyChangedEventHandler型のPropertyChangedイベントを定義し、setter内でOnPropertyChangedを呼び出します。

C#
public event PropertyChangedEventHandler? PropertyChanged;

protected void OnPropertyChanged(
[CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(
this,
new PropertyChangedEventArgs(propertyName)
);
}

実務では、SetPropertyメソッドを使って重複コードを減らしたり、CommunityToolkit.MvvmObservableObjectObservablePropertyを使って簡潔に実装したりできます。

PropertyChangedEventHandlerそのものは難しい仕組みに見えるかもしれませんが、役割はシンプルです。

「プロパティが変わったことを通知する」

この一点を理解すれば、WPF、Xamarin、MAUI、WinUIなどのC#アプリで画面更新されない問題を切り分けやすくなります。