私は、FluentValidationとReactive UIに焦点を当てようとした執筆の過程で、WPF-ReactiveValidationのオープンソースライブラリについてお話したいと思います。 そのタスクは、ユーザーがフォーム内のデータを変更するたびにフォームを検証することです。
ライブラリを操作する例。 良いニュースは、独自のテンプレートを使用できることです。
ライブラリの主な機能:
- ルールは、流れるようなインターフェイスを介して作成されます
- プロパティの変更に対する完全な内部制御
- ローカリゼーションサポート(オンザフライを含む)
- GUIでのメッセージの表示
作成する理由
ユーザーからデータを受信してサーバーに渡すWPFアプリケーションがあります。 サーバーは、データベースストアドプロシージャを呼び出します。 入力データの完全な検証はストアドプロシージャコードで実装されているため、ユーザーは誤ったパラメーターを渡すと、メッセージとともに例外を受け取ることが保証されます(アプリケーションに戻り、表示されます)。 明らかに、クライアントでいくつかの例外を予測できます。そこで、それらを処理する必要があります。 最初は、次の構成を使用しました。
このオプションの欠点は次のとおりです。
次のステップは、注釈属性(DataAnnotations)による検証の実装とIDataErrorInfoの使用でした。 結果は次のコードです。
BaseViewModelは、リフレクション(リフレクション)を通じてプロパティとその検証属性のリストを受け取るメカニズムを実装します。 プロパティが変更されると、すべての属性のチェックが呼び出され、結果がディクショナリに書き込まれます。 IDataErrorInfoインターフェイスから
indexer
を呼び出すと、これらの値が返されます(メッセージの連結)。
このアプローチは、検証を使用する最も一般的なケースを大幅に簡素化しました-必須値のチェック、定数との比較など。 IDataErrorInfoインターフェイスの実装により、GUIで無効なフィールドを表示できます。 ユーザーがすべてのフィールドに正しく入力するまで、実行ボタンをロックすることもできます。 この形式では、ライブラリの作業が完全に適していますが、その後、互いに依存するプロパティに遭遇しました...
上で引用した例だけがこの問題を示しています。 変更可能な2つの値が検証に使用されている場合、一方が変更されたときに、もう一方を再検証する必要があります。 上記で説明したメカニズムはこれをサポートしていませんでしたが、一部の場所では、これをすべて健全な状態でサポートする松葉杖からクラックし始めました(例ではそれらを提供しませんでしたが、ループ制御を備えた別のプロパティのPropertyChangedへの呼び出しに基づいています)。 同僚のアドバイスに耳を傾けながら、前述の欠点を修正する新しいメカニズムを作成しました。その後、仕事に関係しない他のプロジェクトで良心のtwin折なしにそれを使用したいという要望がありました。 そのため、元のデザインのエラーを考慮して新しいコードを追加し 、すべてのコードをゼロから書き直したかったのです。
private override void Execute() { if(string.IsNullOrEmpty(Property1) == true) { MessageBox.Show(" Property1"); return; } if(Property2 < Property3) { MessageBox.Show("Property2 Property3"); return; } ... // do(); }
このオプションの欠点は次のとおりです。
- 余分なコード
- ポップアップのみによるユーザーインタラクション
次のステップは、注釈属性(DataAnnotations)による検証の実装とIDataErrorInfoの使用でした。 結果は次のコードです。
public class ViewModel : BaseViewModel { [IsRequired] public string Property1 { get {...} set {...} } [CustomValidation(typeof(ViewModel), nameof(ValidateProperty2))] public int? Property2 { get {...} set {...} } public int? Property3 { get {...} set {...} } [UsedImplicitly] public static ValidationResult ValidateProperty2(int? property2, ValidationContext validationContext) { var viewModel = (ViewModel)validationContext.ObjectInstance; if (viewModel.Property2 < viewModel.Property3) { return new ValidationResult("Property2 Property3"); } return ValidationResult.Success; } }
BaseViewModelは、リフレクション(リフレクション)を通じてプロパティとその検証属性のリストを受け取るメカニズムを実装します。 プロパティが変更されると、すべての属性のチェックが呼び出され、結果がディクショナリに書き込まれます。 IDataErrorInfoインターフェイスから
string this[string columnName]
indexer
string this[string columnName]
を呼び出すと、これらの値が返されます(メッセージの連結)。
このアプローチは、検証を使用する最も一般的なケースを大幅に簡素化しました-必須値のチェック、定数との比較など。 IDataErrorInfoインターフェイスの実装により、GUIで無効なフィールドを表示できます。 ユーザーがすべてのフィールドに正しく入力するまで、実行ボタンをロックすることもできます。 この形式では、ライブラリの作業が完全に適していますが、その後、互いに依存するプロパティに遭遇しました...
上で引用した例だけがこの問題を示しています。 変更可能な2つの値が検証に使用されている場合、一方が変更されたときに、もう一方を再検証する必要があります。 上記で説明したメカニズムはこれをサポートしていませんでしたが、一部の場所では、これをすべて健全な状態でサポートする松葉杖からクラックし始めました(例ではそれらを提供しませんでしたが、ループ制御を備えた別のプロパティのPropertyChangedへの呼び出しに基づいています)。 同僚のアドバイスに耳を傾けながら、前述の欠点を修正する新しいメカニズムを作成しました。その後、仕事に関係しない他のプロジェクトで良心のtwin折なしにそれを使用したいという要望がありました。 そのため、元のデザインのエラーを考慮し
開発プロセスでは、FluentValidationに焦点を当てようとしたため、構文は簡単に認識できます。 ただし、違いがあります。タスクに合わせて調整されたもの、実装されていないものがありますが、順番にすべてが行われています。
オブジェクトの状態に関するすべての情報は、ルールを使用して生成されるValidatorプロパティに保存されます。 マシンプロパティの例で作成を検討してください。
public class CarViewModel : ValidatableObject { public CarViewModel() { Validator = GetValidator(); } private IObjectValidator GetValidator() { var builder = new ValidationBuilder<CarViewModel>(); builder.RuleFor(vm => vm.Make).NotEmpty(); builder.RuleFor(vm => vm.Model).NotEmpty().WithMessage("Please specify a car model"); builder.RuleFor(vm => vm.Mileage).GreaterThan(0).When(model => model.HasMileage); builder.RuleFor(vm => vm.Vin).Must(BeAValidVin).WithMessage("Please specify a valid VIN"); builder.RuleFor(vm => vm.Description).Length(10, 100); return builder.Build(this); } private bool BeAValidVin(string vin) { // VIN } // INotifyPropertyChanged }
この例はFluentValidationが提供する例と非常に似ているため、コメントが不要であることを願っています。 バリデーターはViewModelに関して内部オブジェクトであり、最終的にはコンストラクター内でビルドされるという事実に焦点を当てています。
ユーザーインターフェイスにエラーを表示するには、ライブラリからリソースディクショナリ(デフォルトでControlTemplateを含む)を接続し、できればスタイルを作成し(コントロールのタイプごとにこれを行う必要があります)、添付プロパティ(ReachedValidation.AutoRefreshErrorTemplateおよび例に示すように、ReactiveValidation.ErrorTemplate:
xmlns:b="clr-namespace:ReactiveValidation.WPF.Behaviors;assembly=ReactiveValidation" ... <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="/ReactiveValidation;component/WPF/Themes/Generic.xaml" /> </ResourceDictionary.MergedDictionaries> <Style x:Key="TextBox" TargetType="TextBox"> <Setter Property="b:ReactiveValidation.AutoRefreshErrorTemplate" Value="True" /> <Setter Property="b:ReactiveValidation.ErrorTemplate" Value="{StaticResource ValidationErrorTemplate}" /> <!-- Margin --> <Setter Property="Margin" Value="3" /> </Style> </ResourceDictionary>
このコードはApp.xamlに最も便利に配置され、アプリケーション全体で使用できるようになります。
ControlTemplateを追加する理由は明らかだと思います。 しかし、プロパティは戸惑う可能性があります。 残念ながら、WPFの標準の検証には、エラーパターンの誤った表示につながる多くの問題が含まれています(プロパティが有効な場合、またはその逆の場合に使用されます)。 これを避けるために、添付プロパティを介して動作する松葉杖がいくつか書かれました。
スタイルをコントロールに適用するだけで、すべてが機能します。
<TextBlock Grid.Row="0" Grid.Column="0" Margin="3" Text="Make: " /> <TextBox Grid.Row="0" Grid.Column="1" Style="{StaticResource TextBox}" Text="{Binding Make, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> <TextBlock Grid.Row="1" Grid.Column="0" Margin="3" Text="Model: " /> <TextBox Grid.Row="1" Grid.Column="1" Style="{StaticResource TextBox}" Text="{Binding Model, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> <TextBlock Grid.Row="2" Grid.Column="0" Margin="3" Text="Has mileage: " /> <CheckBox Grid.Row="2" Grid.Column="1" Margin="3" IsChecked="{Binding HasMileage, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> <TextBlock Grid.Row="3" Grid.Column="0" Margin="3" Text="Mileage: " /> <TextBox Grid.Row="3" Grid.Column="1" Style="{StaticResource TextBox}" IsEnabled="{Binding HasMileage}" Text="{Binding Mileage, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> <TextBlock Grid.Row="4" Grid.Column="0" Margin="3" Text="Vin: " /> <TextBox Grid.Row="4" Grid.Column="1" Style="{StaticResource TextBox}" Text="{Binding Vin, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> <TextBlock Grid.Row="5" Grid.Column="0" Margin="3" Text="Description: " /> <TextBox Grid.Row="5" Grid.Column="1" Style="{StaticResource TextBox}" Text="{Binding Description, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
すべてが組み立てられると、次の簡単なアプリケーションが得られます。
フィールドに入力すると、赤い三角形が消えてエラーを示します。
メッセージテキストとローカライズ
ローカライズを使用する必要がないアプリケーションでは、通常の静的文字列を使用できます。 以下は、プロパティバリデータのメッセージテキスト全体を変更する例です。
builder.RuleFor(vm => vm.PhoneNumber) .NotEmpty() .When(vm => Email, email => string.IsNullOrEmpty(email) == true) .WithMessage("You need to specify a phone or email") .Matches(@"^\d{11}$") .WithMessage("Phone number must contain 11 digits");
DisplayName属性(ReactiveValidation.Attributes名前空間から)を使用して、プロパティの表示名を示すことができます。
[DisplayName(DisplayName = "Minimal amount")] public int MinAmount { get; set; }
メッセージをローカライズするために、リソースとともに作成されるResourceManagerクラスが使用されます。 Default.resxとDefault.ru.resxの2つのファイルを作成することにより、2つの言語のサポートを提供できます。
便宜上、静的クラスを使用して、デフォルトのリソースマネージャーを設定できます。その値をValidationOptions.LanguageManager.DefaultResourceManagerに割り当てるだけです。 ただし、別のリソースマネージャーを使用することは可能です。 この例では、上記のすべてが示されています。
builder.RuleFor(vm => vm.Email) .NotEmpty() .When(vm => PhoneNumber, phoneNumber => string.IsNullOrEmpty(phoneNumber) == true) .WithLocalizedMessage(nameof(Resources.Default.PhoneNumberOrEmailRequired)) .Matches(@"^\w+@\w+.\w+$") .WithLocalizedMessage(Resources.Additional.ResourceManager, nameof(Resources.Additional.NotValidEmail));
EmailまたはPhoneNumberの値が空の場合、PhoneNumberOrEmailRequiredキーを持つメッセージがデフォルトリソースから表示されます。 さらに、メールは正規表現を満たしている必要があり、一致しない場合は、NotValidEmailキーを持つ追加リソースからメッセージが既に表示されます。
表示名をローカライズするには、属性を使用してDisplayNameKeyとResourceTypeを渡してリソースをオーバーライドする必要があります(属性の場合、ResourceManager自体を使用することはできないため、そのタイプが使用されます)。
[DisplayName(DisplayNameKey = nameof(Resources.Default.PhoneNumber))] public string PhoneNumber { get; set; } [DisplayName(ResourceType = typeof(Resources.Additional), DisplayNameKey = nameof(Resources.Additional.Email))] public string Email { get; set; }
ローカライズのために、CultureInfo.CurrentUICultureからカルチャが取得されます。 さらに、ValidationOptions.LanguageManager.CurrentCultureを使用してオーバーライドできます。 デフォルトでは、カルチャが変更されてもメッセージテキストは変更されませんが、この動作はValidationOptions.LanguageManager.TrackCultureChangedオプションを使用して有効にできますが、いくつかの機能を考慮する必要があります。
- ローカライズの変更は、ValidationMessageクラス内でLanguageManagerクラスのイベントへのサブスクリプションが発生するという事実に基づいています
- サブスクリプションは、TrackCultureChangedプロパティがtrueの場合にのみ発生します。 したがって、設定は1回のみで、アプリケーションの開始時に変更する必要があります。
- CultureInfo.CurrentUICultureを使用してカルチャが変更された場合、変更後にValidationOptions.LanguageManager.OnCultureChanged()メソッドを呼び出します
さらに、インターフェイスのローカライズの変更がサポートされていない場合、この動作を有効にすることは意味がありません。
追加機能:
- メッセージには主に2つのタイプがあります。エラーと警告(警告)です。 警告もGUIに表示されます(オレンジ色のみ)が、モデルは有効と見なされます。 さらに、通常/単純(単純)のグラデーションがあります。 コントロールにフォーカスまたはホバリングすると通常のメッセージが表示されますが、ホバリングすると単純なメッセージが表示されます
- ルールは拡張メソッドに基づいているため、独自のバリデーターで簡単に拡張できます。
- 検証済みオブジェクトに必要な基本インターフェイスはIValidatableObjectです。 ライブラリにはINPCベースのValidatableObject実装があります。 実装には少しのコードが含まれているため、このインターフェイスを使用して基本クラスを簡単に定義できます(プロジェクトは、Reactive UIのReactiveObjectのオーバーライドを使用した例を示しています)
- INotifyPropertyChangedまたはINotifyCollectionChangedから継承されたプロパティが検証されると、特別なアダプタークラスが呼び出しを監視し、再検証を開始します。 クラスを使用してサブスクリプションを拡張できます。たとえば、Reactive UIからIReactiveNotifyCollectionItemChanged <>を追加します。
私が言いたいこと:
- 非同期検証はありません。 私たちはそれが必要かどうかについて議論することができると思いますが、それでも、今ではサポートされていません。
- 私は恥ずかしいですが、エラーが発生する可能性があります。 それらがすぐに発見され、修正されることを願っています。
- これは、オープンソースの最初の開発経験です。 私が最初に投資した機会が十分ではないことを非常に心配しています。 これも簡単に修正できることを願っています。
GitHubで入手可能なソースコード、MITライセンス
または、Nugetからダウンロードできます
記事に記載されているプロジェクト:
同僚のadeptussと@baiselに感謝したい
プロジェクトの最初のバージョンの開発を支援します。
また、忍耐とバグ修正のためのtruetan4ik