👋

CommunityToolkit.Mvvmを使用したWPF MVVMパターン発展的実装ガイド

2024/11/18に公開

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;
        }
    }
    public async Task ValidateAsync()
    {
        // すべてのプロパティをバリデーション
        await Task.Run(() =>
        {
            ValidateAllProperties();
        });
    }

}

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. ベストプラクティス

  1. 依存性の分離

    • ViewModelは直接のデータアクセスを避け、サービスを通じて行う
    • インターフェースを使用して疎結合を維持
  2. 状態管理

    • IsBusyフラグで処理中の状態を管理
    • StatusMessageで現在の状態をユーザーに通知
  3. バリデーション

    • 入力時のリアルタイムバリデーション
    • 保存前の完全性チェック
    • カスタムバリデーションルールの実装
  4. エラーハンドリング

    • 適切な例外処理
    • ユーザーフレンドリーなエラーメッセージ
    • デバッグ情報のログ記録
  5. コマンド

    • 非同期操作の適切な処理
    • CanExecute条件の適切な更新
    • コマンドの実行状態の管理

9. まとめ

CommunityToolkit.Mvvmを使用することで、以下のような利点が得られます:

  • コードの可読性と保守性の向上
  • 堅牢なバリデーション機能
  • 効率的な状態管理
  • 適切なエラーハンドリング
  • ユーザーフレンドリーなインターフェース

Discussion