問題意識
ViewModelからViewへの値の反映を考えます。具体的には、ViewModelオブジェクトが、特定のタイミングで、ある変数に格納している値をViewに同期させるにはどうすればいいのか。これを考えます。例えば、ボタンAを押したらそのボタンを無効化し、ボタンBを押したら有効化する。このような挙動の実現を問題とします。
また、本記事でのタイトルでも明示していますが、原則、Microsoftの公式から手に入る情報の解釈で挙動の実現を探っています。
問題のView
MainWindow.xaml
<Window x:Class="WPF_MVVM_20231223.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:WPF_MVVM_20231223"
mc:Ignorable="d"
Title="MainWindow" Height="150" Width="300">
<StackPanel Orientation="Horizontal">
<Button Content="Start" IsEnabled="{Binding IsStartEnabled}" Padding="48 0 48 0" Margin="12 24 12 24"/>
<Button Content="Stop" Padding="48 0 48 0" Margin="12 24 12 24"/>
</StackPanel>
</Window>
問題のViewModel
MainWindowViewModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WPF_MVVM_20231223
{
internal class MainWindowViewModel
{
private bool _isStartEnabled = true;
public bool IsStartEnabled
{
get { return _isStartEnabled; }
set { _isStartEnabled = value; }
}
}
}
調査
色々なサイトを調べていると、どうやらINotifyPropertyChanged
インターフェースを実装することが重要なようです。ここでは以下のページから、ViewからViewmModelへ値の変更について調査することとします。
説明の解釈
バインディング ターゲットのプロパティにバインディング ソースの動的変更が自動的に反映される (たとえば、ユーザーがフォームを編集するとプレビュー ペインが自動的に更新される) ようにするために、OneWay または TwoWay バインディングをサポートするには、クラスが適切なプロパティ変更通知を提供する必要があります。 この例では、INotifyPropertyChanged を実装するクラスを作成する方法を示します。
値を反映するためにはバインディングをサポートする必要があり、そのために「適切なプロパティ変更通知」が必要なようです。そして、「適切なプロパティ変更通知」のひとつの方法として、InotifyPRropertyChanged
インターフェースの利用があるようです。
INotifyPropertyChanged
を実装するには、PropertyChanged
イベントを宣言し、OnPropertyChanged
メソッドを作成する必要があります。 次に、変更を通知する必要のある各プロパティについて、そのプロパティが更新されるたびにOnPropertyChanged
を呼び出します。
続いてこれを一読すると、どうやらインターフェース利用の詳細について書かれていることが推察できます。そして、その詳細は、具体的には以下の手順に分解することができそうです。
-
PropertyChanged
イベントの宣言 -
OnPropertyChanged
メソッドの作成 -
OnPropertyChanged
メソッドの呼び出し
では、この手順について掘り下げていきましょう。
手順について
1. PropertyChanged
イベントの宣言
ソースコードでは、以下のように記されています。
public event PropertyChangedEventHandler PropertyChanged;
PropertyChangedEventHandler型でPropertyChangedという名前のイベント、これを宣言すればいいということが分かりました。
2. OnPropertyChanged
メソッドの作成
調査ページでは、ソースコードの以下の部分にメソッド作成がされていました。
// Create the OnPropertyChanged method to raise the event
// The calling member's name will be used as the parameter.
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
コメント文の翻訳
イベントを上げるためのOnPropertyChangedメソッドを作成
呼び出しメンバの名前が引数として使われるでしょう
メソッドの中身では、PropertyChanged
イベントのInvoke
メソッド(?)が実行されているようです。Invoke
メソッド(?)とは何か。このイベントはPropertyChangedEventHandler
型ですから、この型について調べてみます。
コンポーネントでプロパティが変更されたときに発生する
PropertyChanged
イベントを処理するメソッドを表します
どうやら、この型はメソッドを表すようです。どういうことでしょうか。さらにソースコードを調べてみます。
public delegate void PropertyChangedEventHandler(object? sender, PropertyChangedEventArgs e);
これを見るに、delegate
というキーワードを用いてこの型(=メソッド)は宣言されているようです。
ここまでの説明をまとめると、PropertyChangedEventHandler
型は、型であると同時に、メソッドでもあるということです。また、delegate
キーワードを用いて宣言されています。
delegateについて
今のところ、メソッドからInvoke
メソッド(?)が呼び出されているという、奇妙な状況に思えてしまいます。この違和感を解決する手がかりがないか、さらに読み進めましょう。注釈を見てみます。
PropertyChangedEventHandler
デリゲートを作成する場合は、イベントを処理するメソッドを指定します。
ここで、「PropertyChangedEventHandler
デリゲート」と呼ばれていることに注意してください。この呼び方をしているということは、delegate
キーワードを用いて宣言されたこのPropertyChangedEventHandler
型は、型であると同時に、(メソッドでもあり、しかも)デリゲートだと解釈できます。PropertyChangedEventHandler
はデリゲートなのです。
イベント ハンドラー デリゲートの詳細については、「イベントの 処理と発生」を参照してください。
そしてこのPropertyChangedEventHandler
含むデリゲート全般の情報については、さらに別のページから調査できるようです。以下のページから調査を続けます。
デリゲートの詳細については、「Delegate クラス」を参照してください。
このページの読解については省略します。記事を読み進めた結果、最も大事な情報が参照先にあることを発見したためです。具体的には、このページに、Invoke
に関する直接的な記述がありました。「注釈」という見出しの「注意」を読んでみます。
共通言語ランタイムは、デリゲート型ごとにメソッドを Invoke 提供し、デリゲートと同じシグネチャを持ちます。 このメソッドは、コンパイラによって自動的に呼び出されるため、C#、Visual Basic、または Visual C++ から明示的に呼び出す必要はありません。 メソッドは Invoke 、デリゲート型のシグネチャを見つける場合に リフレクション に役立ちます。
日本語としておかしい気がします。英語で読みます。
The common language runtime provides an Invoke method for each delegate type, with the same signature as the delegate. You do not have to call this method explicitly from C#, Visual Basic, or Visual C++, because the compilers call it automatically. The Invoke method is useful in reflection when you want to find the signature of the delegate type.
ちょっと長いので、DeepLに翻訳してもらいました。
共通言語ランタイムは、デリゲート型ごとに、デリゲートと同じシグネチャを持つ Invokeメソッドを提供する。このメソッドはコンパイラが自動的に呼び出すので、C#、Visual Basic、Visual C++から明示的に呼び出す必要はありません。Invokeメソッドは、リフレクションでデリゲート型のシグネチャを見つけたいときに便利です。
一番大事なことは、デリゲートという型が、自分自身と同じ(シグネチャの)Invoke
メソッドを備えているということです。
OnPropertyChangedメソッドの作成とはどういうことだったのか
この話を、PropertyChangedEventHandler
の話に戻して考えてみましょう。まず、この型は、デリゲートであり、メソッドでもある。そして、デリゲートであるためにInvoke
メソッドを備えており、自分自身と同じ(シグネチャの)メソッドをInvoke
メソッドを通して、いわば再帰的に呼び出せるわけです。
さらに、「OmPropertyChangedメソッドの作成」というもともとの手順に立ち返って考えてみましょう。
// Create the OnPropertyChanged method to raise the event
// The calling member's name will be used as the parameter.
protected void OnPropertyChanged([CallerMemberName] string name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
これがもともとのソースコードです。ここでは、OnPropertyChanged
メソッドの作成がされていましたね。そして、このメソッドには以下の特徴があります。
- メソッドを呼び出したメンバの名前が引数になる。
- メソッドの処理内容としては、
PropertyChangedEventHandlar
というイベントが、自分自身を実行する。 - イベントが自分自身を実行するときに、イベントの引数として「呼び出し元のメンバの名前」が与えられている。
では、これらの特徴がどのような意味を持つのでしょうか。「呼び出し」について考えることで、特徴がどう機能するかをさらに考えてみましょう。
3. OnPropertyChanged
メソッドの呼び出し
本メソッドは、ソースコード上では、以下のように呼び出されています。
public string PersonName
{
get { return name; }
set
{
name = value;
// Call OnPropertyChanged whenever the property is updated
OnPropertyChanged();
}
}
つまり、PresonName
プロパティへのsetアクセサが呼び出されるたびに、PersonName
プロパティがOnPropertyChanged
メソッドを呼び出しています。先ほど調べた「メソッドの特徴」を転用し、「メソッドを呼び出したときの特徴的な挙動」について書き出してみます。
- このメソッドを呼び出したメンバの名前が引数になる。
- 常に"PersonName"という文字列が引数になる。
- メソッドの処理内容としては、
PropertyChangedEventHandlar
というイベントが、自分自身を実行する。- よくわからない。
- イベントが自分自身を実行するときに、イベントの引数として呼び出し元のメンバの名前が与えられている。
- イベントが自分自身を実行するときにも常に"PersonName"という文字列が与えられて実行されている
とにかく"PersonName"という文字列が与えられて、イベントが実行されていることが分かります。
調査のまとめ
"PersonName"という文字列が与えられて、イベントが実行される。ここまでの一連の処理が、「適切なプロパティ変更通知」の方法であり、「値の反映」の方法ということになります。
こんな風にまとめると、何となく察することができますが、おそらく、PropertyChangedEventHandlar
というイベントが実行されると、与えた文字列から名前の一致するプロパティが参照され、プロパティに結びついてる(=バインディングされている)Viewの表示も変更されるのでしょう。
実装
このような仮説に基づいて、実装を考えてみます。調査を経て、もう一度実装を加えるソースコードを記載しておきます。
問題のView
MainWindow.xaml
<Window x:Class="WPF_MVVM_20231223.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:WPF_MVVM_20231223"
mc:Ignorable="d"
Title="MainWindow" Height="150" Width="300">
<StackPanel Orientation="Horizontal">
<Button Content="Start" IsEnabled="{Binding IsStartEnabled}" Padding="48 0 48 0" Margin="12 24 12 24"/>
<Button Content="Stop" Padding="48 0 48 0" Margin="12 24 12 24"/>
</StackPanel>
</Window>
問題のViewModel
MainWindowViewModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WPF_MVVM_20231223
{
internal class MainWindowViewModel
{
private bool _isStartEnabled = true;
public bool IsStartEnabled
{
get { return _isStartEnabled; }
set { _isStartEnabled = value; }
}
}
}
記事の一番最初では「ボタンAを押したらそのボタンを無効化し、ボタンBを押したら有効化する。」という挙動を想定していました。ではまず、Viewの有効/無効にViewModelの有効/無効が反映されるようにしましょう。
値の反映を実装する
調査時に明らかになった手順と同じ手順で実装をします。
-
PropertyChanged
イベントの宣言 -
OnPropertyChanged
メソッドの作成 -
OnPropertyChanged
メソッドの呼び出し
MainWindowViewModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace WPF_MVVM_20231223
{
internal class MainWindowViewModel : INotifyPropertyChanged
{
private bool _isStartEnabled = true;
public event PropertyChangedEventHandler PropertyChanged;
public bool IsStartEnabled
{
get { return _isStartEnabled; }
set
{
_isStartEnabled = value;
OnPropertyChanged();
}
}
protected void OnPropertyChanged([CallerMemberName] string name = null )
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
}
手順を実行する際には、以下のことに注意してください。
-
System.ComponentModel
名前空間及びSystem.Runtime.CompilerServices
名前空間を利用する - ViewModelに
INotifyPropertyChanged
インターフェースを継承(実装)させる
「ボタンが押されたら」を実装する
この時点で「setアクセサが呼び出されたら値を反映する」というところまでの実装が完了しています。ここからさらに、「ボタンが押されたらsetアクセサを呼び出す」処理の実装を行います。
- View側では、ButtonのCommand属性にViewModelのプロパティをバインディングする
- ViewModel側では、ICommandインターフェースを実装したものをプロパティとする
以上が実装の概略です。ViewModel→Viewへの値の反映という趣旨とずれるので詳しい説明は省略します。Command属性に割り当てたプロパティが、ボタンの押下時に実行されるという認識でよいです。結果、以下3つのコードが出来上がります。
MainWindow.xaml
<Window x:Class="WPF_MVVM_20231223.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:WPF_MVVM_20231223"
mc:Ignorable="d"
Title="MainWindow" Height="150" Width="300">
<StackPanel Orientation="Horizontal">
<Button Content="Start" IsEnabled="{Binding IsStartEnabled}" Command="{Binding StartCommand}" Padding="48 0 48 0" Margin="12 24 12 24"/>
<Button Content="Stop" Command="{Binding StopCommand}" Padding="48 0 48 0" Margin="12 24 12 24"/>
</StackPanel>
</Window>
MainWindowViewModel.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace WPF_MVVM_20231223
{
internal class MainWindowViewModel : INotifyPropertyChanged
{
private bool _isStartEnabled = true;
public event PropertyChangedEventHandler PropertyChanged;
public bool IsStartEnabled
{
get { return _isStartEnabled; }
set
{
_isStartEnabled = value;
OnPropertyChanged();
}
}
private RelayCommand? _startCommand;
private RelayCommand? _stopCommand;
public RelayCommand StartCommand
{
get
{
if( _startCommand == null)
{
_startCommand = new RelayCommand( () => this.IsStartEnabled = false);
}
return _startCommand;
}
}
public RelayCommand StopCommand
{
get
{
if (_stopCommand == null)
{
_stopCommand = new RelayCommand(() => this.IsStartEnabled = true);
}
return _stopCommand;
}
}
protected void OnPropertyChanged([CallerMemberName] string name = null )
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
}
RelayCommand.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata.Ecma335;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace WPF_MVVM_20231223
{
class RelayCommand : ICommand
{
private Action _execute;
private Func<bool> _canExecute;
public RelayCommand(Action execute)
:this(execute, null)
{
}
public RelayCommand(Action execute, Func<bool> canExecute)
{
this._execute = execute;
this._canExecute = canExecute;
}
#region 継承部分
public bool CanExecute(object parameter)
{
return _canExecute == null ? true : this._canExecute();
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
this._execute();
}
public void RaiseCanExecuteChanged()
{
this.CanExecuteChanged.Invoke(this, EventArgs.Empty);
}
#endregion
}
}
実装時は、以下の点に注意してください。
-
ICommand
インターフェースを実装したクラス(RelayCommand)を作成する - その際、
System.Windows.Input
名前空間を利用しておく -
ICommand
インターフェース実装時、Execute
,CanExecute
,RaiseCanExecuteChanged
メソッドをオーバーライドする - また、
CanExecuteChanged
イベントもオーバーライドする - 処理を移譲してもらうという意味では、コンストラクタとプライベートメンバに対応するメソッドを用意することで、起動時に処理を引き渡すことができる。
おわりに
以上で、想定した挙動が実現されることを確認できました。
公式ドキュメントを読み、実装に落とし込んで確かめてみることが、解釈を進めるうえで役に立つと感じました。実装時に起こる挙動が正解そのものだからなのでしょう。