C# PropertyChangedの使い方を基礎から解説|画面更新されない原因とINotifyPropertyChanged実装例

はじめに

C#でWPF、WinForms、MAUI、Xamarinなどの画面アプリを作っていると、「プロパティの値は変わっているのに画面が更新されない」という問題に遭遇することがあります。

その原因の多くは、画面側に「値が変わったこと」を通知できていないことです。

この通知の仕組みとしてよく使われるのが、PropertyChangedイベントとINotifyPropertyChangedインターフェースです。

特にWPFやMAUIなどでデータバインディングを使う場合、PropertyChangedを正しく実装しているかどうかは非常に重要です。この記事では、C#のPropertyChangedの基本から、実装例、画面更新されない原因、効率的な書き方までをわかりやすく解説します。

1. C#のPropertyChangedとは

1-1. PropertyChangedはプロパティ変更を通知するイベント

PropertyChangedとは、オブジェクトのプロパティの値が変更されたことを外部へ通知するためのイベントです。

C#では、プロパティの値を変更しただけでは、画面や他のオブジェクトに自動で「値が変わった」と伝わるわけではありません。

たとえば、次のようなプロパティがあるとします。

C#
public string Name { get; set; }

このNameの値を変更しても、画面側は自動的には変更を検知できません。

C#
Name = "田中";

この変更を画面に伝えるために、PropertyChangedイベントを発火します。

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

これにより、データバインディングしている画面側が「Nameプロパティが変更された」と判断し、表示を更新できるようになります。

1-2. 画面更新やデータバインディングで使われる理由

PropertyChangedは、主にデータバインディングで利用されます。

データバインディングとは、画面の部品とC#のプロパティを結びつける仕組みです。

たとえばWPFでは、次のようにTextBlockTextプロパティをViewModelのNameプロパティにバインドできます。

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

この状態でViewModel側のNameを変更したとき、PropertyChangedが発火されれば、TextBlockの表示も自動的に更新されます。

逆に、PropertyChangedを発火していない場合、ViewModelの値は変わっていても画面表示は古いままになることがあります。

1-3. INotifyPropertyChangedとの関係

PropertyChangedイベントは、INotifyPropertyChangedインターフェースとセットで使われることが多いです。

INotifyPropertyChangedは、System.ComponentModel名前空間に定義されているインターフェースです。

C#
using System.ComponentModel;

public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler? PropertyChanged;
}

このインターフェースを実装することで、そのクラスは「プロパティ変更通知に対応しているクラス」であることを表せます。

WPF、MAUI、Xamarinなどのバインディング機能は、バインド先のオブジェクトがINotifyPropertyChangedを実装しているかを確認し、PropertyChangedイベントを監視します。

1-4. WPF・WinForms・MAUI・Xamarinでの利用シーン

PropertyChangedは、C#のさまざまなUIフレームワークで使われます。

WPFでは、MVVMパターンのViewModelで頻繁に使われます。画面のTextBlockTextBoxListViewDataGridなどとViewModelのプロパティをバインドし、ViewModelの値が変わったら画面を自動更新します。

WinFormsでは、BindingSourceやデータバインディングと組み合わせることで、オブジェクトの変更を画面に反映できます。

MAUIやXamarin.Formsでも、XAMLバインディングやMVVM構成でINotifyPropertyChangedがよく使われます。スマートフォンアプリで、入力内容、一覧表示、ボタンの有効状態などをViewModelから制御する場面で重要になります。

2. PropertyChangedが必要になる場面

2-1. プロパティの値を変更しても画面が更新されないケース

最もよくあるのは、プロパティの値を変更しているのに、画面表示が更新されないケースです。

たとえば、次のようなViewModelがあるとします。

C#
public class MainViewModel
{
public string Message { get; set; } = "初期表示";

public void ChangeMessage()
{
Message = "変更後のメッセージ";
}
}

XAMLでMessageをバインドしていても、このままではChangeMessageを呼び出したあとに画面が更新されない場合があります。

理由は、Messageの値が変わったことを画面に通知していないためです。

このような場合にINotifyPropertyChangedを実装し、Message変更時にPropertyChangedを発火する必要があります。

2-2. MVVMパターンでViewModelからViewへ通知したいケース

MVVMパターンでは、Viewは画面、ViewModelは画面に表示するデータや操作ロジックを担当します。

ViewModel側で値を変更したとき、Viewへ直接アクセスして画面を書き換えるのではなく、バインディングを通して変更を通知します。

C#
public string UserName
{
get => _userName;
set
{
_userName = value;
OnPropertyChanged();
}
}

このようにViewModelでPropertyChangedを発火することで、ViewはViewModelの変更を自動的に反映できます。

MVVMでは、ViewModelがViewを直接知らない構成にすることが多いため、PropertyChangedはViewModelからViewへ変更を伝えるための重要な仕組みです。

2-3. ListViewやDataGridの表示を自動更新したいケース

ListViewDataGridにオブジェクトの一覧を表示している場合、各行のプロパティ変更を画面へ反映したいことがあります。

たとえば、商品一覧を表示していて、商品の価格だけを変更したい場合です。

C#
public class Product : INotifyPropertyChanged
{
private int _price;

public string Name { get; set; } = "";

public int Price
{
get => _price;
set
{
if (_price == value) return;
_price = value;
OnPropertyChanged();
}
}

public event PropertyChangedEventHandler? PropertyChanged;

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

一覧自体の追加・削除にはObservableCollection<T>、各要素のプロパティ変更にはINotifyPropertyChangedが必要です。

つまり、行の追加や削除を通知したい場合と、行の中の値の変更を通知したい場合では、必要な仕組みが異なります。

2-4. 双方向バインディングで入力値を反映したいケース

TextBoxなどの入力コントロールでは、画面からViewModelへ値を反映し、さらにViewModel側の変更を画面へ戻す双方向バインディングを使うことがあります。

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

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

この場合、ユーザーが入力した値がNameプロパティへ反映されます。

さらに、ViewModel側でNameを変更したときにも、PropertyChangedを発火すれば画面のTextBoxも更新されます。

双方向バインディングでは、ViewからViewModel、ViewModelからViewの両方向で値が同期されるため、PropertyChangedの実装が重要になります。

3. INotifyPropertyChangedの基本実装

3-1. INotifyPropertyChangedインターフェースを実装する

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

C#
using System.ComponentModel;

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

INotifyPropertyChangedを実装するには、PropertyChangedイベントを定義する必要があります。

このイベントが、プロパティ変更を外部へ通知する入口になります。

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

PropertyChangedイベントは、次のように定義します。

C#
public event PropertyChangedEventHandler? PropertyChanged;

PropertyChangedEventHandlerは、プロパティ変更通知用のイベントハンドラーです。

C# 8以降でNullable参照型を有効にしている場合は、イベントが未購読の可能性を考慮して?を付けることが一般的です。

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

毎回PropertyChanged?.Invoke(...)を書くとコードが重複するため、通常は通知用のメソッドを作成します。

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

このメソッドを作っておくと、各プロパティのsetterから簡単に通知できます。

C#
OnPropertyChanged(nameof(Name));

3-4. setter内でPropertyChangedを発火する

PropertyChangedは、基本的にプロパティのsetter内で値を変更したあとに発火します。

C#
private string _name = "";

public string Name
{
get => _name;
set
{
if (_name == value) return;

_name = value;
OnPropertyChanged(nameof(Name));
}
}

ポイントは、値を変更したあとに通知することです。

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

また、値が変わっていない場合は通知しないようにすると、無駄な画面更新を防げます。

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

nameof(Name)を毎回書いてもよいですが、CallerMemberNameを使うとプロパティ名の指定を省略できます。

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

public class Person : INotifyPropertyChanged
{
private string _name = "";

public string Name
{
get => _name;
set
{
if (_name == value) return;

_name = value;
OnPropertyChanged();
}
}

public event PropertyChangedEventHandler? PropertyChanged;

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

OnPropertyChanged()と書くだけで、呼び出し元のプロパティ名であるNameが自動的に渡されます。

プロパティ名を文字列で直接書く必要がなくなるため、タイプミスやリファクタリング漏れを防ぎやすくなります。

4. C# PropertyChangedの実装例

4-1. 最小構成のサンプルコード

まずは、C#でPropertyChangedを使う最小構成の例です。

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

public class MainViewModel : INotifyPropertyChanged
{
private string _message = "初期メッセージ";

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

_message = value;
OnPropertyChanged();
}
}

public event PropertyChangedEventHandler? PropertyChanged;

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

このコードでは、Messageプロパティの値が変更されたときにPropertyChangedを発火します。

画面側でMessageにバインドしていれば、変更後の値が自動的に表示されます。

4-2. ViewModelでの実装例

ViewModelでは、複数のプロパティを扱うことが多くなります。

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

public class UserViewModel : INotifyPropertyChanged
{
private string _firstName = "";
private string _lastName = "";
private int _age;

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

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

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

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

public int Age
{
get => _age;
set
{
if (_age == value) return;

_age = value;
OnPropertyChanged();
}
}

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

public event PropertyChangedEventHandler? PropertyChanged;

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

FullNameFirstNameLastNameから計算されるプロパティです。

そのため、FirstNameまたはLastNameが変わったときには、FullNameの変更も通知する必要があります。

4-3. XAMLバインディングと組み合わせる例

WPFでViewModelを画面にバインドする例を見てみましょう。

XML
<Window x:Class="SampleApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SampleApp"
Title="PropertyChanged Sample"
Height="200"
Width="300">

<StackPanel Margin="20">
<TextBox Text="{Binding Message, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="{Binding Message}" Margin="0,10,0,0" />
</StackPanel>
</Window>

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

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

ViewModelは次のようにします。

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

public class MainViewModel : INotifyPropertyChanged
{
private string _message = "こんにちは";

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

_message = value;
OnPropertyChanged();
}
}

public event PropertyChangedEventHandler? PropertyChanged;

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

この例では、TextBoxに入力した内容がMessageへ反映され、同時にTextBlockにも表示されます。

UpdateSourceTrigger=PropertyChangedを指定しているため、テキスト入力中にもViewModelへ値が反映されやすくなります。

4-4. 複数プロパティを通知する例

あるプロパティの変更によって、他のプロパティの表示も変わる場合があります。

C#
private int _quantity;
private int _unitPrice;

public int Quantity
{
get => _quantity;
set
{
if (_quantity == value) return;

_quantity = value;
OnPropertyChanged();
OnPropertyChanged(nameof(TotalPrice));
}
}

public int UnitPrice
{
get => _unitPrice;
set
{
if (_unitPrice == value) return;

_unitPrice = value;
OnPropertyChanged();
OnPropertyChanged(nameof(TotalPrice));
}
}

public int TotalPrice => Quantity * UnitPrice;

TotalPriceQuantityUnitPriceから計算されます。

そのため、数量または単価が変更されたときに、TotalPriceの変更も通知する必要があります。

この通知を忘れると、QuantityUnitPriceの表示は更新されても、合計金額の表示だけが古いままになることがあります。

4-5. 計算プロパティを更新する例

計算プロパティはsetterを持たないことが多いため、自分自身ではPropertyChangedを発火できません。

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

このような場合は、依存元のプロパティのsetter内で計算プロパティの変更を通知します。

C#
private string _firstName = "";
private string _lastName = "";

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

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

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

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

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

計算プロパティが更新されない場合は、依存しているプロパティの変更時に通知しているかを確認しましょう。

5. 画面更新されない主な原因と対処法

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

画面更新されない原因として最も基本的なのが、INotifyPropertyChangedを実装していないことです。

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

このようなクラスでは、プロパティの値が変わっても変更通知が行われません。

対処法は、INotifyPropertyChangedを実装し、PropertyChangedイベントを定義することです。

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

5-2. PropertyChangedをsetterで呼び出していない

INotifyPropertyChangedを実装していても、setter内でPropertyChangedを呼び出していなければ画面は更新されません。

悪い例は次のようなコードです。

C#
private string _message = "";

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

この場合、値は変わりますが通知されません。

正しくは、setter内でOnPropertyChanged()を呼び出します。

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

_message = value;
OnPropertyChanged();
}
}

5-3. プロパティ名が間違っている

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

C#
OnPropertyChanged("Massage");

この例では、本来Messageと書くべきところをMassageと書いてしまっています。

文字列の直書きはミスが起きやすいため、nameofまたはCallerMemberNameを使いましょう。

C#
OnPropertyChanged(nameof(Message));

または、CallerMemberNameを使って次のようにします。

C#
OnPropertyChanged();

5-4. フィールドだけを変更してプロパティを経由していない

setterで通知する実装にしていても、privateフィールドを直接変更すると通知が発火されません。

C#
_message = "変更後";

このようにフィールドを直接変更すると、Messageプロパティのsetterを通らないため、OnPropertyChanged()が呼ばれません。

対処法は、外部から値を変更するときはプロパティを経由することです。

C#
Message = "変更後";

クラス内部でも、通知が必要な変更ではプロパティを経由するように意識しましょう。

5-5. DataContextが正しく設定されていない

WPFやMAUIでバインディングが動かない場合、DataContextBindingContextが正しく設定されていないことがあります。

WPFでは、たとえば次のように設定します。

C#
DataContext = new MainViewModel();

MAUIやXamarin.Formsでは、次のようにBindingContextを設定します。

C#
BindingContext = new MainViewModel();

PropertyChangedの実装が正しくても、画面が別のオブジェクトを見ている場合、表示は更新されません。

画面更新されないときは、まずバインディング先のオブジェクトが本当に意図したViewModelになっているかを確認しましょう。

5-6. BindingのModeやUpdateSourceTriggerが適切でない

バインディングの設定が原因で、期待したタイミングで値が更新されないこともあります。

WPFのTextBoxでは、次のように書くことで入力中にViewModelへ反映できます。

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

Mode=TwoWayは、ViewとViewModelの双方向で値を同期する設定です。

UpdateSourceTrigger=PropertyChangedは、TextBoxの値が変わるたびにバインド元へ反映する設定です。

この指定がない場合、フォーカスが外れるまでViewModelへ反映されないことがあります。

5-7. ObservableCollectionではなくListを使っている

一覧の追加・削除が画面に反映されない場合、List<T>を使っていることが原因かもしれません。

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

List<T>は、要素の追加や削除を画面へ通知しません。

一覧の変更を通知したい場合は、ObservableCollection<T>を使います。

C#
using System.Collections.ObjectModel;

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

ただし、ObservableCollection<T>が通知するのは、要素の追加、削除、移動、リセットなどのコレクション変更です。

コレクション内の各要素のプロパティ変更を通知したい場合は、要素側のクラスにINotifyPropertyChangedを実装する必要があります。

5-8. UIスレッド以外から更新している

非同期処理や別スレッドからプロパティを更新した場合、画面が更新されない、または例外が発生することがあります。

UIフレームワークでは、画面部品を操作できるスレッドが決まっていることが多いためです。

WPFでは、UIスレッドで更新するためにDispatcherを使うことがあります。

C#
Application.Current.Dispatcher.Invoke(() =>
{
Message = "更新完了";
});

MAUIでは、次のようにメインスレッドで処理する方法があります。

C#
MainThread.BeginInvokeOnMainThread(() =>
{
Message = "更新完了";
});

非同期処理後に画面更新されない場合は、プロパティ変更がUIスレッドで行われているかを確認しましょう。

6. PropertyChanged実装時の注意点

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

PropertyChangedは、値が本当に変わったときだけ発火するのが基本です。

C#
if (_name == value) return;

このチェックを入れないと、同じ値を代入しただけでも画面更新が発生します。

小さなアプリでは問題になりにくいですが、プロパティ数が多い画面や一覧表示では、無駄な通知がパフォーマンス低下につながることがあります。

6-2. null条件演算子で安全にイベントを発火する

PropertyChangedイベントは、購読者がいない場合はnullです。

そのため、直接呼び出すとNullReferenceExceptionが発生する可能性があります。

C#
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));

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

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

この書き方であれば、イベントの購読者がいない場合は何もせず、購読者がいる場合だけ通知されます。

6-3. プロパティ名の文字列直書きを避ける

次のような文字列直書きは、できるだけ避けましょう。

C#
OnPropertyChanged("Name");

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

代わりにnameofを使います。

C#
OnPropertyChanged(nameof(Name));

または、CallerMemberNameを使えば、setter内ではプロパティ名の指定自体を省略できます。

C#
OnPropertyChanged();

6-4. イベントの多重発火に注意する

1回の操作で同じプロパティのPropertyChangedが何度も発火すると、画面更新が無駄に増えます。

たとえば、setter内と別メソッドの両方で同じ通知をしている場合です。

C#
Name = "田中";
OnPropertyChanged(nameof(Name));

このように、Nameのsetter内ですでに通知しているのに、外側でも通知すると二重通知になります。

基本的には、通知はsetterに集約するのがおすすめです。

ただし、計算プロパティのように明示的な追加通知が必要なケースもあります。

C#
OnPropertyChanged(nameof(FullName));

必要な通知と不要な通知を分けて考えることが大切です。

6-5. メモリリークを防ぐための考え方

PropertyChangedはイベントなので、購読関係によってはメモリリークにつながる可能性があります。

特に、長寿命のオブジェクトが短寿命のオブジェクトのイベントを購読している場合や、その逆の場合には注意が必要です。

一般的なデータバインディングでは、フレームワーク側が適切に管理してくれることも多いですが、自分でPropertyChangedイベントを購読する場合は、不要になったタイミングで解除することを検討しましょう。

C#
viewModel.PropertyChanged -= ViewModel_PropertyChanged;

また、WPFでは弱参照イベントパターンを検討する場面もあります。

イベント購読を増やしすぎないこと、寿命の長いオブジェクトに不要な参照を残さないことが重要です。

7. PropertyChangedを効率よく書く方法

7-1. 共通のSetPropertyメソッドを作る

プロパティが増えると、毎回同じようなsetterを書くのが面倒になります。

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

C#
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));
}
}

このSetPropertyを使うと、各プロパティは次のように短く書けます。

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

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

値が変わったかどうかの判定や通知処理を共通化できるため、コードの重複を減らせます。

7-2. ViewModelBaseを作成して再利用する

複数のViewModelでINotifyPropertyChangedを実装する場合は、基底クラスとしてViewModelBaseを作るのが一般的です。

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

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

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

各ViewModelは、この基底クラスを継承します。

C#
public class UserViewModel : ViewModelBase
{
private string _name = "";

public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
}

これにより、ViewModelごとにPropertyChangedイベントやOnPropertyChangedメソッドを何度も書く必要がなくなります。

7-3. CommunityToolkit.Mvvmを使ってコード量を減らす

CommunityToolkit.Mvvmを使うと、INotifyPropertyChangedの実装を大きく減らせます。

たとえば、ObservableObjectを継承すると、SetPropertyを利用できます。

C#
using CommunityToolkit.Mvvm.ComponentModel;

public class MainViewModel : ObservableObject
{
private string _message = "";

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

さらに、ソースジェネレーターを使うと、属性を付けるだけでプロパティを生成できます。

C#
using CommunityToolkit.Mvvm.ComponentModel;

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

この場合、MessageプロパティやPropertyChanged通知に必要なコードが自動生成されます。

プロパティが多いViewModelでは、手書きコードを大幅に減らせるため便利です。

7-4. Fody.PropertyChangedを使う方法

Fody.PropertyChangedを使うと、ビルド時にPropertyChanged通知のコードを自動的に織り込むことができます。

通常は、次のようなシンプルなプロパティに対して通知処理を自動追加できます。

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

public string Message { get; set; } = "";
}

手書きのsetterを減らせる一方で、ビルド時のコード変更やライブラリの挙動を理解しておく必要があります。

チーム開発では、導入ルールやデバッグ時の確認方法を決めておくと安心です。

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

手書き実装のメリットは、動作がわかりやすく、細かい制御がしやすいことです。

小規模なアプリや学習段階では、まず手書きでINotifyPropertyChangedを実装すると理解しやすくなります。

一方、ViewModelやプロパティが多いアプリでは、手書き実装が増えすぎて保守が大変になります。

その場合は、ViewModelBaseCommunityToolkit.MvvmFody.PropertyChangedなどを使うと、コード量を減らせます。

おすすめの考え方は、次のようになります。

C#
// 学習・小規模アプリ
// → 手書き実装またはViewModelBase

// 中規模以上のMVVMアプリ
// → CommunityToolkit.Mvvm

// 既存コードの通知処理をまとめて簡略化したい場合
// → Fody.PropertyChangedも選択肢

まずは手書きで仕組みを理解し、その後でライブラリを導入すると、トラブルが起きたときにも原因を追いやすくなります。

8. PropertyChangedと関連機能の違い

8-1. PropertyChangedとPropertyChangingの違い

PropertyChangedは、プロパティの値が変更されたあとに通知するイベントです。

一方、PropertyChangingは、プロパティの値が変更される前に通知するイベントです。

C#
// 変更前
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(nameof(Name)));

// 値を変更
_name = value;

// 変更後
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));

画面更新で主に使われるのはPropertyChangedです。

PropertyChangingは、変更前の値を利用したい場合や、変更前後の差分を扱いたい場合に使われることがあります。

8-2. INotifyPropertyChangedとINotifyCollectionChangedの違い

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

一方、INotifyCollectionChangedは、コレクションの変更を通知するためのインターフェースです。

たとえば、次のような違いがあります。

C#
// プロパティの値が変わった
Product.Name = "新商品";

// コレクションに要素が追加された
Products.Add(new Product());

前者にはINotifyPropertyChanged、後者にはINotifyCollectionChangedが関係します。

C#では、ObservableCollection<T>INotifyCollectionChangedを実装しています。

8-3. ObservableCollectionとの役割の違い

ObservableCollection<T>は、コレクションの追加・削除などを通知するためのクラスです。

C#
public ObservableCollection<Product> Products { get; } = new();

この場合、Products.Add(...)Products.Remove(...)は画面に通知されます。

ただし、Productの中のNamePriceが変わったことは、ObservableCollection<T>だけでは通知できません。

C#
Products[0].Price = 1200;

この変更を画面に反映するには、Productクラス側でINotifyPropertyChangedを実装する必要があります。

つまり、役割は次のように分かれます。

C#
// 一覧の追加・削除
ObservableCollection<T>

// 一覧内の各要素のプロパティ変更
INotifyPropertyChanged

8-4. DependencyPropertyとの違い

DependencyPropertyは、WPFなどで使われる特殊なプロパティシステムです。

主にカスタムコントロールやユーザーコントロールで、バインディング、スタイル、アニメーション、既定値、値の継承などを扱うために使われます。

一方、INotifyPropertyChangedは、通常のC#クラスやViewModelでプロパティ変更を通知するために使われます。

ViewModelを作る場合は、基本的にINotifyPropertyChangedを使います。

カスタムコントロールを作り、そのコントロールのプロパティをXAMLから設定したい場合は、DependencyPropertyを使うことがあります。

8-5. BindingSourceとの違い

BindingSourceは、主にWinFormsで使われるデータバインディング用のコンポーネントです。

データソースと画面コントロールの間に入り、データの管理や通貨管理、並び替え、フィルタリングなどを扱いやすくします。

PropertyChangedは、個々のオブジェクトのプロパティ変更を通知するイベントです。

WinFormsでオブジェクトの変更を画面に反映したい場合、BindingSourceINotifyPropertyChangedを組み合わせることがあります。

役割としては、BindingSourceはデータバインディングを仲介する仕組み、PropertyChangedは値の変更を知らせる仕組みです。

9. よくある質問

9-1. PropertyChangedはどのタイミングで呼ぶべきか

PropertyChangedは、プロパティの値を変更したあとに呼ぶのが基本です。

C#
_name = value;
OnPropertyChanged();

値を変更する前に通知すると、画面側がプロパティを読み取ったときに古い値を取得してしまう可能性があります。

また、値が変わっていない場合は呼ばない方がよいです。

C#
if (_name == value) return;

このようにチェックしてから通知すると、無駄な画面更新を減らせます。

9-2. privateフィールドにも通知は必要か

通常、privateフィールド自体に通知は不要です。

通知が必要なのは、画面や外部から参照されるpublicプロパティです。

C#
private string _name = "";

public string Name
{
get => _name;
set
{
if (_name == value) return;

_name = value;
OnPropertyChanged();
}
}

画面は_nameではなくNameにバインドします。

そのため、通知するプロパティ名もNameになります。

9-3. staticプロパティでPropertyChangedは使えるか

通常のINotifyPropertyChangedは、インスタンスのプロパティ変更を通知するための仕組みです。

そのため、staticプロパティとは相性がよくありません。

C#
public static string AppName { get; set; } = "";

staticプロパティの変更を通知したい場合は、独自にstaticイベントを用意する方法があります。

C#
public static event EventHandler<PropertyChangedEventArgs>? StaticPropertyChanged;

private static string _appName = "";

public static string AppName
{
get => _appName;
set
{
if (_appName == value) return;

_appName = value;
StaticPropertyChanged?.Invoke(null, new PropertyChangedEventArgs(nameof(AppName)));
}
}

ただし、通常の画面バインディングではstaticプロパティの扱いがフレームワークによって異なります。

ViewModelで扱える値は、できるだけインスタンスプロパティとして設計する方がシンプルです。

9-4. 非同期処理後に画面更新されない場合はどうするか

非同期処理後に画面更新されない場合は、次の点を確認します。

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

このようにプロパティを経由して値を代入しているかを確認しましょう。

悪い例は、フィールドを直接変更しているケースです。

C#
_message = result;

これではsetterを通らないため、PropertyChangedが発火しません。

また、別スレッドから更新している場合は、UIスレッドで更新する必要があることがあります。

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

C#
Application.Current.Dispatcher.Invoke(() =>
{
Message = result;
});

MAUIでは次のようにメインスレッドへ戻します。

C#
MainThread.BeginInvokeOnMainThread(() =>
{
Message = result;
});

非同期処理では、「プロパティを経由しているか」「UIスレッドで更新しているか」の2点を確認すると原因を見つけやすくなります。

9-5. プロパティが多い場合の実装方法はどうするか

プロパティが多い場合は、すべてを手書きするとコードが長くなります。

まずはViewModelBaseSetPropertyを作って共通化するのがおすすめです。

C#
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#
private string _name = "";

public string Name
{
get => _name;
set => SetProperty(ref _name, value);
}

さらにコード量を減らしたい場合は、CommunityToolkit.MvvmObservableObjectObservablePropertyを使う方法があります。

C#
public partial class UserViewModel : ObservableObject
{
[ObservableProperty]
private string name = "";
}

プロパティ数が多いアプリでは、手書きにこだわりすぎず、共通基底クラスやライブラリを活用すると保守しやすくなります。

まとめ

C#のPropertyChangedは、プロパティの値が変更されたことを画面や外部へ通知するためのイベントです。

WPF、WinForms、MAUI、Xamarinなどでデータバインディングを使う場合、INotifyPropertyChangedを正しく実装しているかどうかが画面更新の重要なポイントになります。

基本的な実装の流れは、次のとおりです。

C#
public class MainViewModel : INotifyPropertyChanged
{
private string _message = "";

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

_message = value;
OnPropertyChanged();
}
}

public event PropertyChangedEventHandler? PropertyChanged;

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

画面更新されない場合は、INotifyPropertyChangedを実装しているか、setterでPropertyChangedを発火しているか、DataContextやBinding設定が正しいか、ListではなくObservableCollectionを使うべき場面ではないかを確認しましょう。

また、プロパティが増えてきたら、SetPropertyを持つViewModelBaseを作る、またはCommunityToolkit.Mvvmなどのライブラリを使うことで、コードを簡潔にできます。

PropertyChangedはC#の画面アプリ開発、特にMVVMパターンでは欠かせない基礎知識です。仕組みを理解しておくことで、「値は変わっているのに画面が更新されない」という問題を効率よく解決できるようになります。