Claude CodeでWPFアプリを作る — dotnet buildとViewModelテストで検証する
Claude CodeはWPFの画面を見られません。ViewModelを別プロジェクトに切り出し、dotnet buildとdotnet testを完了条件にする設計と、XAMLのBinding誤記を拾うテストを示します。
Claude CodeにWPFアプリを任せるときの最大の制約は、出来上がった画面をClaudeが目で確かめられないことです。ボタンが押せるか、文字が切れていないかは、ターミナルの出力には現れません。
そこで検証の足場を画面の外に置きます。ロジックはViewModelに寄せて単体テストで確かめ、画面側はdotnet buildが通ることとBindingの名前の整合までを機械で確かめます。残る見た目の確認は人が1回見る、という分担です。
WPFは何をWindows上で、何を画面なしで確かめられるか
WPFは.NET上で動きますが、動くのはWindowsだけです。ドキュメントにも「.NETはクロスプラットフォームだが、WPFはWindowsでしか動かない」と明記されています。
プロジェクトファイルの要点は3つです。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
</PropertyGroup>
</Project>UseWPFをtrueにすると、WPFライブラリへの参照が入り、MSBuildのパイプラインもWPFのファイルを処理する形に変わります。このとき対象にできるのはWindowsだけです。
この制約が設計に効きます。検証の層ごとに「画面を出さずにできるか」を分けると、次のようになります。
| 確かめたいこと | 画面なしでできるか | 手段 |
|---|---|---|
| XAMLとC#がコンパイルできる | 画面なしでできるかできる(Windows上のビルド) | 手段dotnet build |
| ボタンの処理や計算の結果 | 画面なしでできるかできる | 手段ViewModelの単体テスト |
| BindingのパスがViewModelにある | 画面なしでできるか工夫すればできる | 手段XAMLを読むテスト(後述) |
| レイアウトの崩れ・色・フォーカス移動 | 画面なしでできるかできない | 手段人が起動して目で見る |
見た目だけが人の領域に残ります。Claudeに任せる範囲は、上の3行までです。
この表は、dotnetがWindows上で動く前提です。Claude CodeはネイティブWindowsでも、WSLの中でも動かせます。ネイティブWindowsは、Git for Windowsを入れればBashツールを、入れなければPowerShellツールを使います。WSLは、Linux側にインストールしてclaudeもそこで起動する形です。この記事のdotnet buildとdotnet testは、ネイティブWindowsで動かす構成を想定しています。macOSやLinuxからWPFのビルドを回す構成は扱いません。
ロジックをViewModelに寄せて別プロジェクトにする
画面を見られないなら、画面に載るロジックを画面から切り離します。WPFのコードビハインドは、x:ClassでXAMLと結び付いたクラスで、ボタンのクリックなどに反応する処理を書く場所です。ここに計算や状態を書くと、テストするにはウィンドウを生成するしかなくなります。
構成は次の3プロジェクトです。
src/
MyApp.ViewModels/ net8.0 のクラスライブラリ(WPFを参照しない)
MyApp/ net8.0-windows の WPF アプリ(ViewModels を参照)
tests/
MyApp.Tests/ ViewModels を参照するテストViewModelsにはWPFの型を一切持ち込みません。WPFの画面は、データバインディングでViewModelの値を表示し、編集をViewModelに書き戻すだけの薄い層になります。バインディングは、コントロール(バインディングの対象)とデータ(ソース)を結ぶ仕組みで、マークアップに{Binding ...}と書いて宣言します。
ソースの変更を画面に反映するには、ソースがINotifyPropertyChangedのような変更通知の仕組みを実装している必要があります。自前で書くと定型コードが多いので、Microsoftが保守するCommunityToolkit.MvvmのObservableObjectを使います。このパッケージは.NET Standardを対象にしていて、WPFを含む複数のUIフレームワークで使えます。
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace MyApp.ViewModels;
public class CounterViewModel : ObservableObject
{
private int count;
public CounterViewModel()
{
IncrementCommand = new RelayCommand(Increment, () => Count < 10);
}
public int Count
{
get => count;
private set
{
if (SetProperty(ref count, value))
IncrementCommand.NotifyCanExecuteChanged();
}
}
public RelayCommand IncrementCommand { get; }
private void Increment() => Count++;
}SetPropertyは値が変わったときに変更通知を発行します。RelayCommandはコマンドの実行可否が変わったことをNotifyCanExecuteChangedで知らせます。どちらもToolkitのドキュメントに載っている使い方です。
ViewModelをdotnet testで確かめる
このViewModelは、ウィンドウを作らずにテストできます。
using MyApp.ViewModels;
using Xunit;
public class CounterViewModelTests
{
[Fact]
public void Incrementで1増える()
{
var vm = new CounterViewModel();
vm.IncrementCommand.Execute(null);
Assert.Equal(1, vm.Count);
}
[Fact]
public void 上限の10に達するとコマンドが無効になる()
{
var vm = new CounterViewModel();
for (var i = 0; i < 10; i++) vm.IncrementCommand.Execute(null);
Assert.False(vm.IncrementCommand.CanExecute(null));
}
}実行はdotnet testです。このコマンドはソリューションをビルドしたうえでテストを走らせるので、コンパイルエラーとテスト失敗が1回の出力に出ます。Claudeにはこの出力だけを読ませれば足ります。
dotnet build MyApp.sln --nologo -v q
dotnet test tests/MyApp.Tests --nologoテストの書き方も指示に含めます。「ボタンを押したらこの状態になる」という受け入れ条件を、テストの形でClaudeに先に書かせる流れが安定します。実装を書く前にテストを書かせ、失敗を確認してから実装に進ませる順序です。
XAMLのBinding誤記は、テストで拾う
dotnet buildが通っても安心できない箇所があります。Bindingのパスです。
{Binding Count}のCountは、マークアップ内の単なる文字列です。バインディングはDataContext(データの既定の取り出し元)を見て解決されるので、パスに誤記があっても、コンパイルの段階ではエラーになりにくい箇所です。実行して画面を開いたときに、表示が空のまま気づかれずに残ります。ここは画面を見られないClaudeが最も取りこぼしやすい場所です。
対策は、XAMLの{Binding ...}を機械的に読み、ViewModelに同名のプロパティがあるかを確かめるテストを足すことです。次は考え方を示す最小の例です。公式のサンプルではなく、筆者が組んだ書き方です。
using System.Text.RegularExpressions;
using MyApp.ViewModels;
using Xunit;
public class BindingPathTests
{
[Fact]
public void XamlのBindingパスがViewModelに存在する()
{
var xaml = File.ReadAllText(
"../../../../../src/MyApp/MainWindow.xaml");
var paths = Regex.Matches(xaml, @"\{Binding\s+(?:Path=)?(\w+)")
.Select(m => m.Groups[1].Value)
.Distinct();
foreach (var path in paths)
Assert.True(
typeof(CounterViewModel).GetProperty(path) != null,
$"ViewModelに {path} がありません");
}
}制約も明らかです。DataTemplateの中のBindingは、DataContextが別のオブジェクトになるため、正規表現では区別できず誤検知します。画面ごとに検査の対象を絞るか、テンプレート内を除外する工夫が要ります。あくまで「手の届く範囲の誤記だけを拾う網」です。
落とし穴
パスは合っているのに、入力がViewModelに戻らない
BindingのModeやUpdateSourceTriggerの指定、コンバーターの設定は、パス名のテストでは見えません。TwoWayで書き戻すか、OneWayで表示だけかは、既定がプロパティごとに違います。編集フォームを作らせたら、書き戻しの挙動だけは人が1回触って確認します。
CLAUDE.mdで完了条件を固定する
「ビルドが通ったら完了」とClaudeが判断しないよう、プロジェクト直下のCLAUDE.mdに検証コマンドを書きます。
# WPFアプリ開発のルール
- ロジックは src/MyApp.ViewModels に書く。WPFの型を参照しない
- コードビハインドには画面固有の処理(フォーカス等)だけを書く
- 変更後は `dotnet build MyApp.sln` と `dotnet test` を実行し、
両方が成功するまで完了と報告しない
- XAMLに新しい {Binding X} を足したら、ViewModelにXを足す
- レイアウトと見た目は確認できない。変更点を箇条書きで報告し、
目視確認が必要な箇所を明示する最後の行が実務で効きます。見られないことを前提に、確認を人に返す項目を出させる指示です。
buildとtestを権限とフックで回す
毎回の確認ダイアログを減らすため、検証用のコマンドだけ許可します。ルールはサブコマンドの後ろに*を置く形です。
{
"permissions": {
"allow": [
"Bash(dotnet build *)",
"Bash(dotnet test *)"
]
}
}さらに、ファイル編集のたびにビルドさせるなら、PostToolUseフックをEdit|Writeで絞ります。ただし、WPFのビルドは編集のたびに回すと重くなりがちです。テストの自動実行をどのイベントに載せるかは、PostToolUseとStopの使い分けで整理しています。型検査を完了判定にする同じ考え方は、React Router v7のtypegenの記事にもあります。
1回の依頼の流れ
- 1
テストを先に書かせる
受け入れ条件をViewModelのテストにします。
- 2
実装させる
ViewModelとXAMLを書かせ、Bindingの名前を揃えさせます。
- 3
build と test を実行させる
失敗の出力を読ませて直させ、両方が成功するまで繰り返します。
- 4
目視項目を受け取る
Claudeが挙げた確認項目を、人が実機で見ます。
見た目を直接見られない分、XAMLの書き方で崩れにくくする
画面を確かめられない以上、崩れにくい書き方をあらかじめ指定しておくほうが安全です。WPFのレイアウトは、ウィンドウサイズや表示設定の変化に合わせて、親と子が場所と大きさを折衝する相対配置が土台です。
レイアウト用の部品には、Canvas、DockPanel、Grid、StackPanel、VirtualizingStackPanel、WrapPanelがあります。このうちCanvasは、子のコントロールが自分で位置を決める部品です。座標を直書きしがちなので、Claudeに任せる画面はGridとStackPanelを基本にするよう指示します。
- 画面のレイアウトは Grid と StackPanel で組む
- Canvas と固定のWidth/Heightは使わない(理由を書いた場合のみ可)
- 文字列はリソースにせず直書きでよいが、色はStyleにまとめるもう一つ、Bindingの向きも指示に含めます。OneWayはソースの変更を画面に反映するだけで、画面側の編集はソースに戻りません。TwoWayは両方向に反映するので、編集できるフォームに向きます。多くのプロパティは既定がOneWayですが、TextBoxのTextのように既定が双方向のものもあります。どれが既定かはプロパティごとに違うため、フォームを作らせるときはModeを明示させ、書き戻しを確かめるテストをViewModel側に置きます。
[Fact]
public void 名前を設定すると変更通知が飛ぶ()
{
var vm = new ProfileViewModel();
var changed = new List<string?>();
vm.PropertyChanged += (_, e) => changed.Add(e.PropertyName);
vm.Name = "Taro";
Assert.Contains(nameof(vm.Name), changed);
}通知が飛ぶことをテストで確かめておくと、SetPropertyの呼び忘れという典型的な失敗を、画面を開かずに検出できます。
つまずきやすい点
ViewModelにMessageBoxやWindowを書かせてしまう
テストが走らなくなります。ダイアログを出す処理は、ViewModelが呼ぶインターフェース(例: IDialogService)の背後に置き、テストでは偽物に差し替えます。CLAUDE.mdの「WPFの型を参照しない」は、この予防です。ViewModelsプロジェクトがWPFに依存していなければ、WPFの型を書いた時点でビルドが落ちます。
プロジェクトの対象フレームワークを間違える
WPFアプリはnet8.0-windowsのようなWindows固有の指定が必要です。ViewModelsにUseWPFを付けたりnet8.0-windowsにしたりすると、WPFの型を持ち込める余地が生まれ、切り分けの意味が薄れます。素のnet8.0にしておけば、MessageBoxなどを書いた時点でビルドが落ちます。
コードビハインドを消さずに足し続ける
InitializeComponentはビルド時に生成され、XAMLとコードビハインドを結びます。ボタンのClickをコードビハインドのメソッドに結ぶ書き方は動きますが、テストできません。コマンドとBindingに置き換える方針をCLAUDE.mdに入れておくと、Claudeがイベントハンドラを増やし続けるのを防げます。
MAUIとの違い
同じ.NETの画面系でも、MAUIは複数OS向けのビルドと環境整備が主題になります。その手順はClaude CodeでMAUIアプリを開発するにあります。WPFはWindows専用なので、悩みは環境よりも「画面を見られない」ことに集まります。ここで挙げた設計は、WPFに限らずXAMLを使う他のUIにも持ち込めます。
まとめ
画面が見られないClaudeには、画面の外に検証の足場を置きます。ロジックはViewModelに寄せてdotnet testで確かめ、XAMLはdotnet buildとBindingパスのテストで守ります。見た目だけは人が見ます。
最初の一手は、CLAUDE.mdに「ビルドとテストが両方通るまで完了としない」の1行を入れることです。その1行で、Claudeの「できました」の意味が変わります。
関連する記事
Claude Code をもっと見る →Claude Codeとは — できること・料金・始め方と使い方の全体像
Claude CodeでROS 2パッケージを作る — colcon buildとlaunch_testの反復
Claude CodeでQuartoレポートを作る — quarto renderで失敗を拾わせる
Claude CodeでMQTT検証 — mosquittoでpublishとsubscribeを機械的に確かめる
Claude CodeでMkDocs Materialの文書を書く — build --strictで検証
Claude CodeでHaskellを書く — stack buildとhlintを検証ループに固定