本文へスキップ
BecomeCoder

C# MVVMコース · 第10章 仕上げ ― アプリを完成させる · レッスン40

機能追加をTDDで ― カテゴリ分け

ブラウザで完結

導入

もう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の計算プロパティとして再現したもの

やってみよう

  1. カテゴリの入力欄に何も入れず(既定値「仕事」のまま)タイトルだけ入力して追加し、「仕事のタスク数」が増えることを確認する
  2. カテゴリを「プライベート」に書き換えてから追加し、「仕事のタスク数」が増えないことを確認する
  3. 一覧の各行に[カテゴリ] タイトルの形で表示されていることを確認する

確認ポイント

  • カテゴリが「仕事」のタスクを追加するとWorkCountが増えること
  • カテゴリが「仕事」以外のタスクを追加してもWorkCountが変わらないこと
  • 文字列の一致(==)を条件にした計算プロパティが正しく動くこと

まとめ

  • 「特定の条件で絞り込む」機能追加も、判定・絞り込みロジックはService/Entity側、Viewは結果の表示だけという役割分担で進める
  • .Count(x => 条件)の条件式は真偽値のプロパティだけでなく、文字列の比較(==)にも使える
  • 期限切れ・カテゴリ分けのどちらも、コースを通して学んだ「依存の向き」「TDD」「役割分担」を崩さずに追加できた

次回: コース全体のまとめと、次に学ぶとよいコースを紹介します。

実際に動かしてみよう

上がXAML(View)、真ん中がViewModelのC#風コード、右がプレビューです。ViewModelのプロパティやRelayCommandを書き換えると、プレビュー側の一覧・入力・ボタンの有効/無効にその場で反映されます(本物のC#コンパイルではなく、単純なプロパティ・計算プロパティ・コマンドの実行/CanExecuteを再現した学習用エンジンです。複雑なLINQや独自クラスのメソッド呼び出しなどは対象外です)。本文のコードを書き換えて、Viewとの連動を確かめながら進めましょう。

MVVM (XAML + ViewModel) プレビュー

書いた XAML(View) と ViewModel(C#風) をその場で連携させて動かす学習用プレビューを読み込みます(本物のWPF/MVVMではなく、単純な自動プロパティ・計算プロパティ(=>)・ICommand(RelayCommandのexecute/CanExecute)・{Binding}の双方向バインディングを再現したものです。独自クラスのメソッド呼び出し・複雑なLINQ・非同期処理・DI・CommandParameterは実行されません)。
スクロールして表示された時点でも自動で読み込まれます。