導入
いよいよタスク管理アプリの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です。