C#でプロパティ変更をトリガーに処理を実行する方法|EventHandlerとINotifyPropertyChangedの実装パターン

C# で「ある変数の値が変わった瞬間に処理を走らせたい」というニーズは、業務アプリやツール開発で非常によく登場します。本記事では、プロパティの変更をトリガーに処理を実行する王道パターンから、軽量な代替案、UI バインディング向けの INotifyPropertyChanged まで、実務レベルで使える具体例と注意点をまとめて解説します。

目次

C#で「変数の変更をトリガーに処理を走らせる」とは?

C# では、単純なフィールド(変数)が変更されたことを、ランタイムが自動で検知してくれるわけではありません。int age; のようなフィールドの値を変更しても、「変わったよ」という通知は自動では飛びません。

そこでよく使われるのがプロパティの setterを入り口とした実装です。値を直接フィールドではなくプロパティ経由で設定し、その中で「値が変わったかどうか」を判定して、変わったタイミングだけでイベントを発火します。

この記事では、次の3パターンを軸に解説していきます。

  • 標準的なイベントパターン:EventHandler<TEventArgs> を使う方法
  • 軽量な代替案:Action などのデリゲートを公開してしまう方法
  • UI バインディング向け:INotifyPropertyChanged インターフェースを実装する方法

まずは、.NET の王道ともいえる EventHandler パターン から見ていきましょう。

王道パターン:プロパティの setter + EventHandler<TEventArgs>

最もベーシックで汎用的なのが、専用のイベントとイベント引数クラスを定義して、プロパティの setter でイベントを発火するパターンです。以下は、Age プロパティの変更を検知して通知するクラスの例です。


using System;

public class Person
{
    private int _age;

    // 1) イベント引数クラス
    public sealed class AgeChangedEventArgs : EventArgs
    {
        public int Age { get; }

        public AgeChangedEventArgs(int age)
        {
            Age = age;
        }
    }

    // 2) イベント本体(標準の EventHandler&lt;T&gt; を利用)
    public event EventHandler&lt;AgeChangedEventArgs&gt;? AgeChanged;

    // 3) 発火用の保護メソッド(継承で差し替え可能にするのが慣例)
    protected virtual void OnAgeChanged(int newAge)
    {
        AgeChanged?.Invoke(this, new AgeChangedEventArgs(newAge));
    }

    // 4) 値が変わったときだけ発火するプロパティ
    public int Age
    {
        get =&gt; _age;
        set
        {
            if (_age == value)
            {
                // 同じ値なら何もしない(無駄な通知を防ぐ)
                return;
            }

            _age = value;
            OnAgeChanged(_age);
        }
    }

    // (任意)同一クラス内で自分自身に購読する例
    public Person()
    {
        AgeChanged += (_, e) =&gt; LogAge(e.Age);
    }

    private void LogAge(int age)
    {
        Console.WriteLine($"年齢が {age} に変わりました");
    }
}

この実装を外部から利用するときは、次のようにイベントに購読(+=)し、プロパティを変更します。


var p = new Person();

// 外部からの購読
p.AgeChanged += (sender, e) =&gt;
{
    Console.WriteLine($"通知を受け取りました。新しい年齢: {e.Age}");
};

p.Age = 10;  // このタイミングで AgeChanged イベントが発火する
p.Age = 10;  // 値が変わらないのでイベントは発火しない
p.Age = 20;  // 再度イベントが発火する

このパターンを構成する要素を整理すると、以下のようになります。

要素役割実装例
イベント引数変更された値など、通知したい情報をまとめるクラスAgeChangedEventArgs
イベント本体購読者が登録されるイベントpublic event EventHandler<AgeChangedEventArgs>? AgeChanged;
発火メソッドイベントを呼び出すための共通メソッドprotected virtual void OnAgeChanged(int newAge)
プロパティ setter値が変わったかチェックし、変わったときだけ発火Age プロパティの set
購読者イベントに += して処理を登録する側p.AgeChanged += ...

この構成をひとつテンプレートとして覚えておくと、どのプロパティに対しても同じ考え方で「変更をトリガーに処理を実行する」仕組みを実装できます。

イベントとデリゲートの基礎をざっくり整理

「デリゲートとイベントの違いがよく分からない」という声も多いので、ここで簡単に整理しておきます。

  • デリゲート:メソッドを「変数」として扱うための型(関数ポインタのようなもの)
  • イベント:デリゲートを外部公開するときの「安全なラッパー」

デリゲート単体でもコールバックの登録はできますが、event キーワードを付けることで、外部からは += と -= だけが許可され、勝手に呼び出したり上書きしたりできなくなります(カプセル化が向上します)。

項目eventデリゲート フィールド(例:Action)
外部からの呼び出し不可(クラス内部からのみ呼べる)可能(フィールドにアクセスできれば呼べてしまう)
外部からの再代入不可(+= / -= のみ)可能(= で上書きされる危険がある)
用途通知・購読モデル(イベント)コールバックの受け渡しなど、より自由な用途
推奨度外部に通知したいときは基本こちら内部専用、または割り切った API に限定

「外部に対して何かが起きたことを通知したい」「購読者が複数いるかもしれない」といったケースでは、基本的に event を使う と覚えておくと安全です。

同一クラス内・外部クラスからのイベント購読パターン

先ほどの Person クラスでは、コンストラクター内で自分自身のイベントに購読していました。


public Person()
{
    AgeChanged += (_, e) =&gt; LogAge(e.Age);
}

このように「同じクラスの中で完結する処理」であれば、コンストラクターで自分自身に登録してしまうのがシンプルです。「このクラス内部では、年齢が変わるたびにログを書きたい」といった用途に向きます。

一方で、「他のクラスに通知して、別の処理を行いたい」場合は、外部で次のように購読します。


public class PersonObserver
{
    public void Run()
    {
        var person = new Person();

        person.AgeChanged += OnPersonAgeChanged;

        person.Age = 30;
        person.Age = 31;
    }

    private void OnPersonAgeChanged(object? sender, Person.AgeChangedEventArgs e)
    {
        Console.WriteLine($"Observer: 年齢が {e.Age} に変更されました。");
    }
}

現場でありがちなパターンとしては、次のようなものがあります。

  • ドメインモデル(Person など)の変更を、UI やログ、監視クラスに通知する
  • 設定値オブジェクトの変更を、実行中のサービスに伝える(例:ログレベルの動的変更)
  • バックグラウンド処理の進捗(Progress)を UI に通知する

このとき、「イベントをどこまで公開するか」「どの層から購読させるか」を意識して設計すると、後から仕様変更が入っても修正しやすくなります。

軽量な代替案:Action を使った公開デリゲート

「イベントというほど大げさな仕組みはいらない」「購読者は 1 箇所だけでよい」といったケースでは、シンプルに Action を公開するだけ という手もあります。


public class PersonLite
{
    private int _age;

    // イベントではなく、公開デリゲート
    public Action&lt;int&gt;? OnAgeChanged;

    public int Age
    {
        get =&gt; _age;
        set
        {
            if (_age == value)
            {
                return;
            }

            _age = value;
            OnAgeChanged?.Invoke(_age);
        }
    }
}

使い方も非常にシンプルです。


var p = new PersonLite();

// コールバックを 1 つ登録するだけ
p.OnAgeChanged = age =&gt; Console.WriteLine($"Lite: {age}");

p.Age = 5;
p.Age = 6;

ただし、この方式には注意点があります。

  • 外部から p.OnAgeChanged = null; と上書きできてしまう
  • 複数の購読者を簡単に扱う設計にはなっていない(自分で += / -= を実装する必要がある)
  • 「通知モデル」というより「コールバックを 1 個渡してもらう」イメージに近い

そのため、ライブラリや共通クラスとして広く使われる API には向きません。しかし、アプリ内部で完結する一時的なクラスや、小規模ツールの中だけで使うのであれば、コード量を減らせるので十分に有効な選択肢です。

複数プロパティの変更を扱うなら INotifyPropertyChanged

WPF・WinForms・MAUI などの UI フレームワークでは、プロパティが変わったことをビュー(画面)に通知し、自動的に UI を更新させたい場面が多くあります。このとき定番となるのが、INotifyPropertyChanged インターフェースです。

INotifyPropertyChanged は非常にシンプルで、次の 1 つのイベントだけを持つインターフェースです。


public interface INotifyPropertyChanged
{
    event PropertyChangedEventHandler? PropertyChanged;
}

このイベントに対して、プロパティ名を文字列で通知していく、というスタイルになります。実装例を見てみましょう。


using System.ComponentModel;
using System.Runtime.CompilerServices;

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

    // 呼び出し元のプロパティ名を自動補完するユーティリティメソッド
    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        if (propertyName is null)
        {
            return;
        }

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

    private int _age;
    public int Age
    {
        get =&gt; _age;
        set
        {
            if (_age == value)
            {
                return;
            }

            _age = value;
            OnPropertyChanged(); // propertyName に "Age" が自動で入る
        }
    }

    private string? _name;
    public string? Name
    {
        get =&gt; _name;
        set
        {
            if (_name == value)
            {
                return;
            }

            _name = value;
            OnPropertyChanged(); // こちらは "Name" が自動で通知される
        }
    }
}

このような ViewModel を WPF の XAML などからバインディングすると、Age や Name をコード側で変更したときに、自動的に UI が更新されます。

「専用イベントを作る場合」と「INotifyPropertyChanged を使う場合」の違いをざっくり比較すると、次のようになります。

観点専用イベント(例:AgeChanged)INotifyPropertyChanged
通知の粒度プロパティごとにイベントを分けるプロパティ名を文字列で通知(1本のイベント)
主な用途ドメインイベント、ロジック層での通知UI バインディング、MVVM
拡張性イベントごとに専用の引数を設計できる基本は「どのプロパティが変わったか」だけ
実装コストプロパティごとにイベント+EventArgs が増える1 つのイベントで全プロパティをカバー

UI バインディングが目的なら、まずは INotifyPropertyChanged を使うのが王道です。一方、「処理の意味に応じて、しっかりしたイベントを定義したい」ときには専用イベントを使う、と役割を分けて考えると整理しやすくなります。

実務でハマりやすいポイントとアンチパターン

プロパティ変更イベントは便利ですが、実務では次のような落とし穴がよく問題になります。ここでは代表的なものと対策をまとめます。

同じ値を何度も通知してしまう

ありがちなミスが、「同じ値が再セットされたときにもイベントが発火してしまう」問題です。通知を受ける側が UI 更新や重い処理を行っている場合、パフォーマンス低下や無駄な再描画につながります。

これを防ぐための基本テクニックが、setter での比較です。


set
{
    if (_age == value)
    {
        return; // 値が変わらなければ何もしない
    }

    _age = value;
    OnAgeChanged(_age);
}

特に UI バインディング系の ViewModel では、全プロパティにこの比較条件を入れることを強くおすすめします。共通化するために、ベースクラスに「値をセットして変更時だけ PropertyChanged を発火する」ヘルパーメソッドを用意することも多いです。

イベント購読しっぱなしによるメモリリーク

イベントを使うときに侮れないのが、購読解除漏れによるメモリリークです。長寿命オブジェクト(シングルトンや静的クラスなど)のイベントに短命オブジェクトが購読し、そのまま解除されないと、「参照が残り続けて GC されない」という状況が起きます。

回避策としては、次のようなものがあります。

  • 寿命が長い側(イベントを発行する側)に、明示的な解除メソッドを用意し、不要になったタイミングで -= を呼ぶ
  • IDisposable を実装し、Dispose 内で購読解除を行う
  • UI フレームワークが提供している「WeakEvent パターン」「イベントアグリゲーター」などを活用する

たとえば、購読側のクラスに IDisposable を実装し、破棄時に解除する例は次のようになります。


public class PersonObserver : IDisposable
{
    private readonly Person _person;

    public PersonObserver(Person person)
    {
        _person = person;
        _person.AgeChanged += OnAgeChanged;
    }

    private void OnAgeChanged(object? sender, Person.AgeChangedEventArgs e)
    {
        Console.WriteLine($"Observer: {e.Age}");
    }

    public void Dispose()
    {
        _person.AgeChanged -= OnAgeChanged; // 購読解除を忘れない
    }
}

イベントを多用するシステムでは、「イベントを購読したら、どこで解除するか」を設計段階で決めておくことが重要です。

マルチスレッドと UI スレッドの問題

バックグラウンドスレッドで処理を行い、途中経過をイベントで通知するようなケースでは、UI スレッドにマーシャリングする必要があるかどうか に注意してください。WPF や WinForms の UI コントロールは、基本的に UI スレッド以外から直接触ることができません。

そのため、「イベントはバックグラウンドスレッドから発火する」「イベントハンドラーの中で UI を触る」というコードを書くと、例外が発生したり、予期しない動作になったりします。この場合は、イベントを受け取った側で Dispatcher や SynchronizationContext を使って UI スレッドへ処理を移す設計にするのが一般的です。

シナリオ別:どのパターンを選ぶべきか

ここまで紹介した 3 つのパターンを、「どんなシナリオで使い分けるか」という観点で整理してみます。

シナリオおすすめパターン理由
ドメインモデルの状態変化をアプリ全体に通知したい専用イベント(EventHandler<TEventArgs>)意味のあるイベント名・引数で表現できるため、保守性が高い
WPF/MAUI などの UI バインディングINotifyPropertyChangedフレームワーク側が標準で対応しており、XAML バインディングと相性がよい
小さなツールで、1 箇所にだけ通知を送りたい公開デリゲート(Action<T> など)実装が軽く、余計なイベントクラスを増やさなくてよい
テストコードや一時的な実験いずれのパターンでも可(簡単なほう)設計よりも素早い検証が目的なので、軽量なパターンで十分

特に業務システムでは、「とりあえず全部 INotifyPropertyChanged にする」ではなく、「このイベントはドメインとして意味があるか」「他のサービスに通知したいものか」といった観点も踏まえて設計しておくと、後から仕様変更が入ったときにコードが破綻しにくくなります。

汎用的なベースクラスを用意して楽をする

プロパティの変更検知ロジックは、どうしても同じようなコードが増えがちです。そのため、ベースクラスに共通のヘルパーメソッドを用意するのが実務ではよく行われます。特に INotifyPropertyChanged では定番です。


using System.ComponentModel;
using System.Runtime.CompilerServices;

public abstract class BindableBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        if (propertyName is null)
        {
            return;
        }

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

    // 値をセットし、変更があったときにだけ通知する共通メソッド
    protected bool SetProperty&lt;T&gt;(ref T storage, T value, [CallerMemberName] string? propertyName = null)
    {
        if (Equals(storage, value))
        {
            return false;
        }

        storage = value;
        OnPropertyChanged(propertyName);
        return true;
    }
}

このベースクラスを継承すると、プロパティはかなり簡潔に書けます。


public class PersonViewModel2 : BindableBase
{
    private int _age;
    public int Age
    {
        get =&gt; _age;
        set =&gt; SetProperty(ref _age, value);
    }

    private string? _name;
    public string? Name
    {
        get =&gt; _name;
        set =&gt; SetProperty(ref _name, value);
    }
}

このように「プロパティ変更をトリガーにイベントを発火する」という仕組みも、ベースクラス化してしまえばコピペを減らせて、人為的なミス(比較漏れ・通知漏れ)もかなり抑えることができます。

まとめ:プロパティ変更をトリガーに処理を走らせるための指針

最後に、本記事のポイントを整理します。

  • 「値の変更をトリガーに処理を走らせたい」場合、フィールドではなくプロパティの setter を入り口にするのが基本
  • 値の変更検知は if (_value == value) return; のように、本当に変わったときだけ通知する
  • 外部にも通知したいときは EventHandler<TEventArgs> ベースのイベントを使うと安全かつ標準的
  • UI バインディングが目的なら INotifyPropertyChanged を実装するのが王道
  • 小規模・内部限定なら、Action などの公開デリゲートでシンプルに済ませるのもアリ
  • イベントの購読解除漏れはメモリリークの原因になるため、どこで解除するかを設計に組み込む
  • よく使うパターンは、ベースクラスやヘルパーメソッドとして共通化しておくと実装もレビューも楽になる

これらのパターンを使い分ければ、「C# で変数(プロパティ)の変更をトリガーに処理を走らせたい」という要件は、UI・ドメイン・小規模ツールなど、さまざまな場面で過不足なく実現できます。自分が作っているアプリの性質に合わせて、適切な方式を選んでみてください。


この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次