C#でWPF学習中「INotifyPropertyChanged」

C# コンピュータ
C#

.NET SDK(dotnet.exe)をインストールしたのでWPFを初めてみました。
Visual Studio 2019などのIDEでないと厳しいかとも思いますが、dotnet.exeとVisual Studio Codeで少しずつ学習を進めています。

実行環境
Winodws10 Pro 2004
dotnet –version 5.0.102

プロジェクトの作成
mkdir <プロジェクト名>
cd <プロジェクト名>
dotnet new wpf
code .

ソースコード

ファイル名:MainWindow.xaml

<Window x:Class="WpfSample1.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:WpfSample1"
        mc:Ignorable="d"
        Title="{Binding Title}" Height="450" Width="800">
    <Grid>

    </Grid>
</Window>

Title=”{Bin…の行を書き換えています。

ファイル名:MainWindowViewModel.cs

using System.ComponentModel;

namespace WpfSample1
{
    class MainWindowViewModel : INotifyPropertyChanged
    {
        public MainWindowViewModel()
        {
            PropertyChanged += (o, e) => {};
        }




        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string name)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(name));
        }

        private string _title;

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

新規作成

ファイル名:MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfSample1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            var vm = new MainWindowViewModel();
            DataContext = vm;
            vm.Title = "タイトル";

        }
    }
}

InitializeComponent();以下3行をを追加

説明
サンプルではウィンドウのタイトルバーを書き換えてみます。
MainWindow.xamlでView見た目のレイアウトを定義しています。タイトルを書き換えるにあたり、後からC#のコードで書き換える目印として、”{Binding Title}”と記述しています。

MainWindowViewModelはViewのデータソースとなるオブジェクトのクラスです。INotifyPropertyChangedを実装することで、プロパティとViewの項目を連動(データバインド)します。C#側でプロパティを書き換えるとView側で表示も変化します。今回の例ではタイトルバーを書き換えるため、プロパティ名をTitleとしています。

MainWindowはwpfにおけるプログラムの開始位置となります。厳密にエントリーポイントとは異なるとはおもいますが、今回はMainWindowViewModelのインスタンスを作成しDataContextとしてセットしています。その後プロパティのTitleに値をセットすることでタイトルバーが変化しています。

とりあえず学習したことを要約するとこのような感じになりました。INotifyPropertyChangedについて、調べてみましたが難しくあまり理解できませんでしたが、とりあえずViewModelとしてViewとの紐づけだけを押さえて、後はおまじないだと思って記述したところプログラムは動いてくれました。

追記:20250318

INotifyPropertyChangedを継承したViewModel(MainWindowViewModel)を作成してしています。名前がIから始まるのでインターフィエスですので、継承してメソッドを実装します。

class MainWindowViewModel : System.ComponentModel.INotifyPropertyChanged
{

}

この状態だと、INotifyPropertyChanged.PropertyChangedが実装されていないとお叱りを受けます。

class MainWindowViewModel : System.ComponentModel.INotifyPropertyChanged
{
    public event System.ComponentModel.PropertyChangedEventHandler? PropertyChanged;
}

実装するとPropertyChangedがnullの可能性があると警告を受けます。器が用意されているが中身が無いので、このまま使うと不具合が起きるということですので中身を用意します。

class MainWindowViewModel : System.ComponentModel.INotifyPropertyChanged
{
    public event System.ComponentModel.PropertyChangedEventHandler? PropertyChanged;

    // コンストラクタ
    MainWindowViewModel()
    {
        PropertyChanged += (sender, e) => {};
    }
}

コンストラクタで代入することによりnullではない状態になり、警告が消えました。とりあえず最低限の形にはなったので、この状態で中身を見てみます。

PropertyChangedですがeventとありますのでデリゲートをセットしています。デリゲートはメソッドの参照を保持する型で、eventの組み合わせではイベントが発生時呼び出されるメソッドを定義することが出来ます。

では呼び出されるメソッドを見てみます。引数として(senver, e)とありますがsendaerは呼び出し元のオブジェクトを指し方はobject?です。eはイベント発生時の情報を含む引数で、PropertyChangedEventArgs型です。そして処理内容ですが{}ですので何もしていません。イベントを登録はしていますが何もしないという状態になります。

とりあえずエラーや警告を消すことだけを念頭においたコーディングです。逆に言えばこれからの部分が本番に成ります。

次にOnPropertyChangedになります。文字列のnameを引数にとるメソッドです。

        protected virtual void OnPropertyChanged(string name)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(name));
        }

メソッド内では、先ほど定義したPropertyChanged()を呼び出しています。
引数object?にはthisをPropertyChangedEventArgsはnameでオブジェクトを生成しています。

OnPropertyChangedというメソッド名から、クラス内のプロパティが変更されたら呼び出すメソッドだと思われます。
そうすることでPropertyChangedに登録したデリゲートが呼び出されることに成ります。

ただデリゲートで定義したメソッドは何も処理をしません。ということはプログラマではなくシステム側が然るべきデリゲートをセットして呼び出す仕組みだと考えられます。

ではプロパティの実装部分を見てみます。

        private string _title;

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

_titleというメンバ変数をTitleという名前のプロパティとして公開している、スタンダードなプロパティののコードだと思います。違う点としてはset内でOnPropertyChanged("Title");を呼び出している部分です。確かにsetすることでTitleプロパティの内容が変更されますので、それをOnPropertyChanged()を介してイベントを通知しています。

PropertyChangedから後の通知される先は、このコードからは読み取ることは出来ませんが、多分バインド先のxamlから生成されたviewのコード内に通知されるはずです。

とりあえずViewのTitleとViewModelのTitleを繋ぐ(バインディング)だけであればこれで動作します。

今回中身を書いていないPropertyChangedでTitleが変更された場合の処理(TitleをSubscribeする)を書けば、Titleの変更した場合の共通の処理を記述することが出来ます。View側から変更する場合でもViewModelで変更した場合でも機能する機能するはずです。


多分デザインパターンのオブザーバーパターンだと思うのですが、オブザーバーパターンの具体的な実装が頭の中に定着しておらず、紐づいていない状態です。(学習がたりない)そのあたりも絡めて再学習したいと思います。

コメント