mcpField
D
dotnet-wpf-modern

dotnet-wpf-modern

github.com/wshaddix開発86312 回インストール35 週間前に更新
リポジトリ

dotnet-wpf-modern について

dotnet-wpf-modernは、.NET 8+のWPFを扱うコーディングエージェントや開発者向けのSkillです。Host builder、CommunityToolkit.Mvvm、パフォーマンス、最新のC#、.NET 9+のFluentテーマを扱い、実装時の注意点も解説します。

主な機能

  1. 01.NET 8+のHost builderと依存性注入
  2. 02CommunityToolkit.Mvvmのソースジェネレーター
  3. 03描画・起動・メモリに関する指針
  4. 04最新のC#パターン
  5. 05.NET 9+のFluentテーマとシステムテーマ

活用シーン

  1. 01WPFアプリにHost builderを設定
  2. 02MVVM Toolkitで定型コードを削減
  3. 03Fluentテーマやシステムの配色を設定
  4. 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 });
}

エージェントの注意点

  1. .NET 8 以降のプロジェクトでは、.NET Framework の WPF パターンを使用しないでください。 DI に App.config を使う方法(Host ビルダーを使用)、packages.config(PackageReference を使用)、ServiceLocator パターン(コンストラクターインジェクションを使用)、AssemblyInfo.cs(<PropertyGroup> のプロパティを使用)は避けてください。
  2. 非推奨の WPF API を使用しないでください。 BitmapEffect(Effect/ShaderEffect に置き換え)、DrawingContext.PushEffect(削除済み)、ハードウェアアクセラレーションを無効にした状態での VisualBrush のタイルモードは、いずれも古い機能です。
  3. MVVM Toolkit を使用する場合、{Binding} と手動の INotifyPropertyChanged を併用しないでください。 [ObservableProperty] のソースジェネレーターを一貫して使用してください。これらの方法を混在させると、微妙なバインディング更新の不具合が発生します。
  4. 非同期コードから Dispatcher.Invoke を使用しないでください。 非同期メソッドでは、await が自動的に UI スレッドへ戻します(既定の ConfigureAwait(true) の動作)。タイマー、COM コールバック、ネイティブ相互運用など、非同期でないコンテキストでは、引き続き Dispatcher.Invoke/BeginInvoke が適切です。
  5. WPF アプリでは TrimMode=full を設定しないでください。 WPF は XAML のリフレクションを広範に使用します。TrimMode=partial を使用し、トリミング後にすべてのビューをテストして、型の欠落がないことを確認してください。
  6. Host ビルダーのライフサイクルを忘れないでください。 OnStartup で _host.StartAsync() を呼び出し、OnExit で _host.StopAsync() を呼び出してください。ライフサイクル管理を忘れると、DI に登録された IHostedService インスタンスが開始も停止もされません。
  7. 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 以降

参考資料

コメント