C# WPFにおけるFluentValidationの応用

C# WPFにおけるFluentValidationの応用

この記事では、C# WPFプロジェクトでFluentValidationを使用してプロパティ検証を行う方法と、MVVMパターンを通じてこの機能を実装する方法について詳しく説明します。

最終更新 2024/01/25 5:17
沙漠尽头的狼
読了目安 14 分
カテゴリ
WPF
タグ
.NET C# WPF MVVM FluentValidation

1. はじめに

.NET開発の分野において、FluentValidationはその優雅さと拡張性の高さから、プロパティ検証を行う開発者にとって第一選択のツールとなっています。Web開発(MVC、Web API、ASP.NET CORE)だけでなく、WPFアプリケーションにも完璧に統合でき、強力なデータ検証機能を提供します。この記事では、C# WPFプロジェクトでFluentValidationを使用してプロパティ検証を行い、MVVMパターンを通じてこの機能を実現する方法について詳しく説明します。

2. 機能概要

私たちの目標は、FluentValidationを使用して以下の検証機能を実現するWPFアプリケーションを構築することです。

  1. ViewModel層の基本的なデータ型プロパティ(int、stringなど)を検証します。
  2. ViewModel内の複雑なプロパティ(オブジェクトプロパティのサブプロパティやコレクションプロパティを含む)を検証します。
  3. ユーザーエクスペリエンスを向上させるために、2種類の直感的なエラー表示スタイルを提供します。

まず、実装結果の画像をご覧ください。

3. 問題解決と探求

調査の過程で、FluentValidationの公式ドキュメントは主にWebアプリケーションの検証に焦点を当てていることがわかりました。WPFや複雑なプロパティの検証に関する公式ドキュメントの例は限られています。しかし、深く研究し実践することで、FluentValidationをWPFと組み合わせて使用する効果的な方法、特に複雑なプロパティの検証方法を見つけました。

4. 開発手順

4.1. プロジェクトの作成、ライブラリの導入

まず、新しいWPFプロジェクトを作成し、プロパティ検証のためにFluentValidationライブラリを、MVVMパターンの実装を簡素化するためにPrism.Wpfライブラリを導入します。

<ItemGroup>
  <PackageReference Include="FluentValidation" Version="11.9.0" />
  <PackageReference Include="Prism.Wpf" Version="9.0.271-pre" />
</ItemGroup>

4.2. エンティティクラスの作成

2つのエンティティクラス(StudentとField)を作成しました。これらはそれぞれオブジェクトプロパティとコレクション項目プロパティを表します。これらのクラスは両方ともIDataErrorInfoインターフェースを実装しています。

  1. IDataErrorInfoインターフェースは、エンティティデータ検証のエラーメッセージを提供するためによく使用されます。このインターフェースには、インデクサー(this[string columnName])とErrorプロパティの2つのメンバーが含まれています。インデクサーはプロパティ名ごとにエラーメッセージを提供するために使用され、Errorプロパティはエンティティ全体のエラーの概要を提供します。
  2. 2つのエンティティクラスと、後述するViewModelもIDataErrorInfoインターフェースを実装し、this[string columnName]インデクサーとErrorプロパティ内でFluentValidationを使用してプロパティを検証します。

4.2.1. 通常クラス - Student

Studentクラスには5つのプロパティ(名前、年齢、郵便番号、最小値、最大値)が含まれています。最小値と最大値は関連検証に関係しており、つまり最小値が変更されたら最大値の検証に通知し、その逆も同様です。

/// <summary>
/// 学生エンティティ
/// BindableBaseを継承。つまり、プロパティ変更インターフェースINotifyPropertyChangedを継承
/// IDataErrorInfoインターフェースを実装。FluentValidation検証のために必須
/// </summary>
public class Student : BindableBase, IDataErrorInfo
{
    private int _age;
    private string? _name;
    private string? _zip;
    private readonly StudentValidator _validator = new();

    public string? Name
    {
        get => _name;
        set => SetProperty(ref _name, value);
    }

    public int Age
    {
        get => _age;
        set => SetProperty(ref _age, value);
    }

    public string? Zip
    {
        get => _zip;
        set => SetProperty(ref _zip, value);
    }

    private int _minValue;

    public int MinValue
    {
        get => _minValue;
        set
        {
            SetProperty(ref _minValue, value);

            // 最大値の検証を関連更新
            RaisePropertyChanged(nameof(MaxValue));
        }
    }

    private int _maxValue;

    public int MaxValue
    {
        get => _maxValue;
        set
        {
            SetProperty(ref _maxValue, value);

            // 最小値の検証を関連更新
            RaisePropertyChanged(nameof(MinValue));
        }
    }

    public string this[string columnName]
    {
        get
        {
            var validateResult = _validator.Validate(this);
            if (validateResult.IsValid)
            {
                return string.Empty;
            }

            var firstOrDefault =
                validateResult.Errors.FirstOrDefault(error => error.PropertyName == columnName);
            return firstOrDefault == null ? string.Empty : firstOrDefault.ErrorMessage;
        }
    }

    public string Error
    {
        get
        {
            var validateResult = _validator.Validate(this);
            if (validateResult.IsValid)
            {
                return string.Empty;
            }

            var errors = string.Join(Environment.NewLine, validateResult.Errors.Select(x => x.ErrorMessage).ToArray());
            return errors;
        }
    }
}

上記の重要なコードはpublic string this[string columnName]です。ここで入力フォーム項目のデータ検証が行われます。FluentValidationの呼び出しはここで行われ、検証ロジックはStudentValidatorにカプセル化されています。フォーム入力時にこのコードがリアルタイムで呼び出され、columnNameはフォーム項目の列名、つまりViewにバインドされているプロパティ名です。

4.2.2. コレクションクラス - Field

このクラスはViewModel内のコレクション項目として使用され、動的なフォームデータ検証をシミュレートします。4つのプロパティ(フィールド名、フィールド表示名、データ型、データ値)をシンプルに含み、フォームは主にデータ型に基づいて入力されたデータ値が有効かどうかを検証します。同様に、このエンティティはFluentValidation検証をトリガーするためにIDataErrorInfoインターフェースを継承する必要があります。

/// <summary>
/// 拡張フィールド。動的フォーム生成に使用
/// BindableBaseを継承。つまり、プロパティ変更インターフェースINotifyPropertyChangedを継承
/// IDataErrorInfoインターフェースを実装。FluentValidation検証のために必須
/// </summary>
public class Field : BindableBase, IDataErrorInfo
{
    private string? _value;
    private readonly FieldValidator _validator = new();


    public Field(DataType type, string typeLabel, string name, string value)
    {
        Type = type;
        TypeLabel = typeLabel;
        Name = name;
        Value = value;
    }

    /// <summary>
    /// データ型
    /// </summary>
    public DataType Type { get; set; }

    /// <summary>
    /// データ型名
    /// </summary>
    public string TypeLabel { get; set; }

    /// <summary>
    /// 名前
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// 値
    /// </summary>
    public string? Value
    {
        get => _value;
        set => SetProperty(ref _value, value);
    }

    public string this[string columnName]
    {
        get
        {
            var validateResult = _validator.Validate(this);
            if (validateResult.IsValid)
            {
                return string.Empty;
            }

            var firstOrDefault =
                validateResult.Errors.FirstOrDefault(error => error.PropertyName == columnName);
            return firstOrDefault == null ? string.Empty : firstOrDefault.ErrorMessage;
        }
    }

    public string Error
    {
        get
        {
            var validateResult = _validator.Validate(this);
            if (validateResult.IsValid)
            {
                return string.Empty;
            }

            var errors = string.Join(Environment.NewLine, validateResult.Errors.Select(x => x.ErrorMessage).ToArray());
            return errors;
        }
    }
}

public enum DataType
{
    Text,
    Number,
    Date
}

上記のコードを見ると、public string this[string columnName]の記述はStudentクラスと同じですが、_validator変数の型が異なります。前者はStudentValidator、ここではFieldValidatorです。次に、これらの2つのクラスの書き方を見てみましょう。

4.3. バリデーターの作成

各エンティティクラスに対して、対応するバリデータークラス(StudentValidatorFieldValidator)を作成しました。これらのバリデータークラスはAbstractValidatorを継承し、その中で検証ルールを定義します。検証プロパティの記述方法には2つあります。

  1. エンティティプロパティの上に属性を追加する方法(この記事では特に説明しません。多くの記事で紹介されています)。
  2. コードで追加する方法。以下のように、バリデータークラスを作成し、AbstractValidatorを継承し、このバリデーターのコンストラクターでルールを記述してプロパティを検証します。管理が容易です。

この記事では2番目の方法を使用し、StudentValidatorFieldValidatorの2つのバリデータークラスを作成して説明します。

4.3.1. StudentValidator

以下は学生バリデーターStudentValidatorです。AbstractValidatorを継承し、ジェネリック型には検証対象のエンティティクラスStudentを指定します。

public class StudentValidator : AbstractValidator<Student>
{
    public StudentValidator()
    {
        RuleFor(vm => vm.Name)
            .NotEmpty()
            .WithMessage("学生名を入力してください!")
            .Length(5, 30)
            .WithMessage("学生名は5~30文字の範囲で入力してください。");

        RuleFor(vm => vm.Age)
            .GreaterThanOrEqualTo(0)
            .WithMessage("学生の年齢は整数で入力してください!")
            .ExclusiveBetween(10, 150)
            .WithMessage("学生の年齢を正しく入力してください(10~150)");

        _ = RuleFor(vm => vm.Zip)
            .NotEmpty()
            .WithMessage("郵便番号は必須です!")
            .Must(BeAValidZip)
            .WithMessage("郵便番号は6桁の数字で構成されています。");

        RuleFor(model => model.MinValue).Must((model, minValue) => minValue < model.MaxValue).WithMessage("最小値は最大値より小さくなければなりません");

        RuleFor(model => model.MaxValue).Must((model, maxValue) => maxValue > model.MinValue).WithMessage("最大値は最小値より大きくなければなりません");
    }

    private static bool BeAValidZip(string? zip)
    {
        if (string.IsNullOrEmpty(zip))
        {
            return false;
        }

        var regex = new Regex(@"\d{6}");
        return regex.IsMatch(zip);
    }
}

コードはシンプルで、数値の大小と範囲検証(Age参照)、文字列の必須入力と長さ制限(Name参照)、文字列の正規表現検証(Zip参照)、複数プロパティの関連検証(最小値と最大値。ここではプロパティのset時に他のプロパティの検証通知RaisePropertyChanged(nameof(MaxValue));と連携)を使用しています。

4.3.2. FieldValidator

動的フォームデータ値バリデーターです。同様にAbstractValidatorを継承し、ジェネリック型には検証対象のエンティティクラスFieldを指定します。

public class FieldValidator : AbstractValidator<Field>
{
    public FieldValidator()
    {
        RuleFor(field => field.Value)
            .Must((field, value) => (field.Type == DataType.Text && !string.IsNullOrWhiteSpace(value))
                                    || (field.Type == DataType.Number && double.TryParse(value, out _))
                                    || (field.Type == DataType.Date && DateTime.TryParse(value, out _)))
            .WithMessage("1.テキストは空にできません。2.数値型は数値を入力してください。3.日付型は日付形式を入力してください");
    }
}

ここでは簡略化しています。

  1. テキストデータ型の場合、値は必須です。
  2. 数値データ型の場合、double型である必要があります。
  3. 日付型の場合、DateTimeで変換できる必要があります。

この記事では簡単なデモのみを行い、複数のデータ型をMustメソッド内で一括検証し、検証エラーが発生した場合に統一されたエラーメッセージを表示します。実際の状況に応じて変更してください。

4.3.3. StudentViewModelValidator

さらに、ViewModel層のプロパティを検証するためのStudentViewModelValidatorも作成しました。このバリデーターは、基本データ型、オブジェクトプロパティ、コレクションプロパティの検証を処理できます。

public class StudentViewModelValidator : AbstractValidator<StudentViewModel>
{
    public StudentViewModelValidator()
    {
        RuleFor(vm => vm.Title)
            .NotEmpty()
            .WithMessage("タイトルは必須です!")
            .Length(5, 30)
            .WithMessage("タイトルは5~30文字の範囲で入力してください。");

        RuleFor(vm => vm.CurrentStudent).SetValidator(new StudentValidator());

        RuleForEach(vm => vm.Fields).SetValidator(new FieldValidator());
    }
}
  1. Titleは基本データ型(string型)の検証に関連付けられています。
  2. CurrentStudentはオブジェクトプロパティ(Studentクラスのインスタンス)を検証するために使用され、このプロパティの検証時にはStudentValidatorバリデーターを使用します。
  3. Fieldsはコレクションプロパティ(ObservableCollection<Field>)を検証するために使用され、このプロパティの各項目の検証時にはFieldValidatorバリデーターを使用します。RuleForEachはコレクション内の各項目に対するバリデーターを関連付けるために使用されていることに注意してください。

4.4. ViewModel層の実装

StudentViewModelStudentエンティティクラスと同様の構造を持ち、IDataErrorInfoインターフェースを実装する必要があります。このクラスは、シンプルなstringプロパティ(Title)、複雑なStudentオブジェクトプロパティ(CurrentStudent)、コレクションプロパティ(ObservableCollection<Field> Fields)で構成されています。コードは以下の通りです。

/// <summary>
/// ビューViewModel
/// BindableBaseを継承。つまり、プロパティ変更インターフェースINotifyPropertyChangedを継承
/// IDataErrorInfoインターフェースを実装。FluentValidation検証のために必須
/// </summary>
public class StudentViewModel : BindableBase, IDataErrorInfo
{
    private Student _currentStudent;
    private string _title;

    private readonly StudentViewModelValidator _validator;

    public string Title
    {
        get => _title;
        set => SetProperty(ref _title, value);
    }

    public Student CurrentStudent
    {
        get => _currentStudent;
        set => SetProperty(ref _currentStudent, value);
    }

    public ObservableCollection<Field> Fields { get; } = new();

    private DelegateCommand _saveCommand;

    public DelegateCommand SaveCommand => _saveCommand ??= new DelegateCommand(HandleSaveCommand,
        HandleCanExecuteSaveCommand);

    private DelegateCommand _cancelCommand;

    public DelegateCommand CancelCommand =>
        _cancelCommand ??= new DelegateCommand(HandleCancelCommand, () => true);

    public StudentViewModel()
    {
        _validator = new StudentViewModelValidator();
        CurrentStudent = new Student
        {
            Name = "李剛の子",
            Age = 23
        };
        Fields.Add(new Field(DataType.Text, "テキスト(例:四川省成都市)", "住所", ""));
        Fields.Add(new Field(DataType.Number, "数値(例:12)", "勤続年数", ""));
        Fields.Add(new Field(DataType.Date, "日時(例:2023-09-26 05:13:23)", "研修日時", ""));

        PropertyChanged += Validate;
        CurrentStudent.PropertyChanged += Validate;
        foreach (var field in Fields)
        {
            field.PropertyChanged += Validate;
        }
    }

    ~StudentViewModel()
    {
        PropertyChanged -= Validate;
        CurrentStudent.PropertyChanged -= Validate;
        foreach (var field in Fields)
        {
            field.PropertyChanged -= Validate;
        }
    }

    private void Validate(object sender, PropertyChangedEventArgs e)
    {
        _isCanExecuteSaveCommand = _validator.Validate(this).IsValid;
        SaveCommand.RaiseCanExecuteChanged();
    }

    private void HandleSaveCommand()
    {
        var validateResult = _validator.Validate(this);
        if (validateResult.IsValid)
        {
            MessageBox.Show("これが表示されれば検証成功です!");
        }
        else
        {
            var errorMsg = string.Join(Environment.NewLine,
                validateResult.Errors.Select(x => x.ErrorMessage).ToArray());
            MessageBox.Show($"ちょっと待って、入力を確認してください:\r\n{errorMsg}");
        }
    }

    private bool _isCanExecuteSaveCommand;

    private bool HandleCanExecuteSaveCommand()
    {
        return _isCanExecuteSaveCommand;
    }

    private void HandleCancelCommand()
    {
        MessageBox.Show("何もしません、引退しました");
    }

    public string this[string columnName]
    {
        get
        {
            var validateResult = _validator.Validate(this);
            if (validateResult.IsValid)
            {
                return string.Empty;
            }

            var firstOrDefault =
                validateResult.Errors.FirstOrDefault(error => error.PropertyName == columnName);
            return firstOrDefault == null ? string.Empty : firstOrDefault.ErrorMessage;
        }
    }

    public string Error
    {
        get
        {
            var validateResult = _validator.Validate(this);
            if (validateResult.IsValid)
            {
                return string.Empty;
            }

            var errors = string.Join(Environment.NewLine, validateResult.Errors.Select(x => x.ErrorMessage).ToArray());
            return errors;
        }
    }
}

ViewModelのプロパティ検証はStudentFieldと同様です。ここでは保存(SaveCommand)とキャンセル(CancelCommand)の2つのコマンドを追加しています。保存コマンドはすべてのプロパティ検証が成功した場合にのみ使用可能になります。プロパティ変更イベントPropertyChangedを登録し、変更イベントハンドラー内で検証を行っています。

PropertyChanged += Validate;
CurrentStudent.PropertyChanged += Validate;
foreach (var field in Fields)
{
    field.PropertyChanged += Validate;
}
private void Validate(object sender, PropertyChangedEventArgs e)
{
    _isCanExecuteSaveCommand = _validator.Validate(this).IsValid;
    SaveCommand.RaiseCanExecuteChanged();
}

4.5. ビュー層の実装

ビュー層では、入力フォームと検証結果を表示するためのユーザーコントロールStudentViewを作成しました。ViewModel層のプロパティとコマンドにバインドすることで、ビュー層はViewModel層と対話し、検証エラーをリアルタイムで表示できます。ここでは比較的シンプルで、シンプルなプロパティ(タイトル(Title))、複雑なプロパティ(学生名(CurrentStudent.Name)、学生年齢(CurrentStudent.Age)、学生郵便番号(CurrentStudent.Zip)、最小値(CurrentStudent.MinValue)、最大値(CurrentStudent.MaxValue))、コレクションプロパティ(Fields)の検証を提供します。xamlコードは以下の通りです。

<UserControl
    x:Class="WpfFluentValidation.Views.StudentView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:models="clr-namespace:WpfFluentValidation.Models"
    xmlns:vm="clr-namespace:WpfFluentValidation.ViewModels"
    mc:Ignorable="d" Padding="10">
    <UserControl.DataContext>
        <vm:StudentViewModel />
    </UserControl.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="50" />
        </Grid.RowDefinitions>
        <ScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Auto">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="*" />
                </Grid.RowDefinitions>

                <GroupBox Header="ViewModel直接プロパティ検証">
                    <StackPanel>
                        <Label Content="タイトル:" />
                        <TextBox Style="{StaticResource Styles.TextBox.ErrorStyle1}"
                                 Text="{Binding Title, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
                    </StackPanel>
                </GroupBox>

                <GroupBox Grid.Row="1" Header="ViewModelオブジェクトプロパティCurrentStudentのプロパティ検証">
                    <StackPanel>
                        <StackPanel>
                            <Label Content="名前:" />
                            <TextBox Style="{StaticResource Styles.TextBox.ErrorStyle2}"
                                     Text="{Binding CurrentStudent.Name, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
                        </StackPanel>
                        <StackPanel>
                            <Label Content="年齢:" />
                            <TextBox Style="{StaticResource Styles.TextBox.ErrorStyle2}"
                                     Text="{Binding CurrentStudent.Age, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
                        </StackPanel>
                        <StackPanel>
                            <Label Content="郵便番号:" />
                            <TextBox Style="{StaticResource Styles.TextBox.ErrorStyle2}"
                                     Text="{Binding CurrentStudent.Zip, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
                        </StackPanel>
                        <StackPanel>
                            <Label Content="最小値:" />
                            <TextBox Style="{StaticResource Styles.TextBox.ErrorStyle2}"
                                     Text="{Binding CurrentStudent.MinValue, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
                        </StackPanel>
                        <StackPanel>
                            <Label Content="最大値:" />
                            <TextBox Style="{StaticResource Styles.TextBox.ErrorStyle2}"
                                     Text="{Binding CurrentStudent.MaxValue, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
                        </StackPanel>
                    </StackPanel>
                </GroupBox>

                <GroupBox Grid.Row="2" Header="ViewModelコレクションプロパティFieldsのプロパティ検証">
                    <ItemsControl ItemsSource="{Binding Fields}">
                        <ItemsControl.ItemTemplate>
                            <DataTemplate DataType="{x:Type models:Field}">
                                <Border Padding="10">
                                    <Grid>
                                        <Grid.RowDefinitions>
                                            <RowDefinition Height="Auto" />
                                            <RowDefinition Height="Auto" />
                                        </Grid.RowDefinitions>
                                        <TextBlock Margin="0,0,0,5">
                                            <Run Text="{Binding Name}" />
                                            <Run Text="(" />
                                            <Run Text="{Binding TypeLabel}" />
                                            <Run Text=")" />
                                        </TextBlock>
                                        <TextBox Grid.Row="1" Style="{StaticResource Styles.TextBox.ErrorStyle2}"
                                                 Text="{Binding Value, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
                                    </Grid>
                                </Border>
                            </DataTemplate>
                        </ItemsControl.ItemTemplate>
                    </ItemsControl>
                </GroupBox>
            </Grid>
        </ScrollViewer>

        <StackPanel Grid.Row="1" HorizontalAlignment="Right" Orientation="Horizontal">
            <Button Content="キャンセル" Command="{Binding CancelCommand}" Style="{StaticResource Styles.Button.Common}"
                    Margin="0 3 40 3" />
            <Button Content="送信" Command="{Binding SaveCommand}" Style="{StaticResource Styles.Button.Blue}"
                    Margin="0 3 10 3" />
        </StackPanel>
    </Grid>
</UserControl>

4.6. エラー表示スタイル

ユーザーエクスペリエンスを向上させるために、2種類のエラー表示スタイルを定義しました。1つ目は入力ボックスの横に赤いアイコンでエラーを示すスタイル、2つ目は入力ボックスの右側にエラーテキストを表示するスタイルです。これらのスタイルはApp.xamlで定義されており、アプリケーション全体で再利用できます。

<Application
    x:Class="WpfFluentValidation.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    StartupUri="MainWindow.xaml">
    <Application.Resources>
        <Style TargetType="StackPanel">
            <Setter Property="Margin" Value="0,5" />
        </Style>
        <!--  1つ目のエラースタイル:赤い枠線  -->
        <Style x:Key="Styles.TextBox.ErrorStyle1" TargetType="{x:Type TextBox}">
            <Setter Property="Width" Value="250" />
            <Setter Property="Height" Value="25" />
            <Setter Property="HorizontalAlignment" Value="Left" />
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <DockPanel>
                            <Grid
                                Width="16"
                                Height="16"
                                Margin="3,0,0,0"
                                VerticalAlignment="Center"
                                DockPanel.Dock="Right">
                                <Ellipse
                                    Width="16"
                                    Height="16"
                                    Fill="Red" />
                                <Ellipse
                                    Width="3"
                                    Height="8"
                                    Margin="0,2,0,0"
                                    HorizontalAlignment="Center"
                                    VerticalAlignment="Top"
                                    Fill="White" />
                                <Ellipse
                                    Width="2"
                                    Height="2"
                                    Margin="0,0,0,2"
                                    HorizontalAlignment="Center"
                                    VerticalAlignment="Bottom"
                                    Fill="White" />
                            </Grid>
                            <Border
                                BorderBrush="Red"
                                BorderThickness="2"
                                CornerRadius="2">
                                <AdornedElementPlaceholder />
                            </Border>
                        </DockPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="ToolTip"
                            Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}" />
                </Trigger>
            </Style.Triggers>
        </Style>

        <!--  2つ目のエラースタイル:右側にテキスト表示  -->
        <Style x:Key="Styles.TextBox.ErrorStyle2" TargetType="{x:Type TextBox}">
            <Setter Property="Width" Value="250" />
            <Setter Property="Height" Value="25" />
            <Setter Property="VerticalContentAlignment" Value="Center" />
            <Setter Property="Padding" Value="5,0" />
            <Setter Property="HorizontalAlignment" Value="Left" />
            <Setter Property="Validation.ErrorTemplate">
                <Setter.Value>
                    <ControlTemplate>
                        <StackPanel Orientation="Horizontal">
                            <AdornedElementPlaceholder x:Name="textBox" />
                            <Grid>
                                <TextBlock Margin="10 0 0 0" Width="130"
                                           Foreground="Red" TextWrapping="Wrap"
                                           Text="{Binding [0].ErrorContent}" />
                            </Grid>
                        </StackPanel>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="ToolTip"
                            Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}" />
                    <Setter Property="Background" Value="LightPink" />
                    <Setter Property="BorderBrush" Value="Red" />
                    <Setter Property="Foreground" Value="White" />
                </Trigger>
            </Style.Triggers>
        </Style>

        <Style TargetType="GroupBox">
            <Setter Property="Margin" Value="5" />
            <Setter Property="Padding" Value="2" />
            <Setter Property="BorderBrush" Value="#FF0078D7" />
            <Setter Property="BorderThickness" Value="2" />
            <Setter Property="Background" Value="#FFF0F0F0" />
            <Setter Property="Foreground" Value="#FF0078D7" />
            <Setter Property="FontWeight" Value="Bold" />
        </Style>

        <Style x:Key="Styles.Button.Common" TargetType="{x:Type Button}">
            <Setter Property="MinWidth" Value="75" />
            <Setter Property="MinHeight" Value="25" />
            <Setter Property="Background" Value="White" />
            <Setter Property="Foreground" Value="Black" />
        </Style>

        <Style
            x:Key="Styles.Button.Blue"
            BasedOn="{StaticResource ResourceKey=Styles.Button.Common}"
            TargetType="{x:Type Button}">
            <Setter Property="Background" Value="Green" />
            <Setter Property="Foreground" Value="White" />
        </Style>
    </Application.Resources>
</Application>

5. 効果表示

上記の手順を実装することで、機能豊富なWPFアプリケーションが完成しました。ユーザーの入力に応じてリアルタイムで検証を行い、直感的なエラーメッセージを表示します。すべてのプロパティが検証を通過すると、送信ボタンが使用可能になります。

6. ソースコード共有

読者の学習と交流の便宜を図り、本記事のすべてのコードをGiteeおよびGitHubプラットフォームに同期しました。興味のある開発者は以下のリンクからソースコードを入手してください。

7. まとめ

この記事の紹介と実践を通じて、FluentValidationをC# WPFプロジェクトに適用し、ViewModel層のプロパティに対する包括的な検証を実現しました。これにより、データの安全性と正確性が向上し、ユーザーにより良い対話体験を提供できます。この記事が、WPFプロジェクトでFluentValidationを使用する多くの開発者にとって有益な参考とヒントになることを願っています。

参考:

さらに探索

関連読書

その他の記事