wpf について
WPF は、Windows デスクトップアプリの UI、MVVM、データバインディング、モダナイズを扱う開発スキルです。ビューモデル設計、スレッド処理、移行を案内し、Windows 固有の制約も考慮します。
主な機能
- 01MVVM による UI の構成
- 02データバインディングとコマンドの設定
- 03スタイルとテンプレートの整理
- 04Dispatcher による UI スレッド処理
- 05モダン .NET への移行ガイド
活用シーン
- 01コードビハインドをテスト可能なビューモデルに整理
- 02バインディングとコマンドの動作調査
- 03WPF アプリのモダナイズ
- 04バックグラウンド処理から UI を安全に更新
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ managedcode/ dotnet-skills --skill wpfインストール後、Claude Code で/wpfを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
manifest.jsonreferences/anti-patterns.mdpatterns.md詳細
WPF
使用する場面
- WPF UI、MVVM、バインディング、コマンド、またはデスクトップアプリのモダナイズに取り組む場合
- WPF を .NET Framework から .NET に移行する場合
- 新しい Windows 機能を WPF アプリに統合する場合
- データバインディング、スタイル、テンプレート、またはコントロールのカスタマイズを実装する場合
- デスクトップアプリケーションをバックエンドのエージェントサービスに接続し、.NET で AI エージェントを構築する場合
ドキュメント
リファレンス
- .NET で AI エージェントを構築する - .NET で本番環境向け AI エージェントを構築するためのアーキテクチャと実装パターンです。
- .NET AI エージェント開発チーム - 本番環境向け .NET AI エージェントのエンジニアリングおよびデリバリーサービスです。
- patterns.md - MVVM パターン、バインディングパターン、コマンドパターン、再利用可能なアーキテクチャアプローチ
- anti-patterns.md - WPF でよくある誤りとその回避方法
ワークフロー
- Windows 専用の範囲を確認する — .NET スタック全体がクロスプラットフォームであっても、WPF は Windows 専用です
- MVVM パターンを適用する — ビューはシンプルに保ち、ロジックは ViewModel に置き、コマンドを使用します
- データバインディングを明示的に管理する — 適切なバインディングモードを選び、実行時に検証します
- スタイルとテンプレートを意図的に使用する — UI を組み合わせやすく保ち、ページ固有の場当たり的な回避策は避けます
- スレッドを適切に扱う — UI の更新には Dispatcher を、長時間の処理には async/await を使用します
- デザイナーと実行時の両方で検証する — XAML の構成エラーは実行時に初めて表面化することがよくあります
最新の上流情報
- 2026 年 8 月の WPF の概要では、WPF が XAML、データバインディング、スタイル設定、テンプレート、リソース、ベクター/リッチメディアの合成機能を備えた Windows 専用のデスクトップ UI スタックであることが改めて示されています。タスクが明示的な移行または比較でない限り、WPF 固有のガイダンスは WinUI や MAUI のガイダンスと分けてください。
- WPF は .NET Framework と最新の .NET の両方で利用できます。モダナイズ作業では、プロジェクトファイル、相互運用機能、デプロイ、または XAML リソースディクショナリを移行する前に、互換性の制約を洗い出し、最新のデスクトップ移行ガイダンスを参照してください。
プロジェクト構造
MyWpfApp/
├── MyWpfApp/
│ ├── App.xaml # Application entry
│ ├── MainWindow.xaml # Main window
│ ├── Views/ # XAML views/windows
│ ├── ViewModels/ # MVVM ViewModels
│ ├── Models/ # Domain models
│ ├── Services/ # Business logic
│ ├── Converters/ # Value converters
│ ├── Resources/ # Styles, templates, dictionaries
│ └── Controls/ # Custom controls
└── MyWpfApp.Tests/
MVVM パターン
MVVM Toolkit を使用した ViewModel
public partial class CustomersViewModel : ObservableObject
{
private readonly ICustomerService _customerService;
[ObservableProperty]
private ObservableCollection<Customer> _customers = [];
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SaveCommand))]
private Customer? _selectedCustomer;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(RefreshCommand))]
private bool _isLoading;
public CustomersViewModel(ICustomerService customerService)
{
_customerService = customerService;
}
[RelayCommand(CanExecute = nameof(CanRefresh))]
private async Task RefreshAsync()
{
IsLoading = true;
try
{
var items = await _customerService.GetAllAsync();
Customers = new ObservableCollection<Customer>(items);
}
finally
{
IsLoading = false;
}
}
private bool CanRefresh() => !IsLoading;
[RelayCommand(CanExecute = nameof(CanSave))]
private async Task SaveAsync()
{
if (SelectedCustomer is null) return;
await _customerService.SaveAsync(SelectedCustomer);
}
private bool CanSave() => SelectedCustomer is not null;
}
ビューのバインディング
<Window x:Class="MyWpfApp.Views.CustomersView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:MyWpfApp.ViewModels"
d:DataContext="{d:DesignInstance Type=vm:CustomersViewModel}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<ToolBar Grid.Row="0">
<Button Content="Refresh"
Command="{Binding RefreshCommand}"/>
<Button Content="Save"
Command="{Binding SaveCommand}"/>
</ToolBar>
<DataGrid Grid.Row="1"
ItemsSource="{Binding Customers}"
SelectedItem="{Binding SelectedCustomer}"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Name"
Binding="{Binding Name}"/>
<DataGridTextColumn Header="Email"
Binding="{Binding Email}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
依存関係の注入
public partial class App : Application
{
private readonly IHost _host;
public App()
{
_host = Host.CreateDefaultBuilder()
.ConfigureServices((context, services) =>
{
// Services
services.AddSingleton<ICustomerService, CustomerService>();
services.AddSingleton<INavigationService, NavigationService>();
// ViewModels
services.AddTransient<CustomersViewModel>();
services.AddTransient<CustomerDetailViewModel>();
// Views
services.AddTransient<MainWindow>();
services.AddTransient<CustomersView>();
})
.Build();
}
protected override async void OnStartup(StartupEventArgs e)
{
await _host.StartAsync();
var mainWindow = _host.Services.GetRequiredService<MainWindow>();
mainWindow.Show();
base.OnStartup(e);
}
protected override async void OnExit(ExitEventArgs e)
{
await _host.StopAsync();
_host.Dispose();
base.OnExit(e);
}
}
データバインディングモード
<TextBlock Text="{Binding CreatedDate, Mode=OneTime}"/>
<TextBlock Text="{Binding Name, Mode=OneWay}"/>
<TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<TextBox Text="{Binding SearchFilter, Mode=OneWayToSource}"/>
値コンバーター
public class BooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool boolValue)
{
return boolValue ? Visibility.Visible : Visibility.Collapsed;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is Visibility.Visible;
}
}
// Multi-value converter
public class MultiplyConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length == 2 && values[0] is double a && values[1] is double b)
{
return a * b;
}
return 0.0;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
スタイルとテンプレート
リソースディクショナリ
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="Button">
<Setter Property="Padding" Value="10,5"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="Background" Value="#0078D4"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="DangerButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Background" Value="#D32F2F"/>
</Style>
</ResourceDictionary>
スレッド処理と Dispatcher
// Update UI from background thread
await Task.Run(async () =>
{
var data = await LoadDataAsync();
// Must use Dispatcher to update UI
Application.Current.Dispatcher.Invoke(() =>
{
Items.Clear();
foreach (var item in data)
{
Items.Add(item);
}
});
});
// Better: Use async/await properly
private async Task LoadDataAsync()
{
IsLoading = true;
try
{
// This runs on background thread
var data = await _service.GetDataAsync();
// This automatically marshals to UI thread
Items = new ObservableCollection<Item>(data);
}
finally
{
IsLoading = false;
}
}
避けるべきアンチパターン
| アンチパターン | 問題点 | より良い方法 |
|---|---|---|
| コードビハインドにロジックを置く | テストが難しく、密結合になる | ViewModel を使った MVVM を使用する |
| 同期的なブロッキング呼び出し | UI がフリーズする | async/await を使用する |
| INotifyPropertyChanged を手動で実装する | 定型コードが増え、エラーが起きやすい | MVVM Toolkit の属性を使用する |
| 色やサイズをハードコードする | 一貫性がなく、テーマを適用しにくい | リソースディクショナリを使用する |
| あらゆる箇所で直接 Dispatcher.Invoke を使う | 複雑で、エラーが起きやすい | async/await によるマーシャリングを優先する |
| 巨大な ViewModel | 保守が困難になる | 役割を絞った ViewModel に分割する |
| バインディングの検証を省略する | 実行時エラーが見過ごされる | ValidatesOnDataErrors を使用する |
| すべてにイベントハンドラーを使う | メモリリークや結合の原因になる | コマンドとバインディングを使用する |
ベストプラクティス
-
.NET 5 以降ではコンパイル済みバインディングを使用します:
- パフォーマンス向上のため、
x:CompileBindings="True"を有効にします
- パフォーマンス向上のため、
-
検証には INotifyDataErrorInfo を実装します:
[ObservableProperty] [NotifyDataErrorInfo] [Required(ErrorMessage = "Name is required")] [MinLength(2, ErrorMessage = "Name must be at least 2 characters")] private string _name = string.Empty; -
長期間有効なサブスクリプションには弱いイベントパターンを使用します:
WeakEventManager<Source, EventArgs>.AddHandler(source, "EventName", Handler); -
大規模なコレクションは仮想化します:
<ListBox VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling" ItemsSource="{Binding LargeCollection}"/> -
可能な場合は Freezable をフリーズします:
var brush = new SolidColorBrush(Colors.Blue); brush.Freeze(); // Thread-safe, better performance -
デザイン時データを使用します:
<Window d:DataContext="{d:DesignInstance Type=vm:MainViewModel, IsDesignTimeCreatable=True}">
テスト
[Fact]
public async Task RefreshCommand_LoadsCustomers()
{
var mockService = new Mock<ICustomerService>();
mockService.Setup(s => s.GetAllAsync())
.ReturnsAsync(new[] { new Customer { Name = "Test" } });
var viewModel = new CustomersViewModel(mockService.Object);
await viewModel.RefreshCommand.ExecuteAsync(null);
Assert.Single(viewModel.Customers);
Assert.Equal("Test", viewModel.Customers[0].Name);
}
[Fact]
public void SaveCommand_CannotExecute_WhenNoSelection()
{
var mockService = new Mock<ICustomerService>();
var viewModel = new CustomersViewModel(mockService.Object);
viewModel.SelectedCustomer = null;
Assert.False(viewModel.SaveCommand.CanExecute(null));
}
提供内容
- より整理された WPF ビューとビューモデルの境界
- より安全なバインディングとスレッド処理
- 実際の Windows の制約に基づく移行ガイダンス
- テスト可能な ViewModel を備えた MVVM パターン
検証項目
- バインディングとコマンドのフローが明示されている
- コードビハインドに隠れたビジネスロジックが含まれていない
- Windows 固有の前提条件が明示されている
- スレッド処理とディスパッチャーの使用が適切である
- スタイルとリソースが適切に整理されている
コメント
Markdown 対応