本文へスキップ
BecomeCoder

C# MVVMコース · 第6章 ViewModel ― 画面の状態と操作 · レッスン21

TaskListViewModel ― 状態を持つ

ブラウザで完結

導入

いよいよタスク管理アプリのViewModel本体、TaskListViewModelに着手します。画面が必要とする状態は主に3つ——一覧(Tasks)・入力中のタイトル(NewTaskTitle)・残タスク数(RemainingCount)です。一覧にはObservableCollectionという特別なコレクションを使います。これはListと違い、要素の追加・削除のたびに自動で画面へ通知してくれます。

図解

classDiagram
    class ObservableCollection~TaskDto~ {
        <<List + 変更通知>>
        +Add(item)
        +CollectionChanged event
    }
    class TaskListViewModel {
        +Tasks : ObservableCollection~TaskDto~
        +NewTaskTitle : string
        +RemainingCount : int
    }
    TaskListViewModel --> ObservableCollection~TaskDto~ : Tasks

サンプル

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;

var vm = new TaskListViewModel();
vm.Tasks.CollectionChanged += (s, e) => Console.WriteLine($"一覧が変化: {e.Action}");

vm.Tasks.Add(new TaskDto(Guid.NewGuid(), "牛乳を買う", "Medium", false));
vm.Tasks.Add(new TaskDto(Guid.NewGuid(), "レポート提出", "High", true));
// 一覧が変化: Add
// 一覧が変化: Add

Console.WriteLine($"残タスク: {vm.RemainingCount}");   // 残タスク: 1(未完了は1件)

record TaskDto(Guid Id, string Title, string Priority, bool IsCompleted);

class TaskListViewModel : ViewModelBase
{
    private string _newTaskTitle = "";
    public ObservableCollection<TaskDto> Tasks { get; } = new();
    public string NewTaskTitle
    {
        get => _newTaskTitle;
        set => SetProperty(ref _newTaskTitle, value);
    }
    public int RemainingCount => Tasks.Count(t => !t.IsCompleted);   // 計算プロパティ(保存はしない)
}

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;
    }
}
  • ObservableCollection<T>Add/RemoveのたびにCollectionChangedを発火し、画面の一覧表示を自動更新する(List<T>にはこの仕組みがない)
  • RemainingCountは保存されたフィールドではなく=>計算プロパティ。呼ばれるたびにTasksから数え直す
  • NewTaskTitleは前レッスンのSetPropertyをそのまま使う(入力中のテキストボックスと双方向バインドする想定)

演習

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;

var vm = new TaskListViewModel();
int collectionChangedCount = 0;
vm.Tasks.CollectionChanged += (s, e) => collectionChangedCount++;

vm.Tasks.Add(new TaskDto(Guid.NewGuid(), "牛乳を買う", "Medium", false));
vm.Tasks.Add(new TaskDto(Guid.NewGuid(), "レポート提出", "High", true));

Assert(vm.Tasks.Count == 2, "2件追加される");
Assert(collectionChangedCount == 2, "追加のたびに通知される");
Assert(vm.RemainingCount == 1, "未完了は1件(レポート提出は完了済み)");

int propChangedCount = 0;
vm.PropertyChanged += (s, e) => propChangedCount++;
vm.NewTaskTitle = "掃除する";
Assert(vm.NewTaskTitle == "掃除する", "入力中のタイトルを保持する");
Assert(propChangedCount == 1, "NewTaskTitleの変更が通知される");

Console.WriteLine("全テスト成功");

record TaskDto(Guid Id, string Title, string Priority, bool IsCompleted);

// TODO: TaskListViewModel を完成させてください
//       ・Tasks: ObservableCollection<TaskDto>(画面に一覧表示する)
//       ・NewTaskTitle: string(SetPropertyで変更通知する)
//       ・RemainingCount: 未完了タスクの件数を返す計算プロパティ(IsCompleted == false の数)
class TaskListViewModel : ViewModelBase
{
    ___
}

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;
    }
}

void Assert(bool condition, string name)
{
    if (!condition) throw new Exception($"FAIL: {name}");
}
  • 期待される出力: 全テスト成功
ヒント1を見る

public ObservableCollection<TaskDto> Tasks { get; } = new();

ヒント2を見る

private string _newTaskTitle = ""; public string NewTaskTitle { get => _newTaskTitle; set => SetProperty(ref _newTaskTitle, value); } public int RemainingCount => Tasks.Count(t => !t.IsCompleted);

まとめ

  • ObservableCollection<T>は要素の増減を画面へ自動通知する一覧用コレクション
  • 計算プロパティ(=>)は保存せず、呼ばれるたびに再計算する
  • ViewModelの状態は「入力中の値」と「表示用の一覧」に分けて考える

次回: 一覧だけでは操作できない。画面からの「実行」を表すCommandです。

実際に動かしてみよう

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

C# — ブラウザ内で実行

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