はじめに
この記事では、最新の**.NET 9.0とC#13プレビュー機能**を使用して、CommunityToolkit.MvvmによるモダンなWPFアプリケーションの作成方法を紹介します。
従来のMVVMパターンと比較して、CommunityToolkit.Mvvmを使用することで大幅にコード量を削減し、保守性の高いアプリケーションを構築できます。
完成品
シンプルですが、以下の要素を含む堅牢なWPFアプリケーションを作成します:
- ✨ C#13部分プロパティを活用したViewModel
- 🚀 DryIocによる高性能DI
- 📝 NLogによる企業級ログ
- 🛡️ 包括的な例外ハンドリング
- ⚙️ appsettings.jsonによる設定管理
技術スタック
技術 | バージョン | 用途 |
---|---|---|
.NET | 9.0 | フレームワーク |
C# | 13.0 (preview) | 言語機能 |
WPF | - | UIフレームワーク |
CommunityToolkit.Mvvm | 8.4.0 | MVVMパターン |
DryIoc | 6.2.0 | DIコンテナ |
NLog | 6.0.4 | ログ |
Microsoft.Extensions.* | 9.0.9 | 設定・ログ基盤 |
プロジェクト構成
WpfExperimental1/
├── App.xaml # アプリケーション定義
├── App.xaml.cs # DIコンテナ設定
├── appsettings.json # 設定ファイル
├── ViewModels/
│ └── MainWindowViewModel.cs # メインViewModel
├── Views/
│ ├── MainWindow.xaml # メインビュー
│ └── MainWindow.xaml.cs # コードビハインド
└── WpfExperimental1.csproj # プロジェクトファイル
ステップ1: プロジェクトファイルの設定
まず、必要なパッケージを含むプロジェクトファイルを作成します。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<LangVersion>preview</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.9" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.135" />
<PackageReference Include="NLog.Extensions.Logging" Version="6.0.4" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
🔑 ポイント
-
LangVersion:
preview
でC#13の最新機能を有効化 -
TargetFramework:
net9.0-windows
で最新の.NET使用 -
Nullable:
enable
で型安全性を向上
ステップ2: ViewModelの実装
CommunityToolkit.MvvmとC#13の部分プロパティを活用したViewModelを作成します。
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
namespace WpfExperimental1.ViewModels;
public partial class MainWindowViewModel : ObservableObject
{
private readonly ILogger<MainWindowViewModel> _logger;
[ObservableProperty]
public partial string Title { get; set; } = "Hello, World!";
public MainWindowViewModel(ILogger<MainWindowViewModel> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
[RelayCommand]
private void ClickMe()
{
try
{
Title = "You clicked me!";
_logger.LogInformation("Title changed to: {Title}", Title);
}
catch (Exception ex)
{
_logger.LogError(ex, "ClickMeコマンドの実行中にエラーが発生しました");
// UIスレッドで安全にメッセージを表示
System.Windows.Application.Current?.Dispatcher.Invoke(() =>
{
System.Windows.MessageBox.Show($"操作中にエラーが発生しました: {ex.Message}",
"エラー",
System.Windows.MessageBoxButton.OK,
System.Windows.MessageBoxImage.Warning);
});
}
}
}
🚀 CommunityToolkit.Mvvmの威力
従来のINotifyPropertyChanged実装
// 従来の書き方(約15行)
private string _title = "Hello, World!";
public string Title
{
get => _title;
set
{
if (_title != value)
{
_title = value;
OnPropertyChanged();
}
}
}
CommunityToolkit.Mvvm + C#13
// 新しい書き方(1行!)
[ObservableProperty]
public partial string Title { get; set; } = "Hello, World!";
📝 ポイント解説
-
[ObservableProperty]
: プロパティ変更通知を自動生成 -
public partial
: C#13の部分プロパティ機能 -
[RelayCommand]
: ICommandの実装を自動生成 - コンストラクタインジェクション: DIコンテナからLogger注入
- 例外処理: UIスレッドセーフなエラーハンドリング
ステップ3: Viewの実装
WPFのXAMLとコードビハインドを実装します。
MainWindow.xaml
<Window x:Class="WpfExperimental1.Views.MainWindow"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfExperimental1.Views"
xmlns:vm="clr-namespace:WpfExperimental1.ViewModels"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<StackPanel.Resources>
<!-- 統一スタイル設定 -->
<Style TargetType="Control">
<Setter Property="Margin" Value="10,8,10,8"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
</Style>
<Style TargetType="TextBlock">
<Setter Property="Margin" Value="10,8,10,8"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
</Style>
</StackPanel.Resources>
<TextBlock Text="{Binding Title}" FontSize="24"/>
<Button Content="Click Me" Width="100" Height="30"
Command="{Binding ClickMeCommand}"/>
</StackPanel>
</Window>
MainWindow.xaml.cs
using System.Windows;
using WpfExperimental1.ViewModels;
namespace WpfExperimental1.Views;
public partial class MainWindow : Window
{
public MainWindow(MainWindowViewModel viewModel)
{
try
{
InitializeComponent();
DataContext = viewModel;
}
catch (Exception ex)
{
MessageBox.Show($"ViewModelの初期化中にエラーが発生しました: {ex.Message}",
"初期化エラー",
MessageBoxButton.OK,
MessageBoxImage.Error);
throw;
}
}
}
🎨 XAMLのポイント
-
データバインディング:
{Binding Title}
でViewModelと連携 -
コマンドバインディング:
{Binding ClickMeCommand}
で自動生成コマンド使用 - 暗黙的スタイル: 統一されたUI設計
- DIインジェクション: コンストラクタでViewModelを受け取り
ステップ4: アプリケーション起動とDI設定
App.xaml.csでDIコンテナの設定と例外ハンドリングを実装します。
using System.Windows;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
using NLog.Extensions.Logging;
using WpfExperimental1.ViewModels;
using WpfExperimental1.Views;
using DryIoc;
using DryIoc.Microsoft.DependencyInjection;
namespace WpfExperimental1;
public partial class App : Application
{
private IServiceProvider _serviceProvider = null!;
private IConfiguration _configuration = null!;
public App()
{
// グローバル例外ハンドラーを設定
this.DispatcherUnhandledException += App_DispatcherUnhandledException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
}
protected override void OnStartup(StartupEventArgs e)
{
try
{
base.OnStartup(e);
// 1. Configuration の構築
BuildConfiguration();
// 2. DI Container の構築
BuildServiceProvider();
// 3. MainWindow の表示
var mainWindow = _serviceProvider.GetRequiredService<MainWindow>();
mainWindow?.Show();
}
catch (Exception ex)
{
MessageBox.Show($"アプリケーションの起動中にエラーが発生しました: {ex.Message}",
"エラー",
MessageBoxButton.OK,
MessageBoxImage.Error);
Current?.Shutdown();
}
}
private void BuildConfiguration()
{
var builder = new ConfigurationBuilder()
.SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
// 環境変数があれば環境固有の設定ファイルも読み込み
var environment = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? "Production";
builder.AddJsonFile($"appsettings.{environment}.json", optional: true, reloadOnChange: true);
_configuration = builder.Build();
}
private void BuildServiceProvider()
{
var services = new ServiceCollection();
// Configuration をサービスに登録
services.AddSingleton<IConfiguration>(_configuration);
// Logging の設定
services.AddLogging(builder =>
{
builder.ClearProviders();
builder.SetMinimumLevel(LogLevel.Trace);
builder.AddNLog();
});
// DryIocコンテナの設定
var container = new Container(rules => rules.With(propertiesAndFields: PropertiesAndFields.Auto))
.WithDependencyInjectionAdapter(services);
// ViewModelsとViewsの登録
container.Register<MainWindowViewModel>(Reuse.Singleton);
container.Register<MainWindow>(Reuse.Transient);
_serviceProvider = container.BuildServiceProvider();
}
private void App_DispatcherUnhandledException(object sender,
System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
string errorMessage = $"予期しないエラーが発生しました:\n{e.Exception.Message}\n\nアプリケーションを続行しますか?";
var result = MessageBox.Show(errorMessage, "エラー", MessageBoxButton.YesNo, MessageBoxImage.Error);
if (result == MessageBoxResult.Yes)
{
e.Handled = true;
}
else
{
Current?.Shutdown();
}
}
private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
string errorMessage = $"致命的なエラーが発生しました:\n{e.ExceptionObject}";
MessageBox.Show(errorMessage, "致命的エラー", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
⚙️ DIコンテナ設定のポイント
- DryIoc選択理由: .NET最速クラスの性能
- Microsoft.Extensions統合: 標準的なサービス登録
- ライフサイクル管理: Singleton/Transientの適切な使い分け
- 例外ハンドリング: アプリケーション全体の安全性確保
ステップ5: 設定ファイル
appsettings.jsonで外部設定を管理します。
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"AppSettings": {
"Title": "Hello, WPF with .NET 9 and MVVM!"
},
"NLog": {
"internalLogLevel": "Warn",
"internalLogFile": "c:\\temp\\nlog-internal.log",
"targets": {
"async": true,
"logfile": {
"type": "File",
"fileName": "logs/app-${shortdate}.log",
"layout": "${longdate} ${level:uppercase=true} ${logger} ${message} ${exception:format=tostring}"
},
"console": {
"type": "Console",
"layout": "${time} [${level:uppercase=true}] ${message} ${exception:format=tostring}"
}
},
"rules": [
{
"logger": "*",
"minLevel": "Info",
"writeTo": "logfile,console"
}
]
}
}
実行結果
アプリケーションを実行すると:
- 起動時: "Hello, World!" が表示
- ボタンクリック: テキストが "You clicked me!" に変更
- ログ出力: NLogによりファイルとコンソールに出力
- 例外処理: エラー発生時の適切なメッセージ表示
コード量比較
従来のMVVMパターン
総行数: 約150行
- ViewModel: 約70行(PropertyChanged実装含む)
- Command: 約30行(ICommand実装)
- その他: 約50行
CommunityToolkit.Mvvm使用
総行数: 約40行
- ViewModel: 約40行(注記含む)
- Command: 0行(自動生成)
- 削減率: 約73%削減!
パフォーマンス比較
項目 | 従来実装 | CommunityToolkit.Mvvm |
---|---|---|
コンパイル時間 | 標準 | 軽微な増加(Source Generator) |
実行時性能 | 標準 | 同等またはそれ以上 |
メモリ使用量 | 標準 | 最適化済み |
起動時間 | 標準 | DryIocにより高速化 |
まとめ
✅ 達成できたこと
- 大幅なコード削減: 従来比73%削減
- 型安全性向上: C#13 + nullable参照型
- 企業級品質: ログ・例外処理・DI完備
- 最新技術活用: .NET 9.0 + C#13プレビュー
- 高パフォーマンス: DryIocによる最適化
🚀 CommunityToolkit.Mvvmのメリット
- 生産性向上: ボイラープレートコード削減
- 保守性向上: シンプルで読みやすいコード
- バグ削減: 自動生成による人的ミス防止
- 標準準拠: Microsoft推奨のベストプラクティス
- 性能最適化: Source Generatorによる最適化
📚 学習リソース
次回予告
次回は、このベースプロジェクトに以下の機能を追加する予定です:
- 🧪 単体テスト (xUnit + Moq)
- 🎨 Material Design UI
- 🌐 多言語対応 (i18n)
- 📊 データバインディング高度技法
お楽しみに!
タグ: #CSharp #WPF #MVVM #DotNet9 #CommunityToolkit