鉴于之前已经询问过 Prism 的多项功能(包括异步验证),我将提供一个精简且聚焦的回答,基于 WPF 环境,避免重复之前的内容
Prism 异步验证的示例。鉴于之前已经询问过 Prism 的多项功能(包括异步验证),我将提供一个精简且聚焦的回答,基于 WPF 环境,避免重复之前的内容(如INotifyDataErrorInfo和模拟 API 的异步验证示例)。本次示例将展示一个不同场景的异步验证(例如验证密码强度通过异步 API 调用),包括代码、测试代码、适用场景和使用方法。如果您有特定需求(如与导航/区域的集成、更复杂的验证逻辑或特定测试场景),请进一步说明,我会定制答案。
一、异步验证概述
Prism 通过 WPF 的INotifyDataErrorInfo接口支持异步验证,结合BindableBase和异步方法(async/await)实现动态输入验证。异步验证适用于需要远程或复杂逻辑的场景(如 API 调用检查密码强度),并通过错误通知机制更新 UI。主要特性:
- 异步处理:支持耗时验证(如 API 调用)。
- 错误通知:通过
INotifyDataErrorInfo提供动态错误反馈。 - MVVM 集成:与 Prism 的 MVVM 模式无缝结合。
- 模块化支持:验证逻辑可定义在模块中。
二、适用场景
异步验证适用于:
- 远程验证:如通过 API 检查密码强度或唯一性。
- 复杂规则:如验证密码是否符合安全策略(长度、字符类型)。
- 实时反馈:输入时异步验证并显示错误或状态。
- 模块化应用:模块中的视图模型需要独立异步验证。
- 表单提交:确保提交前所有字段通过异步验证。
三、异步验证示例
以下是一个异步验证密码强度的示例,假设通过 API 检查密码是否满足要求。
1. 项目配置
- 安装 Prism NuGet 包(如
Prism.DryIoc)。 - 创建 Prism 应用程序,继承
PrismApplication. - 确保视图模型继承
BindableBase。
2. 定义模拟 API 服务
模拟异步 API 调用以验证密码强度:
IPasswordValidationService.cs:
usingSystem.Threading.Tasks;namespaceWpfApp.Services{publicinterfaceIPasswordValidationService{Task<(boolIsValid,stringMessage)>ValidatePasswordAsync(stringpassword);}}PasswordValidationService.cs:
usingSystem.Text.RegularExpressions;usingSystem.Threading.Tasks;namespaceWpfApp.Services{publicclassPasswordValidationService:IPasswordValidationService{publicasyncTask<(boolIsValid,stringMessage)>ValidatePasswordAsync(stringpassword){// 模拟 API 延迟awaitTask.Delay(1000);if(string.IsNullOrEmpty(password))return(false,"Password cannot be empty.");if(password.Length<8)return(false,"Password must be at least 8 characters.");if(!Regex.IsMatch(password,@"[A-Z]"))return(false,"Password must contain at least one uppercase letter.");if(!Regex.IsMatch(password,@"[0-9]"))return(false,"Password must contain at least one number.");return(true,"Password is strong.");}}}3. 定义视图模型
实现INotifyDataErrorInfo和异步验证逻辑:
MainWindowViewModel.cs:
usingPrism.Commands;usingPrism.Mvvm;usingSystem.Collections;usingSystem.Collections.Generic;usingSystem.ComponentModel;usingSystem.Linq;usingSystem.Threading.Tasks;usingWpfApp.Services;namespaceWpfApp.ViewModels{publicclassMainWindowViewModel:BindableBase,INotifyDataErrorInfo{privatereadonlyIPasswordValidationService_validationService;privatestring_password;privatebool_isValidating;privatestring_validationMessage;privatereadonlyDictionary<string,List<string>>_errors=newDictionary<string,List<string>>();publicstringPassword{get=>_password;set{if(SetProperty(ref_password,value)){ValidatePasswordAsync();}}}publicboolIsValidating{get=>_isValidating;set=>SetProperty(ref_isValidating,value);}publicstringValidationMessage{get=>_validationMessage;set=>SetProperty(ref_validationMessage,value);}publicDelegateCommandSubmitCommand{get;}publicMainWindowViewModel(IPasswordValidationServicevalidationService){_validationService=validationService;SubmitCommand=newDelegateCommand(OnSubmit,CanSubmit);}privateasyncvoidValidatePasswordAsync(){_errors.Remove(nameof(Password));IsValidating=true;ValidationMessage="Validating...";try{var(isValid,message)=await_validationService.ValidatePasswordAsync(_password);if(!isValid)AddError(nameof(Password),message);ValidationMessage=message;}finally{IsValidating=false;RaiseErrorsChanged(nameof(Password));SubmitCommand.RaiseCanExecuteChanged();}}privatevoidOnSubmit(){ValidationMessage="Form submitted successfully!";}privateboolCanSubmit(){return!HasErrors&&!IsValidating;}privatevoidAddError(stringpropertyName,stringerror){if(!_errors.ContainsKey(propertyName))_errors[propertyName]=newList<string>();_errors[propertyName].Add(error);}privatevoidRaiseErrorsChanged(stringpropertyName){ErrorsChanged?.Invoke(this,newDataErrorsChangedEventArgs(propertyName));}publicboolHasErrors=>_errors.Any();publiceventEventHandler<DataErrorsChangedEventArgs>ErrorsChanged;publicIEnumerableGetErrors(stringpropertyName){return_errors.ContainsKey(propertyName)?_errors[propertyName]:null;}}}4. 定义视图
在MainWindow.xaml中应用异步验证并显示状态:
<Window x:Class="WpfApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfApp.Converters" Title="MainWindow" Height="450" Width="800"> <Window.Resources> <local:ErrorConverter x:Key="ErrorConverter"/> <local:BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter"/> <Style TargetType="TextBox"> <Style.Triggers> <Trigger Property="Validation.HasError" Value="True"> <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/> <Setter Property="BorderBrush" Value="Red"/> </Trigger> </Style> </Style> </Window.Resources> <Grid> <StackPanel Margin="10"> <TextBox Width="200" Margin="5"> <TextBox.Text> <Binding Path="Password" UpdateSourceTrigger="PropertyChanged" ValidatesOnNotifyDataErrors="True"/> </TextBox.Text> <TextBox.ToolTip> <Binding Path="(Validation.Errors)" RelativeSource="{RelativeSource Self}" Converter="{StaticResource ErrorConverter}"/> </TextBox.ToolTip> </TextBox> <TextBlock Text="{Binding ValidationMessage}" FontSize="14" Margin="5"/> <TextBlock Text="Validating..." Visibility="{Binding IsValidating, Converter={StaticResource BoolToVisibilityConverter}}" Margin="5"/> <Button Content="Submit" Command="{Binding SubmitCommand}" Width="100" Margin="5"/> </StackPanel> </Grid> </Window>ErrorConverter.cs:
usingSystem;usingSystem.Globalization;usingSystem.Windows.Controls;usingSystem.Windows.Data;namespaceWpfApp.Converters{publicclassErrorConverter:IValueConverter{publicobjectConvert(objectvalue,TypetargetType,objectparameter,CultureInfoculture){if(valueisReadOnlyObservableCollection<ValidationError>errors&&errors.Any())returnstring.Join("\n",errors.Select(e=>e.ErrorContent));returnnull;}publicobjectConvertBack(objectvalue,TypetargetType,objectparameter,CultureInfoculture){thrownewNotImplementedException();}}}BooleanToVisibilityConverter.cs:
usingSystem;usingSystem.Globalization;usingSystem.Windows;usingSystem.Windows.Data;namespaceWpfApp.Converters{publicclassBooleanToVisibilityConverter:IValueConverter{publicobjectConvert(objectvalue,TypetargetType,objectparameter,CultureInfoculture){return(valueisboolisVisible&&isVisible)?Visibility.Visible:Visibility.Collapsed;}publicobjectConvertBack(objectvalue,TypetargetType,objectparameter,CultureInfoculture){returnvalueisVisibilityvisibility&&visibility==Visibility.Visible;}}}5. 配置应用程序
在App.xaml.cs中注册服务:
usingPrism.Ioc;usingSystem.Windows;usingWpfApp.Services;namespaceWpfApp{publicpartialclassApp:PrismApplication{protectedoverrideWindowCreateShell(){returnContainer.Resolve<MainWindow>();}protectedoverridevoidRegisterTypes(IContainerRegistrycontainerRegistry){containerRegistry.Register<IPasswordValidationService,PasswordValidationService>();}}}四、测试代码
以下是使用 MSTest 和 Moq 的单元测试示例,测试异步验证逻辑:
1. 测试依赖
安装 NuGet 包:
Microsoft.NET.Test.SdkMSTest.TestFrameworkMSTest.TestAdapterMoqPrism.DryIoc
2. 测试代码
测试 PasswordValidationService:
usingMicrosoft.VisualStudio.TestTools.UnitTesting;usingSystem.Threading.Tasks;usingWpfApp.Services;[TestClass]publicclassPasswordValidationServiceTests{privatePasswordValidationService_service;[TestInitialize]publicvoidSetup(){_service=newPasswordValidationService();}[TestMethod]publicasyncTaskValidatePasswordAsync_ValidPassword_ReturnsValid(){var(isValid,message)=await_service.ValidatePasswordAsync("Password123");Assert.IsTrue(isValid);Assert.AreEqual("Password is strong.",message);}[TestMethod]publicasyncTaskValidatePasswordAsync_ShortPassword_ReturnsInvalid(){var(isValid,message)=await_service.ValidatePasswordAsync("Pass123");Assert.IsFalse(isValid);Assert.AreEqual("Password must be at least 8 characters.",message);}[TestMethod]publicasyncTaskValidatePasswordAsync_NoUppercase_ReturnsInvalid(){var(isValid,message)=await_service.ValidatePasswordAsync("password123");Assert.IsFalse(isValid);Assert.AreEqual("Password must contain at least one uppercase letter.",message);}}测试 MainWindowViewModel:
usingMicrosoft.VisualStudio.TestTools.UnitTesting;usingMoq;usingSystem.Linq;usingSystem.Threading.Tasks;usingWpfApp.Services;usingWpfApp.ViewModels;[TestClass]publicclassMainWindowViewModelTests{privateMock<IPasswordValidationService>_validationServiceMock;privateMainWindowViewModel_viewModel;[TestInitialize]publicvoidSetup(){_validationServiceMock=newMock<IPasswordValidationService>();_viewModel=newMainWindowViewModel(_validationServiceMock.Object);}[TestMethod]publicvoidPassword_Empty_AddsError(){_viewModel.Password="";Assert.IsTrue(_viewModel.HasErrors);varerrors=_viewModel.GetErrors(nameof(_viewModel.Password))asList<string>;Assert.IsTrue(errors.Contains("Password cannot be empty."));}[TestMethod]publicasyncTaskPassword_Invalid_AddsError(){_validationServiceMock.Setup(s=>s.ValidatePasswordAsync("weak")).ReturnsAsync((false,"Password must be at least 8 characters."));_viewModel.Password="weak";awaitTask.Delay(1500);// 等待异步验证完成Assert.IsTrue(_viewModel.HasErrors);varerrors=_viewModel.GetErrors(nameof(_viewModel.Password))asList<string>;Assert.IsTrue(errors.Contains("Password must be at least 8 characters."));Assert.AreEqual("Password must be at least 8 characters.",_viewModel.ValidationMessage);}[TestMethod]publicasyncTaskPassword_Valid_NoErrors(){_validationServiceMock.Setup(s=>s.ValidatePasswordAsync("Password123")).ReturnsAsync((true,"Password is strong."));_viewModel.Password="Password123";awaitTask.Delay(1500);// 等待异步验证完成Assert.IsFalse(_viewModel.HasErrors);Assert.AreEqual("Password is strong.",_viewModel.ValidationMessage);Assert.IsTrue(_viewModel.SubmitCommand.CanExecute(null));}[TestMethod]publicasyncTaskIsValidating_SetDuringValidation(){_validationServiceMock.Setup(s=>s.ValidatePasswordAsync("Password123")).ReturnsAsync((true,"Password is strong."));boolisValidatingChanged=false;_viewModel.PropertyChanged+=(s,e)=>{if(e.PropertyName==nameof(_viewModel.IsValidating))isValidatingChanged=true;};_viewModel.Password="Password123";awaitTask.Delay(1500);// 等待异步验证完成Assert.IsTrue(isValidatingChanged);Assert.IsFalse(_viewModel.IsValidating);}}3. 测试说明
- PasswordValidationService 测试:
- 验证有效密码返回正确结果。
- 验证短密码和无大写字母的密码返回正确错误。
- MainWindowViewModel 测试:
- 验证空密码触发错误。
- 验证无效密码触发异步错误。
- 验证有效密码无错误且命令可执行。
- 验证异步验证期间
IsValidating属性变化。
五、注意事项
- 异步处理:
- 使用
async void在属性设置中触发异步验证,但需谨慎处理异常。 - 考虑使用防抖(debounce)技术避免频繁触发异步调用。
- 使用
- UI 反馈:
- 使用
IsValidating和BooleanToVisibilityConverter显示验证进度。 - 使用
ErrorConverter显示错误消息。
- 使用
- 模块化集成:
- 异步验证逻辑可定义在模块中,与区域或导航结合。
- 性能优化:
- 避免短时间内多次触发异步验证。
- 使用缓存或延迟机制优化 API 调用。
- 测试注意:
- 使用
Task.Delay模拟异步等待,确保测试覆盖异步逻辑。 - 使用 Moq 模拟异步服务。
- 使用
六、总结
Prism 异步验证通过INotifyDataErrorInfo和async/await实现动态输入验证,适合远程验证(如密码强度检查)和复杂业务规则。结合 Prism 的 MVVM 支持,异步验证可集成到模块化应用中。上述代码示例展示了密码强度的异步验证,测试代码验证了核心功能。
如果您需要更具体的内容(例如与区域/导航/事件聚合器的集成、其他异步验证场景或特定测试用例),请提供细节,我会进一步优化答案!