LoginSignup
2
2

Microsoftの公式ドキュメントから調べる、ViewModelからViewへの値の反映

Last updated at Posted at 2023-12-24

問題意識

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を呼び出します。

続いてこれを一読すると、どうやらインターフェース利用の詳細について書かれていることが推察できます。そして、その詳細は、具体的には以下の手順に分解することができそうです。

  1. PropertyChangedイベントの宣言
  2. OnPropertyChangedメソッドの作成
  3. 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の有効/無効が反映されるようにしましょう。

値の反映を実装する

調査時に明らかになった手順と同じ手順で実装をします。

  1. PropertyChangedイベントの宣言
  2. OnPropertyChangedメソッドの作成
  3. 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アクセサを呼び出す」処理の実装を行います。

  1. View側では、ButtonのCommand属性にViewModelのプロパティをバインディングする
  2. 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イベントもオーバーライドする
  • 処理を移譲してもらうという意味では、コンストラクタとプライベートメンバに対応するメソッドを用意することで、起動時に処理を引き渡すことができる。

おわりに

以上で、想定した挙動が実現されることを確認できました。

公式ドキュメントを読み、実装に落とし込んで確かめてみることが、解釈を進めるうえで役に立つと感じました。実装時に起こる挙動が正解そのものだからなのでしょう。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2