👋
CommunityToolkit.Mvvmを使用したWPF MVVMパターン発展的実装ガイド
1. はじめに
基本的なMVVMパターンに以下の発展的な機能を追加する方法を説明します:
- バリデーション機能
- コマンドパターン
- 非同期処理
- エラーハンドリング
- ユーザー入力の検証
2. プロジェクトのセットアップ
2.1 必要なパッケージ
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
2.2 基本的なプロジェクト構成
MyApp/
├── Models/
│ ├── UserModel.cs
│ └── ValidationResult.cs
├── ViewModels/
│ ├── ViewModelBase.cs
│ └── MainWindowViewModel.cs
├── Views/
│ └── MainWindow.xaml
└── App.xaml
3. バリデーション機能の実装
3.1 ValidationResult モデル
public class ValidationResult
{
public bool IsValid { get; set; }
public Dictionary<string, string> Errors { get; set; } = new();
}
3.2 バリデーション属性の使用
public partial class MainWindowViewModel : ObservableValidator
{
[ObservableProperty]
[NotifyDataErrorInfo]
[Required(ErrorMessage = "Name is required")]
[MinLength(2, ErrorMessage = "Name must be at least 2 characters")]
private string _name;
[ObservableProperty]
[NotifyDataErrorInfo]
[Required(ErrorMessage = "Email is required")]
[EmailAddress(ErrorMessage = "Invalid email format")]
private string _email;
}
4. コマンドパターンの実装
4.1 基本的なコマンド
public partial class MainWindowViewModel : ObservableValidator
{
[RelayCommand]
private void SaveUser()
{
if (HasErrors) return;
// 保存処理
}
}
4.2 非同期コマンド
public partial class MainWindowViewModel : ObservableValidator
{
[RelayCommand(CanExecute = nameof(CanSaveUser))]
private async Task SaveUserAsync()
{
try
{
IsBusy = true;
await _userService.SaveUserAsync(new UserModel
{
Name = Name,
Email = Email
});
StatusMessage = "保存が完了しました";
}
catch (Exception ex)
{
StatusMessage = $"エラーが発生しました: {ex.Message}";
}
finally
{
IsBusy = false;
}
}
private bool CanSaveUser()
{
return !HasErrors && !IsBusy;
}
}
5. エラーハンドリング
5.1 グローバルエラーハンドリング
public static class ExceptionHandler
{
public static async Task HandleAsync(Exception ex, string operation = "")
{
var message = $"操作 '{operation}' でエラーが発生しました。\n{ex.Message}";
await Application.Current.Dispatcher.InvokeAsync(() =>
{
MessageBox.Show(message, "エラー",
MessageBoxButton.OK, MessageBoxImage.Error);
});
// ログ記録
Debug.WriteLine($"[ERROR] {DateTime.Now}: {message}");
}
}
5.2 ViewModelでのエラーハンドリング
public partial class MainWindowViewModel : ObservableValidator
{
[RelayCommand]
private async Task SaveUserAsync()
{
try
{
await ValidateAsync();
if (HasErrors) return;
IsBusy = true;
await _userService.SaveUserAsync(new UserModel
{
Name = Name,
Email = Email
});
}
catch (ValidationException ex)
{
SetError(ex.Message, nameof(Name));
}
catch (Exception ex)
{
await ExceptionHandler.HandleAsync(ex, "ユーザー保存");
}
finally
{
IsBusy = false;
}
}
}
6. ユーザー入力の検証
6.1 リアルタイムバリデーション
public partial class MainWindowViewModel : ObservableValidator
{
partial void OnNameChanged(string value)
{
ValidateProperty(value, nameof(Name));
UpdateCanExecuteCommand();
}
partial void OnEmailChanged(string value)
{
ValidateProperty(value, nameof(Email));
UpdateCanExecuteCommand();
}
private void UpdateCanExecuteCommand()
{
SaveUserCommand.NotifyCanExecuteChanged();
}
}
6.2 カスタムバリデーションルール
public partial class MainWindowViewModel : ObservableValidator
{
[CustomValidation(typeof(MainWindowViewModel), nameof(ValidatePassword))]
[ObservableProperty]
private string _password;
public static ValidationResult ValidatePassword(string password)
{
var errors = new List<string>();
if (string.IsNullOrEmpty(password))
errors.Add("パスワードは必須です");
else
{
if (password.Length < 8)
errors.Add("パスワードは8文字以上必要です");
if (!password.Any(char.IsUpper))
errors.Add("パスワードは大文字を含む必要があります");
if (!password.Any(char.IsLower))
errors.Add("パスワードは小文字を含む必要があります");
if (!password.Any(char.IsDigit))
errors.Add("パスワードは数字を含む必要があります");
}
return new ValidationResult
{
IsValid = !errors.Any(),
Errors = errors.ToDictionary(e => nameof(Password), e => e)
};
}
}
7. ビューの実装
7.1 MainWindow.xaml
<Window x:Class="MyApp.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="User Registration" Height="450" Width="800">
<Grid Margin="20">
<StackPanel>
<TextBox Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"
Margin="0,5"/>
<TextBlock Text="{Binding (Validation.Errors)[0].ErrorContent,
RelativeSource={RelativeSource AncestorType=TextBox}}"
Foreground="Red"/>
<TextBox Text="{Binding Email, UpdateSourceTrigger=PropertyChanged}"
Margin="0,5"/>
<TextBlock Text="{Binding (Validation.Errors)[0].ErrorContent,
RelativeSource={RelativeSource AncestorType=TextBox}}"
Foreground="Red"/>
<Button Content="Save"
Command="{Binding SaveUserCommand}"
IsEnabled="{Binding !IsBusy}"
Margin="0,20"/>
<TextBlock Text="{Binding StatusMessage}"
Foreground="{Binding HasErrors,
Converter={StaticResource ErrorColorConverter}}"/>
</StackPanel>
</Grid>
</Window>
8. ベストプラクティス
-
依存性の分離
- ViewModelは直接のデータアクセスを避け、サービスを通じて行う
- インターフェースを使用して疎結合を維持
-
状態管理
- IsBusyフラグで処理中の状態を管理
- StatusMessageで現在の状態をユーザーに通知
-
バリデーション
- 入力時のリアルタイムバリデーション
- 保存前の完全性チェック
- カスタムバリデーションルールの実装
-
エラーハンドリング
- 適切な例外処理
- ユーザーフレンドリーなエラーメッセージ
- デバッグ情報のログ記録
-
コマンド
- 非同期操作の適切な処理
- CanExecute条件の適切な更新
- コマンドの実行状態の管理
9. まとめ
CommunityToolkit.Mvvmを使用することで、以下のような利点が得られます:
- コードの可読性と保守性の向上
- 堅牢なバリデーション機能
- 効率的な状態管理
- 適切なエラーハンドリング
- ユーザーフレンドリーなインターフェース
Discussion