本文へスキップ
BecomeCoder

C#デザインパターンコース · 第3章 現代の設計パターン · レッスン24

データバインディングの実践 ― ViewModelBase・ViewModel・View に分ける

ブラウザで完結

導入

前回、ViewModel が INotifyPropertyChanged を実装して変更を通知するしくみを見ました。でもプロパティが増えるたびに set の中で PropertyChanged?.Invoke(...) を書くのは面倒で、書き忘れも起きます。実際のアプリでは、この通知の共通処理を基底クラス ViewModelBase にまとめ、各画面の ViewModel はそれを継承します。ここでは ViewModelBase・ViewModel・View の3つに分けて、データバインディングの現実的な形を作ります。

図解

classDiagram
    class ViewModelBase {
        <<abstract>>
        +PropertyChanged
        #OnPropertyChanged(name)
        #SetProperty(field, value) bool
    }
    class UserViewModel {
        +Name
        +Age
    }
    class View {
        購読して表示を更新
    }
    ViewModelBase <|-- UserViewModel : 継承
    View --> UserViewModel : バインド(購読)

説明

役割を3つに分けます。

  • ViewModelBase(土台):INotifyPropertyChanged を実装し、通知の共通処理 OnPropertyChangedSetProperty を提供します。[CallerMemberName] を使うと、呼び出し元のプロパティ名が自動で入るので nameof(...) すら書かずに済みます。
  • ViewModel(画面用の状態):ViewModelBase を継承し、画面固有のプロパティを SetProperty で書くだけ。通知のことは基底に任せます。
  • View(画面):ViewModel を購読し、変更が来たら表示を更新します。実アプリでは XAML の {Binding} がこの購読・更新を自動でやってくれます。

ViewModelBase(ViewModelBase.cs ― 通知の共通処理をまとめた土台です。

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

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

    // 呼び出し元のプロパティ名が [CallerMemberName] で自動的に入る
    protected void OnPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));

    // 値が変わったときだけ field を更新し、変更を通知する共通ヘルパー
    protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? name = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(name);
        return true;
    }
}

ViewModel(UserViewModel.cs ― 基底を継承し、SetProperty を呼ぶだけです。

class UserViewModel : ViewModelBase
{
    private string _name = "";
    public string Name
    {
        get => _name;
        set => SetProperty(ref _name, value);   // 変わったら自動で通知
    }

    private int _age;
    public int Age
    {
        get => _age;
        set => SetProperty(ref _age, value);
    }
}

View(画面のかわり/Program.cs ― ViewModel を購読し、変化を受け取って表示します。

var vm = new UserViewModel();
// 実アプリの XAML {Binding} の代わりに、変更通知を購読して表示を更新する
vm.PropertyChanged += (_, e) => Console.WriteLine($"表示更新: {e.PropertyName}");

vm.Name = "田中";   // 表示更新: Name
vm.Age = 20;        // 表示更新: Age
vm.Age = 20;        // 同じ値なので通知は出ない(SetProperty が弾く)
  • 通知の共通処理を ViewModelBase に集約すると、各 ViewModel は SetProperty(ref _x, value) だけで済む
  • [CallerMemberName] により、通知に乗るプロパティ名は自動で入る(nameof(Name) を書かなくてよい)
  • SetProperty は「値が変わったときだけ」通知するので、同じ値の再代入では画面を無駄に更新しない
  • 下のエディタで動かすときは、using を先頭にまとめ、View(トップレベル文)→ ViewModelBase → ViewModel の順に並べて貼り付けます(C#はトップレベル文をクラス定義より前・using をすべての先頭に置く決まりのため)。下の「演習」がそのまとめた形です

やってみよう

UserViewModelIsAdultAge >= 18 を返す読み取り専用プロパティ)を足し、AgesetSetProperty が変化したときだけ OnPropertyChanged(nameof(IsAdult)) も呼んでみましょう。1回の代入で複数の表示更新が飛ぶ様子を確認できます。

演習

ViewModelBase を継承した CounterViewModel を作り、Count プロパティを SetProperty で実装してください。

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

var vm = new CounterViewModel();
vm.PropertyChanged += (_, e) => Console.WriteLine($"changed: {e.PropertyName}");
vm.Count = 1;   // changed: Count

abstract class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? name = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value; OnPropertyChanged(name); return true;
    }
}

class CounterViewModel : ViewModelBase
{
    private int _count;
    public int Count
    {
        get => _count;
        // TODO: SetProperty を使って _count を更新してください
        set => ___;
    }
}
  • 期待される出力: changed: Count
ヒント1を見る

set => SetProperty(ref _count, value);

ヒント2を見る

SetProperty がプロパティ名(Count)を通知に自動で載せるので、nameof を書く必要はありません

まとめ

  • ViewModelBase に「変更通知の共通処理(OnPropertyChangedSetProperty)」を集約する
  • 各 ViewModel は基底を継承し、SetProperty(ref _field, value) だけで通知つきプロパティになる
  • View は ViewModel を購読して表示を更新する(実アプリの {Binding} の役割)
  • [CallerMemberName] でプロパティ名は自動、SetProperty は無駄な通知を防ぐ

次回: 読み取りと書き込みを分ける CQRS です。

実際に動かしてみよう

本文のサンプルや演習のコードは、コードブロック右上の「コピー」ボタンでコピーして、下のエディタに貼り付ければそのまま実行できます。

C# — ブラウザ内で実行

ブラウザ内でC#を動かす環境を読み込みます(初回のみ数秒)。
スクロールして表示された時点でも自動で読み込まれます。