導入
もう1つ、よくある要望「タスクを仕事/プライベートのようなカテゴリで分けたい」を追加してみましょう。これも進め方は同じです。「あるカテゴリのタスクだけを取り出す」という判断は第5章で学んだ通りServiceの仕事、ViewModelとViewは、その結果を表示するだけにとどめます。
説明
flowchart LR
SVC["TaskService.GetByCategory(category)<br/>(Where で絞り込み・csharp-runnerでTDD)"] --> DTO["TaskDto.Category : string"]
DTO --> VM["TaskListViewModel<br/>WorkCount => Tasks.Count(t => t.Category == 仕事)"]
VM --> V["View(mvvm-runner)"]
<Window Title="タスク一覧(カテゴリ分け)" Width="380" Height="380">
<StackPanel Margin="16">
<TextBlock Text="仕事のタスク数: " FontWeight="Bold" />
<TextBlock Text="{Binding WorkCount}" FontSize="20" Foreground="Teal" />
<TextBox Text="{Binding NewTaskTitle, Mode=TwoWay}" Margin="0,10" />
<TextBox Text="{Binding NewTaskCategory, Mode=TwoWay}" Margin="0,4" />
<Button Content="追加" Command="{Binding AddCommand}" Width="80" />
<ListBox ItemsSource="{Binding Tasks}" DisplayMemberPath="Title" Height="120" Margin="0,10" />
</StackPanel>
</Window>
public record TaskDto(string Title, string Category);
public class TaskListViewModel
{
public string NewTaskTitle { get; set; } = "";
public string NewTaskCategory { get; set; } = "仕事";
public ObservableCollection<TaskDto> Tasks { get; set; } = new();
public int WorkCount => Tasks.Count(t => t.Category == "仕事");
public ICommand AddCommand => new RelayCommand(
() => { Tasks.Add(new TaskDto($"[{NewTaskCategory}] {NewTaskTitle}", NewTaskCategory)); NewTaskTitle = ""; },
() => !string.IsNullOrWhiteSpace(NewTaskTitle)
);
}
NewTaskCategory用のTextBoxをもう1つ足すだけで、カテゴリを入力できるようにする(レッスン27と同じMode=TwoWay)WorkCount => Tasks.Count(t => t.Category == "仕事")は、文字列の一致(==)を条件にした.Count(x => 条件)。これまでの!t.IsCompletedのような真偽値条件だけでなく、値の比較も同じ形で書ける- 一覧の各行に
[仕事]のようにカテゴリを含めて表示しているのは、DisplayMemberPathが1つのプロパティしか表示できない(複数プロパティの組み合わせ表示は非対応)という、このランナーの制約に合わせた簡略化。実務ではDataTemplateで自由なレイアウトの行を作れる
TDDで押さえておきたい絞り込みロジック(読み物)
カテゴリでの絞り込みも、第5章のTaskServiceと同じ形でTDDできます。
// 読み物: csharp-runnerで試せるTDDのイメージ(Red→Green)
var tasks = new List<(string Title, string Category)>
{
("企画書を書く", "仕事"),
("牛乳を買う", "プライベート"),
("見積もりを送る", "仕事"),
};
var workTasks = GetByCategory(tasks, "仕事");
Assert(workTasks.Count == 2, "指定カテゴリだけが2件返る");
Assert(workTasks.All(t => t.Category == "仕事"), "返るのは指定カテゴリだけ");
Console.WriteLine("全テスト成功");
// Green: Whereで「指定カテゴリと一致するものだけ」を絞り込む
List<(string Title, string Category)> GetByCategory(List<(string Title, string Category)> source, string category)
=> source.Where(t => t.Category == category).ToList();
void Assert(bool condition, string name)
{
if (!condition) throw new Exception($"FAIL: {name}");
}
- 「あるカテゴリだけを取り出す」という判断は、第5章で学んだ「Repositoryは出し入れだけ、Serviceが判断する」という役割分担にそのまま従う
- LINQの
Whereを使えば、GetActiveTasks(未完了だけ)と同じ書き方で「カテゴリが一致するものだけ」を取り出せる <mvvm-runner>の.Count(t => t.Category == "仕事")は、この考え方をViewModelの計算プロパティとして再現したもの
やってみよう
- カテゴリの入力欄に何も入れず(既定値「仕事」のまま)タイトルだけ入力して追加し、「仕事のタスク数」が増えることを確認する
- カテゴリを「プライベート」に書き換えてから追加し、「仕事のタスク数」が増えないことを確認する
- 一覧の各行に
[カテゴリ] タイトルの形で表示されていることを確認する
確認ポイント
- カテゴリが「仕事」のタスクを追加すると
WorkCountが増えること - カテゴリが「仕事」以外のタスクを追加しても
WorkCountが変わらないこと - 文字列の一致(
==)を条件にした計算プロパティが正しく動くこと
まとめ
- 「特定の条件で絞り込む」機能追加も、判定・絞り込みロジックはService/Entity側、Viewは結果の表示だけという役割分担で進める
.Count(x => 条件)の条件式は真偽値のプロパティだけでなく、文字列の比較(==)にも使える- 期限切れ・カテゴリ分けのどちらも、コースを通して学んだ「依存の向き」「TDD」「役割分担」を崩さずに追加できた
次回: コース全体のまとめと、次に学ぶとよいコースを紹介します。