Uncategorized

【C#/WPF実践入門編(6)】データバインディングの基礎②~INotifyPropertyChangedとは?~

Windows Presentation Foundation (WPF) のデータバインディングにおいて、データ変更時にUIを自動的に更新する仕組みについて学びます。

前回は初回同期のみ対応しましたが、今回はプロパティ変更時にUIを更新する方法を習得します。

以下の方に役立つ内容となっています。

  • データ変更時にUIを自動更新したい方
  • INotifyPropertyChangedインターフェースの使い方を知りたい方
  • WPFのOneWayバインディングを完全に理解したい方

プロパティの値が変更されたときに、UIに自動的に反映させるにはINotifyPropertyChangedインターフェースの実装が必要です。

この記事では、最も基本的な実装から、実用的な共通化手法まで段階的に学習します。

演習では、前回作成したPersonクラスを拡張し、年齢の増減ボタンで年齢値のUIが更新されるアプリを作成します。

前回の記事からみてもらえると、データバインディングについてより理解が深まるかと思います。

【C#/WPF実践入門編(5)】データバインディングの基礎①~DataContextとは?~ Windows Presentation Foundation (WPF) の特徴的な機能の一つであるデータバインディングについて学び...
プロ太

WPFの特徴的な機能である「データバインディング」について段階的に学んでいきましょう!

MVVMパターンでも必須となる重要な仕組みです。

演習のコード一式はGitHubに置いてあります。

講義:INotifyPropertyChangedによる動的UI更新

前回の課題:プロパティ変更が反映されない

前回作成したアプリでは、初回のデータ表示は正しく動作しますが、アプリ実行後にプロパティを変更してもUIには反映されません。

// 前回のPersonクラス
public class Person
{
    public string Name { get; set; } // この値を変更してもUIは更新されない
    public int Age { get; set; }  
    public string Job { get; set; }
}

...

//例えば、ボタンを押されたときにpersonを書き換えてもUIへ反映されない
private void OnButtonClick(...){
   person.Name = "新しい名前";  //画面へ自動では反映されない
}

これは、WPFフレームワークがプロパティの変更を検知できないためです。

INotifyPropertyChanged

INotifyPropertyChangedとは?

INotifyPropertyChangedは、.NETで提供されているインターフェースで、プロパティの変更をUIフレームワークに通知するための仕組みです。

public interface INotifyPropertyChanged
{
    event PropertyChangedEventHandler? PropertyChanged;
}

このインターフェースを実装すると、プロパティ変更時にPropertyChangedイベントを発火させることで、WPFのデータバインディング機能が自動的にUI更新を行います。

プロ太

データバインディングの「OneWay」や「TwoWay」で自動更新を実現するには、このインターフェースの実装が必須です。

基本的な実装方法

最もシンプルな実装方法から見てみましょう。Personクラスを例に、基本的な実装を示します。

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

    public string Name
    {
        get => _name;
        set
        {
            _name = value;
            // プロパティ変更を直接通知
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Name"));
        }
    }

    public int Age
    {
        get => _age;
        set
        {
            _age = value;
            // プロパティ変更を直接通知
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Age"));
        }
    }

    public string Job
    {
      get => _job;
        set
        {
            _job = value;
            // プロパティ変更を直接通知
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Job"));
        }
    }

    // INotifyPropertyChangedの必須イベント
    public event PropertyChangedEventHandler? PropertyChanged;
}

この実装により、`person.Age = 35;`のような変更が即座にUIに反映されるようになります。

以下のようなイメージです。①でDataContextを設定すると、「②購読開始、③変更通知」が自動で行われ、「データ→UI」の自動同期処理を実現します。

WPFフレームワーク側で自動で、「PropertyChanged」において、DataContextで紐づけられたUI側のコントロールが購読を行うよう設定されます。(図の②)

プロパティ変更時に「PropertyChanged?.Invoke」で呼び出され、紐づけられたコントロールが「あ、プロパティ変更あったので、UIも更新しよう!」となります。(図の③)

ポイントは以下です。

  • プライベートフィールド:実際の値を保持(_nameなど)
  • プロパティのget/set:フィールドへのアクセサとイベント発火
  • PropertyChanged?.Invoke:プロパティ名を指定して変更通知
  • 文字列での指定:`”Name”のように文字列でプロパティ名を指定

保守性向上のための手法

上記の基本実装では、プロパティが増えるたびに同じようなコードを書く必要があります。実用的な開発では、以下の手法を使います。

1. nameofキーワードを使用した改善

文字列の直接指定はタイプミスやリファクタリング時の問題を引き起こします。nameofを使うと安全性が向上します。

// nameofでコンパイル時安全性を確保
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Age)));

2. 共通メソッドによる更なる簡略化

using System.Runtime.CompilerServices; // CallerMemberName用
...

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

    public string Name
    {
        get => _name;
        set
        {
            _name = value;
            OnPropertyChanged(); // プロパティ名の指定不要!
        }
    }

    public int Age
    {
        get => _age;
        set
        {
            _age = value;
            OnPropertyChanged(); // プロパティ名の指定不要!
        }
    }

     public string Job
     {
        get => _job;
        set
        {
            _job = value;
            OnPropertyChanged(); // プロパティ変更を通知
        }
      }      

    public event PropertyChangedEventHandler? PropertyChanged;

    // CallerMemberName属性を使った通知メソッド
    private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

CallerMemberName属性により、コンパイラが自動的に呼び出し元のメンバー名(この場合はプロパティ名)を挿入してくれます。

プロ太

この方法は、コードが簡潔で安全性も高いためよく使われます。

WPFのサンプルコードとしてもよく見かけますね。

演習:年齢増減ボタンでデータとUIを更新

作成するアプリの概要

前回のアプリを拡張して、以下の機能を追加します。

  • 年齢の「-」「+」ボタンを追加し、ボタンクリックで年齢を増減
  • 年齢変更がリアルタイムでUIへ反映

土台となる前回の演習・作ったアプリについては以下の記事とコードを参考にしてください。

【C#/WPF実践入門編(5)】データバインディングの基礎①~DataContextとは?~ Windows Presentation Foundation (WPF) の特徴的な機能の一つであるデータバインディングについて学び...

以下の手順で作成します。

  • 手順1:画面にボタンを追加(XAML)
  • 手順2:INotifyPropertyChangedとイベント実装(C#コードビハインド)
プロ太

CallerMemberName属性」を使った方法で、コードの共通化をしながら実装します!

手順1:画面にボタンを追加(XAML)

前回のプロジェクトのMainWindow.xamlの「人物情報」部分を以下のように修正し、年齢の「-」「+」ボタンを追加します。

...
        <!-- 人物情報エリア -->
        <GroupBox Header="人物情報" Margin="0,0,0,10">
            <StackPanel Margin="5">
                <TextBlock Text="{Binding Name, StringFormat='名前: {0}'}"/>
                <StackPanel Orientation="Horizontal" Margin="0,10,0,0">
                    <TextBlock Text="{Binding Age, StringFormat='年齢: {0}'}"/>
                    <Button Content="-" Width="20" Height="20" Margin="10,0,5,0" Click="OnDecrementAge"/>
                    <Button Content="+" Width="20" Height="20" Margin="5,0,0,0" Click="OnIncrementAge"/>
                </StackPanel>
                <TextBlock Text="{Binding Job, StringFormat='職業: {0}'}"/>
            </StackPanel>
        </GroupBox>
...

以下のようにプレビュー表示されます。

ポイントは以下です。

  • Horizontal StackPanel:年齢表示とボタンを横並びに配置
  • 増減ボタン:「-」「+」ボタンを追加し、Clickイベントハンドラを設定
  • 適切なMargin:ボタンの配置を調整

手順2:INotifyPropertyChangedとイベント実装(C#コードビハインド)

MainWindow.xaml.csを以下のように修正します。

...

    public partial class MainWindow : Window
    {
        private readonly Person person; // フィールドに変更(後でアクセスするため)

        public MainWindow()
        {
            InitializeComponent();

            // データオブジェクトを作成
            person = new Person
            {
                Name = "田中太郎",
                Age = 30,
                Job = "ソフトウェアエンジニア"
            };

            var company = new Company
            {
                Name = "株式会社サンプル",
                Department = "開発部"
            };

            // DataContextを設定
            this.DataContext = person;
            CompanyGroup.DataContext = company;
        }

        // ボタンクリックのイベントハンドラー(次の手順で実装)
        private void OnDecrementAge(object sender, RoutedEventArgs e)
        {
            // 年齢を1減らす
            person.Age--;
        }

        private void OnIncrementAge(object sender, RoutedEventArgs e)
        {
            // 年齢を1増やす
            person.Age++;
        }
    }

    // INotifyPropertyChangedを実装したPersonクラス
    public class Person : INotifyPropertyChanged
    {
        private string _name = "";
        private int _age;
        private string _job = "";

        public string Name
        {
            get => _name;
            set
            {
                _name = value;
                OnPropertyChanged(); // プロパティ変更を通知
            }
        }

        public int Age
        {
            get => _age;
            set
            {
                _age = value;
                OnPropertyChanged(); // プロパティ変更を通知
            }
        }

        public string Job
        {
            get => _job;
            set
            {
                _job = value;
                OnPropertyChanged(); // プロパティ変更を通知
            }
        }

        public event PropertyChangedEventHandler? PropertyChanged;

        // CallerMemberName属性を使った通知メソッド
        private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    // Companyクラスは変更なし
    public class Company
    {
        public required string Name { get; set; }
        public required string Department { get; set; }
    }
}

実装のポイントは以下です。

  • Person変数をフィールド化:ボタンクリックハンドラからアクセスするためprivate readonlyフィールドに変更
  • INotifyPropertyChanged実装:CallerMemberName属性を使った方法を採用
  • プロパティを明示的に実装:プライベートフィールドとget/setを明示的に実装(Ageだけではなく、Name、Jobについても実装)
  • Companyクラスは変更なし:会社情報は変更しないため、従来の自動プロパティのまま

アプリを実行

アプリをデバッグ実行し、「-」「+」ボタンをクリックしてみましょう。

年齢の値が即座に画面に反映されることが確認できます!

プロ美

ボタンをクリックするたびに、年齢がリアルタイムで変わっている!これがINotifyPropertyChangedの効果だね。

プロ太

今回の演習で「単方向同期」(データ→UI)については、ばっちりですね!

プロ美

名前とか、職業とかもユーザ入力と同期して変数値が変わるようにできないかな?

プロ太

それについては、次回の「双方向同期」(データ↔UI)で扱うので、楽しみにしていてください!

まとめ

本記事では、WPFにおける単方向同期(データ→UI)のデータバインディングの実装について学びました。

INotifyPropertyChangedを使うことで、データの値が変わったときにUIも自動で更新されます。

また、CallerMemberName属性を使うことで、プロパティ変更の通知処理を一度実装すれば、どのプロパティでも同じ仕組みを使い回せます。

年齢増減ボタンの演習を通じて、INotifyPropertyChangedやCallerMemberName属性を実際に使ったアプリを作りました。

このようなデータバインディングの仕組みは、「UIとデータの分離」を行うMVVMパターン実現においても必須となります。

次回は双方向同期(データ↔UI)について解説予定です。

プロ太

引き続き、一緒にWPFアプリ開発について学んでいきましょう!

ABOUT ME
プロ太
●仕事:現在は個人事業主(メンター・情報発信等)、大手IT企業で技術者・マネージャ(15年以上)、大学の外部講師、学生時代は学習塾で非常勤講師(約4年間) ●博士(工学)の学位取得 ●高校生の頃に独学で始め、プログラミング歴20年以上 ●言語:C# 、Java、C/C++、Python、JavaScript/TypeScript等 ●分野:Webアプリ、テスト自動化、生成AI、デバッガ、コード解析、ドメイン特化言語

ご依頼・ご相談について

プログラミング学習のご相談、お仕事のご依頼については、
こちらのお問い合わせページをご確認ください。