dotnet-wpf-modern について
dotnet-wpf-modernは、.NET 8+のWPFを扱うコーディングエージェントや開発者向けのSkillです。Host builder、CommunityToolkit.Mvvm、パフォーマンス、最新のC#、.NET 9+のFluentテーマを扱い、実装時の注意点も解説します。
主な機能
- 01.NET 8+のHost builderと依存性注入
- 02CommunityToolkit.Mvvmのソースジェネレーター
- 03描画・起動・メモリに関する指針
- 04最新のC#パターン
- 05.NET 9+のFluentテーマとシステムテーマ
活用シーン
- 01WPFアプリにHost builderを設定
- 02MVVM Toolkitで定型コードを削減
- 03Fluentテーマやシステムの配色を設定
- 04トリミングや非同期処理の注意点を確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-wpf-modernインストール後、Claude Code で/dotnet-wpf-modernを入力して読み込まれたか確認します。
詳細
dotnet-wpf-modern
.NET 8 以降の WPF:ホストビルダーと依存性注入、CommunityToolkit.Mvvm のソースジェネレーターを使った MVVM、ハードウェアアクセラレーションによるレンダリングの改善、最新の C# パターン(レコード、プライマリコンストラクター、パターンマッチング)、Fluent テーマ(.NET 9 以降)、システムテーマの検出、.NET Framework WPF からの変更点について説明します。
バージョンの前提: .NET 8.0 以降をベースラインとします(現在の LTS)。TFM は net8.0-windows です。.NET 9 の機能(Fluent テーマ)は明示的に表示します。
スコープの境界: このスキルは、最新の .NET における WPF のパターン(ホストビルダー、MVVM Toolkit、パフォーマンス、最新の C#、テーマ設定)を対象とします。.NET Framework から .NET 8 以降への移行は、[skill:dotnet-wpf-migration] が担当します。デスクトップのテストは、[skill:dotnet-ui-testing-core] が担当します。
対象外: WPF .NET Framework のパターン(レガシー)――このスキルの対象は .NET 8 以降のみです。移行ガイダンスについては、[skill:dotnet-wpf-migration] を参照してください。デスクトップのテストについては、[skill:dotnet-ui-testing-core] を参照してください。一般的な Native AOT のパターンについては、[skill:dotnet-native-aot] を参照してください。UI フレームワークの選定については、[skill:dotnet-ui-chooser] を参照してください。
相互参照:デスクトップのテストについては [skill:dotnet-ui-testing-core]、WinUI 3 のパターンについては [skill:dotnet-winui]、移行ガイダンスについては [skill:dotnet-wpf-migration]、一般的な AOT については [skill:dotnet-native-aot]、フレームワークの選定については [skill:dotnet-ui-chooser]、アクセシビリティのパターン(AutomationProperties、AutomationPeer、UI Automation)については [skill:dotnet-accessibility] を参照してください。
.NET 8 以降の変更点
.NET 8 以降の WPF は、.NET Framework WPF から大幅に近代化されています。プロジェクト形式、DI パターン、言語機能、ランタイムの動作がすべて変更されています。
新しいプロジェクトテンプレート
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.*" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.*" />
</ItemGroup>
</Project>
.NET Framework WPF との主な違い:
- SDK スタイルの
.csproj(packages.configとAssemblyInfo.csは不要) - Nullable 参照型が既定で有効
- 暗黙的な using が有効
- NuGet の
PackageReference形式(packages.configではありません) - DI 用の
App.configは不要――ホストビルダーを使用します dotnet publishにより、単一のデプロイ成果物が生成されます- .NET をサイドバイサイドでインストール(マシン全体に適用されるフレームワークへの依存はありません)
ホストビルダーパターン
最新の WPF アプリでは、依存性注入、構成、ログ記録に汎用ホストを使用し、従来の ServiceLocator や手動の DI アプローチに置き換えます。
// App.xaml.cs
public partial class App : Application
{
private readonly IHost _host;
public App()
{
_host = Host.CreateDefaultBuilder()
.ConfigureAppConfiguration((context, config) =>
{
config.AddJsonFile("appsettings.json", optional: true);
})
.ConfigureServices((context, services) =>
{
// Services
services.AddSingleton<INavigationService, NavigationService>();
services.AddSingleton<IProductService, ProductService>();
services.AddSingleton<ISettingsService, SettingsService>();
// HTTP client
services.AddHttpClient("api", client =>
{
client.BaseAddress = new Uri(
context.Configuration["ApiBaseUrl"] ?? "https://api.example.com");
});
// ViewModels
services.AddTransient<MainViewModel>();
services.AddTransient<ProductListViewModel>();
services.AddTransient<SettingsViewModel>();
// Windows and pages
services.AddSingleton<MainWindow>();
})
.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);
}
public static T GetService<T>() where T : class
{
var app = (App)Application.Current;
return app._host.Services.GetRequiredService<T>();
}
}
MVVM Toolkit
CommunityToolkit.Mvvm(Microsoft MVVM Toolkit)は、最新の WPF に推奨される MVVM フレームワークです。ソースジェネレーターを使用して定型コードを削減します。
// ViewModels/ProductListViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class ProductListViewModel : ObservableObject
{
private readonly IProductService _productService;
public ProductListViewModel(IProductService productService)
{
_productService = productService;
}
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(SearchCommand))]
private string _searchTerm = "";
[ObservableProperty]
private ObservableCollection<Product> _products = [];
[ObservableProperty]
private bool _isLoading;
[RelayCommand]
private async Task LoadProductsAsync(CancellationToken ct)
{
IsLoading = true;
try
{
var items = await _productService.GetProductsAsync(ct);
Products = new ObservableCollection<Product>(items);
}
finally
{
IsLoading = false;
}
}
[RelayCommand(CanExecute = nameof(CanSearch))]
private async Task SearchAsync(CancellationToken ct)
{
var results = await _productService.SearchAsync(SearchTerm, ct);
Products = new ObservableCollection<Product>(results);
}
private bool CanSearch() => !string.IsNullOrWhiteSpace(SearchTerm);
}
MVVM Toolkit による XAML バインディング
<Window x:Class="MyApp.Views.ProductListWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:vm="clr-namespace:MyApp.ViewModels"
d:DataContext="{d:DesignInstance vm:ProductListViewModel}">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="16">
<TextBox Text="{Binding SearchTerm, UpdateSourceTrigger=PropertyChanged}"
Width="300" Margin="0,0,8,0" />
<Button Content="Search" Command="{Binding SearchCommand}" />
</StackPanel>
<ListBox ItemsSource="{Binding Products}" Margin="16">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="4">
<TextBlock Text="{Binding Name}" FontWeight="Bold" Margin="0,0,12,0" />
<TextBlock Text="{Binding Price, StringFormat='{}{0:C}'}" Foreground="Gray" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</Window>
主なソースジェネレーター属性:
[ObservableProperty]――バッキングフィールドからINotifyPropertyChangedを実装するプロパティを生成します[RelayCommand]――メソッドからICommandを生成します(非同期、キャンセル、CanExecuteに対応)[NotifyPropertyChangedFor]――依存プロパティのPropertyChangedを発生させます[NotifyCanExecuteChangedFor]――プロパティが変更されたときに、コマンドのCanExecuteを再評価します
パフォーマンス
.NET 8 以降の WPF は、.NET Framework WPF に比べて大幅なパフォーマンス向上を実現します。
ハードウェアアクセラレーションによるレンダリング
- DirectX 11 のレンダリングパスが .NET 8 以降で既定になりました(.NET Framework の DirectX 9 から変更)
- GPU アクセラレーションによるテキストレンダリングにより、テキストの明瞭さが向上し、テキストを多用する UI の CPU 使用率が低下します
- ランタイムの改善(動的 PGO、オンスタック置換)により、GC の負荷が軽減されます
起動時間
- ReadyToRun(R2R)――アセンブリを事前コンパイルし、起動時の JIT オーバーヘッドを削減します
- 段階的コンパイル――起動を高速化し、段階的に最適化します
- トリミングへの対応――
.NET 8+の WPF は、デプロイサイズを小さくする IL トリミングをサポートします
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<TrimMode>partial</TrimMode>
</PropertyGroup>
トリミングに関する注意: WPF は、データバインディングとリソース解決に XAML のリフレクションを多用します。TrimMode=full ではなく TrimMode=partial を使用し、十分にテストしてください。コンパイル済みバインディングと x:Type 参照は、文字列ベースのバインディングよりトリミングに安全です。
メモリと GC
- フローズンオブジェクトヒープ(.NET 8)-- 静的文字列とシングルトンの割り当てを、ガベージコレクションの対象外となるヒープセグメントに配置します
- 動的 PGO -- ランタイムプロファイルを活用して、ホットパスの JIT 最適化を行います
- ワーキングセットの削減 -- .NET 8 ランタイムは、.NET Framework CLR よりもベースラインメモリの使用量が少なくなっています
期待される改善
.NET 8 の WPF は、主要な指標において .NET Framework 4.8 と比べ、測定可能な改善をもたらします。正確な数値はワークロード、ハードウェア、アプリケーションの複雑さによって異なるため、必ずご自身のシナリオでベンチマークを実施してください。
- コールドスタート -- ReadyToRun、階層型コンパイル、フレームワーク初期化オーバーヘッドの削減により、大幅に高速化されます
- UI の仮想化 -- レンダリングパイプラインの改善と GC により、大規模な ItemsControl(ListBox、DataGrid)の処理時間が短縮されます
- GC の一時停止 -- .NET 8 の GC 改善(動的 PGO、フローズンオブジェクトヒープ、ピン留めオブジェクトヒープ)により、Gen2 コレクションの一時停止が短くなり、頻度も低下します
- メモリフットプリント -- .NET Framework CLR と比べて、ベースラインのワーキングセットが小さくなります
モダンな C#
.NET 8 以降の WPF プロジェクトでは、最新の C# 言語機能を使用できます。これらのパターンにより、ボイラープレートコードを減らし、コードの可読性を高められます。
データモデルにレコードを使用する
// Immutable data models
public record Product(string Name, decimal Price, string Category);
// Records with computed properties
public record ProductViewModel(Product Product)
{
public string DisplayPrice => Product.Price.ToString("C");
public string Summary => $"{Product.Name} - {DisplayPrice}";
}
サービスでプライマリコンストラクターを使用する
// Service with primary constructor (C# 12)
public class ProductService(HttpClient httpClient, ILogger<ProductService> logger)
: IProductService
{
public async Task<IReadOnlyList<Product>> GetProductsAsync(CancellationToken ct)
{
logger.LogInformation("Fetching products");
var response = await httpClient.GetAsync("/products", ct);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<List<Product>>(ct) ?? [];
}
}
コンバーターでパターンマッチングを使用する
// Modern converter using pattern matching (C# 11+)
public class StatusToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value switch
{
OrderStatus.Pending => Brushes.Orange,
OrderStatus.Processing => Brushes.Blue,
OrderStatus.Shipped => Brushes.Green,
OrderStatus.Cancelled => Brushes.Red,
_ => Brushes.Gray
};
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> throw new NotSupportedException();
}
コレクション式
// C# 12 collection expressions
[ObservableProperty]
private ObservableCollection<Product> _products = [];
// In methods
List<string> categories = ["Electronics", "Clothing", "Books"];
テーマ設定
Fluent テーマ(.NET 9 以降)
.NET 9 では、モダンな Windows 11 スタイルの外観を実現する Fluent テーマが WPF に導入されます。角丸、更新されたコントロールテンプレート、Mica/Acrylic の背景効果のサポートが適用されます。
<Application x:Class="MyApp.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
ThemeMode="System"
StartupUri="MainWindow.xaml">
</Application>
または、コードビハインドで設定します。
// App.xaml.cs: set theme programmatically (.NET 9+)
Application.Current.ThemeMode = ThemeMode.System; // or ThemeMode.Light / ThemeMode.Dark
// Per-window theming is also supported
mainWindow.ThemeMode = ThemeMode.Dark;
ThemeMode の値:
None-- クラシックな WPF の外観(Fluent スタイルなし)Light-- 明るい色の Fluent テーマDark-- 暗い色の Fluent テーマSystem-- Windows のシステムのライト/ダークテーマ設定に従います
Fluent テーマに含まれるもの:
- ボタン、テキストボックス、リスト項目の角丸
- Windows 11 のデザイン言語に合わせた、新しいカラーパレット
- Mica および Acrylic の背景効果のサポート(Windows 11)
- Windows のシステム設定とのアクセントカラー連携
- システムテーマに従うダーク/ライトモード
システムテーマの検出
Windows のシステムのライト/ダークテーマを検出し、それに応じて動作させます。
// Detect system theme
public static bool IsDarkTheme()
{
using var key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(
@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize");
var value = key?.GetValue("AppsUseLightTheme");
return value is int i && i == 0;
}
// Listen for theme changes
SystemEvents.UserPreferenceChanged += (sender, args) =>
{
if (args.Category == UserPreferenceCategory.General)
{
// Theme may have changed; re-read and apply
ApplyTheme(IsDarkTheme() ? AppTheme.Dark : AppTheme.Light);
}
};
カスタムテーマ
.NET 9 より前のアプリや独自のブランド設定では、リソースディクショナリを使用します。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<SolidColorBrush x:Key="WindowBackground" Color="#1E1E1E" />
<SolidColorBrush x:Key="TextForeground" Color="#FFFFFF" />
<SolidColorBrush x:Key="AccentBrush" Color="#0078D7" />
</ResourceDictionary>
// Switch themes at runtime
public void ApplyTheme(AppTheme theme)
{
var themeUri = theme switch
{
AppTheme.Dark => new Uri("Themes/DarkTheme.xaml", UriKind.Relative),
AppTheme.Light => new Uri("Themes/LightTheme.xaml", UriKind.Relative),
_ => throw new ArgumentOutOfRangeException(nameof(theme))
};
Application.Current.Resources.MergedDictionaries.Clear();
Application.Current.Resources.MergedDictionaries.Add(
new ResourceDictionary { Source = themeUri });
}
エージェントの注意点
- .NET 8 以降のプロジェクトでは、.NET Framework の WPF パターンを使用しないでください。 DI に
App.configを使う方法(Host ビルダーを使用)、packages.config(PackageReferenceを使用)、ServiceLocatorパターン(コンストラクターインジェクションを使用)、AssemblyInfo.cs(<PropertyGroup>のプロパティを使用)は避けてください。 - 非推奨の WPF API を使用しないでください。
BitmapEffect(Effect/ShaderEffectに置き換え)、DrawingContext.PushEffect(削除済み)、ハードウェアアクセラレーションを無効にした状態でのVisualBrushのタイルモードは、いずれも古い機能です。 - MVVM Toolkit を使用する場合、
{Binding}と手動のINotifyPropertyChangedを併用しないでください。[ObservableProperty]のソースジェネレーターを一貫して使用してください。これらの方法を混在させると、微妙なバインディング更新の不具合が発生します。 - 非同期コードから
Dispatcher.Invokeを使用しないでください。 非同期メソッドでは、awaitが自動的に UI スレッドへ戻します(既定のConfigureAwait(true)の動作)。タイマー、COM コールバック、ネイティブ相互運用など、非同期でないコンテキストでは、引き続きDispatcher.Invoke/BeginInvokeが適切です。 - WPF アプリでは
TrimMode=fullを設定しないでください。 WPF は XAML のリフレクションを広範に使用します。TrimMode=partialを使用し、トリミング後にすべてのビューをテストして、型の欠落がないことを確認してください。 - Host ビルダーのライフサイクルを忘れないでください。
OnStartupで_host.StartAsync()を呼び出し、OnExitで_host.StopAsync()を呼び出してください。ライフサイクル管理を忘れると、DI に登録されたIHostedServiceインスタンスが開始も停止もされません。 - Fluent テーマを使用する場合、色をハードコードしないでください。 テーマリソース(
{DynamicResource SystemAccentColor})を参照し、ライト/ダークモードやシステムのアクセントカラーの変更との互換性を保ってください。
前提条件
- Windows デスクトップ ワークロードを含む .NET 8.0 以降
- TFM:
net8.0-windows(WPF では Windows SDK バージョンは不要です) - Visual Studio 2022 以降、C# Dev Kit を使用した VS Code、または JetBrains Rider
- Fluent テーマを使用する場合:.NET 9 以降