本文へスキップ
BecomeCoder

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

機能追加をTDDで ― 期限切れ強調表示

ブラウザで完結

導入

「締切を過ぎたタスクを目立たせてほしい」という要望が来たとします。実務でこの手の機能追加をするときも、進め方はこれまでと同じです。「期限切れかどうか」を判定するロジックはドメイン(Entity寄り)の責務としてTDDで先にテストし、Viewはその結果(true/false)を受け取って表示を変えるだけ、という責務の分け方を崩さないことが大切です。

説明

flowchart LR
    D["DueDate(締切)と Now(現在時刻)"] --> J["IsOverdue 判定<br/>(ドメイン・csharp-runnerでTDD)"]
    J --> DTO["TaskDto.IsOverdue : bool"]
    DTO --> V["View(mvvm-runner)<br/>件数の表示"]
<Window Title="タスク一覧(期限切れ強調)" Width="380" Height="360">
  <StackPanel Margin="16">
    <TextBlock Text="期限切れ: " FontWeight="Bold" />
    <TextBlock Text="{Binding OverdueCount}" FontSize="20" Foreground="Red" />
    <TextBox Text="{Binding NewTaskTitle, Mode=TwoWay}" Margin="0,10" />
    <Button Content="追加" Command="{Binding AddCommand}" Width="80" />
    <Button Content="期限切れの例を追加" Command="{Binding AddOverdueSampleCommand}" Margin="0,6" Width="160" />
    <ListBox ItemsSource="{Binding Tasks}" DisplayMemberPath="Title" Height="120" Margin="0,10" />
  </StackPanel>
</Window>
public record TaskDto(string Title, bool IsCompleted, bool IsOverdue);

public class TaskListViewModel
{
    public string NewTaskTitle { get; set; } = "";
    public ObservableCollection<TaskDto> Tasks { get; set; } = new();
    public int OverdueCount => Tasks.Count(t => t.IsOverdue);

    public ICommand AddCommand => new RelayCommand(
        () => { Tasks.Add(new TaskDto(NewTaskTitle, false, false)); NewTaskTitle = ""; },
        () => !string.IsNullOrWhiteSpace(NewTaskTitle)
    );

    public ICommand AddOverdueSampleCommand => new RelayCommand(
        () => { Tasks.Add(new TaskDto("提出物 ⚠ 期限切れ", false, true)); }
    );
}
  • TaskDtoIsOverdueを1つ足すだけで、判定結果をViewまで運べる。判定ロジックそのもの(締切と現在時刻を比べる部分)はViewModelにもViewにも書かない
  • OverdueCount => Tasks.Count(t => t.IsOverdue)第7章と同じ.Count(x => 条件)の形。件数として画面に反映する
  • このランナーでは日時の比較や、一覧の行ごとに色を変える「スタイルトリガー」までは再現できないため、「期限切れの例を追加」ボタンでIsOverdue: trueのタスクを足し、件数表示で確認する形に簡略化している。実務のWPFではDataTriggerで行の背景色などを条件分岐できる

TDDで押さえておきたい判定ロジック(読み物)

実際のアプリでは、IsOverdueTaskItem(またはそれに近いドメインの型)が持つ振る舞いとして、これまでの章と同じようにTDDで育てます。画面がなくてもテストできることを、もう一度確認しておきましょう。

// 読み物: csharp-runnerで試せるTDDのイメージ(このレッスンの実行はWPF側の確認に使う)
Assert(IsOverdue(due: new DateTime(2026, 1, 10), now: new DateTime(2026, 2, 1)) == true, "締切を過ぎたら期限切れ");
Assert(IsOverdue(due: new DateTime(2026, 3, 1), now: new DateTime(2026, 2, 1)) == false, "締切前は期限切れではない");
Console.WriteLine("全テスト成功");

// Green: 締切(due)が現在時刻(now)より前なら期限切れ
bool IsOverdue(DateTime due, DateTime now) => due < now;

void Assert(bool condition, string name)
{
    if (!condition) throw new Exception($"FAIL: {name}");
}
  • IsOverdueはWPFのことを何も知らない、ただのboolを返す関数(実務ではTaskItemのプロパティやメソッドにする)
  • 判定ロジックをここまで小さく独立させておけば、締切の境界値(ちょうど締切当日など)のテストも、画面なしで何十パターンでも書ける
  • Viewが受け取るのは、その結果であるIsOverdue: true/falseという1つの値だけでよい

やってみよう

  1. 「追加」で通常のタスクをいくつか追加し、「期限切れ: 0」のまま変わらないことを確認する
  2. 「期限切れの例を追加」を押すと一覧に警告付きのタスクが増え、「期限切れ」の件数が増えることを確認する
  3. 通常の追加と期限切れの追加を混ぜて、件数表示が正しく連動することを確認する

確認ポイント

  • 期限切れのタスクを追加するとOverdueCountが増えること
  • 通常のタスクの追加ではOverdueCountが変わらないこと
  • 判定ロジック(IsOverdue)自体は、Viewと切り離して単体でテストできること

まとめ

  • 新機能を追加するときも「判定ロジックはドメインでTDD」「Viewは結果を表示するだけ」という責務分担は崩さない
  • TaskDtoにプロパティを1つ足すだけで、判定結果を画面まで運べる
  • 一覧の行ごとの見た目分岐(DataTrigger)はこの教材用ランナーの対象外。仕組み自体は「値を受け取って表示を変える」という点で同じ

次回: 「カテゴリ分け」機能を、同じ進め方でもう1つ追加します。

実際に動かしてみよう

上が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は実行されません)。
スクロールして表示された時点でも自動で読み込まれます。