導入
「締切を過ぎたタスクを目立たせてほしい」という要望が来たとします。実務でこの手の機能追加をするときも、進め方はこれまでと同じです。「期限切れかどうか」を判定するロジックはドメイン(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)); }
);
}
TaskDtoにIsOverdueを1つ足すだけで、判定結果をViewまで運べる。判定ロジックそのもの(締切と現在時刻を比べる部分)はViewModelにもViewにも書かないOverdueCount => Tasks.Count(t => t.IsOverdue)は第7章と同じ.Count(x => 条件)の形。件数として画面に反映する- このランナーでは日時の比較や、一覧の行ごとに色を変える「スタイルトリガー」までは再現できないため、「期限切れの例を追加」ボタンで
IsOverdue: trueのタスクを足し、件数表示で確認する形に簡略化している。実務のWPFではDataTriggerで行の背景色などを条件分岐できる
TDDで押さえておきたい判定ロジック(読み物)
実際のアプリでは、IsOverdueはTaskItem(またはそれに近いドメインの型)が持つ振る舞いとして、これまでの章と同じように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つの値だけでよい
やってみよう
- 「追加」で通常のタスクをいくつか追加し、「期限切れ: 0」のまま変わらないことを確認する
- 「期限切れの例を追加」を押すと一覧に警告付きのタスクが増え、「期限切れ」の件数が増えることを確認する
- 通常の追加と期限切れの追加を混ぜて、件数表示が正しく連動することを確認する
確認ポイント
- 期限切れのタスクを追加すると
OverdueCountが増えること - 通常のタスクの追加では
OverdueCountが変わらないこと - 判定ロジック(
IsOverdue)自体は、Viewと切り離して単体でテストできること
まとめ
- 新機能を追加するときも「判定ロジックはドメインでTDD」「Viewは結果を表示するだけ」という責務分担は崩さない
TaskDtoにプロパティを1つ足すだけで、判定結果を画面まで運べる- 一覧の行ごとの見た目分岐(
DataTrigger)はこの教材用ランナーの対象外。仕組み自体は「値を受け取って表示を変える」という点で同じ
次回: 「カテゴリ分け」機能を、同じ進め方でもう1つ追加します。