Reusable async validation for WPF with Prism 5
WPF has supported validation since the first release in .NET 3.0. That support is built into the binding object and allows you to indicate validation errors through exceptions, an implementation of the IDataErrorInfo interface, or by using WPF ValidationRules. Additional support was added in .NET 4.5 for INotifyDataErrorInfo, an async variant of IDataErrorInfo that was first introduced in Silverlight 4. WPF not only supports evaluating whether input data is valid using one of those mechanisms, but will change the appearance of the control to indicate errors, and it holds those errors on the control as properties that can be used to further customize the presentation of the errors.
Prism is a toolkit produced by Microsoft patterns & practices that helps you build loosely coupled, extensible, maintainable and testable WPF and Silverlight applications. It has had several releases since Prism 1 released in June 2008. There is also a Prism for Windows Runtime for building Windows Store applications. I’ve had the privilege to work with the Prism team as a vendor and advisor contributing to the requirements, design and code of Prism since the first release. The Prism team is working on a new release that will be titled Prism 5 that targets WPF 4.5 and above and introduces a number of new features. For the purposes of this post, let’s leverage some stuff that was introduced in Prism 4, as well as some new stuff in Prism 5.
Picking a validation mechanism
Of the choices for supporting validation, the best one to choose in most cases is the new support for INotifyDataErrorInfo because it’s evaluated by default by the bindings, supports more than one error per property and allows you to evaluate validation rules both synchronously and asynchronously.
The definition of INotifyDataErrorInfo looks like this:
public interface INotifyDataErrorInfo
{
IEnumerable GetErrors(string propertyName); event EventHandler ErrorsChanged; bool HasErrors { get; }
}
The way INotifyDataErrorInfo works is that by default the Binding will inspect the object it is bound to and see if it implements INotifyDataErrorInfo. If it does, any time the Binding sets the bound property or gets a PropertyChanged notification for that property, it will query GetErrors to see if there are any validation errors for the bound property. If it gets any errors back, it will associate the errors with the control and the control can display the errors immediately.
Additionally, the Binding will subscribe to the ErrorsChanged event and call GetErrors again when it is raised, so it gives the implementation the opportunity to go make an asynchronous call (such as to a back end service) to check validity and then raise the ErrorsChanged event when those results are available. The display of errors is based on the control’s ErrorTemplate, which is a red box around the control by default. That is customizable through control templates and you can easily do things like add a ToolTip to display the error message(s).
Implementing INotifyPropertyChanged with Prism 5 BindableBase
One other related thing to supporting validation on your bindable objects is supporting INotifyPropertyChanged. Any object that you are going to bind to that can have its properties changed by code other than the Binding should implement this interface and raise PropertyChanged events from the bindable property set blocks so that the UI can stay in sync with the current data values. You can just implement this interface on every object you are going to bind to, but that results in a lot of repetitive code. As a result, most people encapsulate that implementation into a base class.
Prism 4 had a base class called NotificationObject that did this. However, the project templates for Windows 8 (Windows Store) apps in Visual Studio introduced a base class for this purpose that had a better pattern that reduced the amount of code in the derived data object classes even more. This class was called BindableBase. Prism for Windows Runtime reused that pattern, and in Prism 5 the team decided to carry that over to WPF to replace NotificationObject.
BindableBase has an API that looks like this:
public abstract class BindableBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged; protected virtual bool SetProperty(ref T storage, T value,
[CallerMemberName] string propertyName = null)
{
...
} protected void OnPropertyChanged(string propertyName)
{
...
} protected void OnPropertyChanged(
Expression> propertyExpression)
{
...
}
}
When you derive from BindableBase, your derived class properties can simply call SetProperty in their property set blocks like this:
public string FirstName
{
get { return _FirstName; }
set { SetProperty(ref _FirstName, value); }
}
Additionally if you have properties that need to raise PropertyChanged for other properties (such as raising a PropertyChanged for a computed FullName property that concatenates FirstName and LastName from those two properties set blocks), you can simply call OnPropertyChanged:
public string FirstName
{
get { return _FirstName; }
set
{
SetProperty(ref _FirstName, value);
OnPropertyChanged(() => FullName);
}
} public string LastName
{
get { return _LastName; }
set
{
SetProperty(ref _LastName, value);
OnPropertyChanged(() => FullName);
}
} public string FullName { get { return _FirstName + " " + _LastName; } }
So the starting point for bindable objects that support async validation is to first derive from BindableBase for the PropertyChanged support encapsulated there. Then implement INotifyDataErrorInfo to add in the async validation support.
Implementing INotifyDataErrorInfo with Prism 5
To implement INotifyDataErrorInfo, you basically need a dictionary indexed by property name that you can reach into from GetErrors, where the value of each dictionary entry is a collection of error objects (typically just strings) per property. Additionally, you have to be able to raise an event whenever the errors for a given property change. Additionally you need to be able to raise ErrorsChanged events whenever the set of errors in that dictionary for a given property changes. Prism has a class that was introduced for the MVVM QuickStart in Prism 4 called ErrorsContainer<T>. This class gives you a handy starting point for the dictionary of errors that you can reuse.
To encapsulate that and get the BindableBase support, I will derive from BindableBase, implement INotifyDataErrorInfo, and encapsulate an ErrorsContainer to manage the implementation details.
public class ValidatableBindableBase : BindableBase, INotifyDataErrorInfo
{
ErrorsContainer _errorsContainer;
...
}
I’ll get into more details on more of the implementation a little later in the post.
Defining validation rules
There are many ways and different places that people need to implement validation rules. You can put them on the data objects themselves, you might use a separate framework to define the rules and execute them, or you may need to call out to a remote service that encapsulates the business rules in a service or rules engine on the back end. You can support all of these due to the flexibility of INotifyDataErrorInfo, but one mechanism you will probably want to support “out of the box” is to use DataAnnotations. The System.ComponentModel.DataAnnotations namespace in .NET defines an attribute-based way of putting validation rules directly on the properties they affect that can be automatically evaluated by many parts of the .NET framework, including ASP.NET MVC, Web API, and Entity Framework. WPF does not have any hooks to automatically evaluate these, but we have the hooks we need to evaluate them based on PropertyChanged notifications and we can borrow some code from Prism for Windows Runtime on how to evaluate them.
An example of using a DataAnnotation attribute is the following:
[CustomValidation(typeof(Customer), "CheckAcceptableAreaCodes")]
[RegularExpression(@"^\D?(\d{3})\D?\D?(\d{3})\D?(\d{4})$",
ErrorMessage="You must enter a 10 digit phone number in a US format")]
public string Phone
{
get { return _Phone; }
set { SetProperty(ref _Phone, value); }
}
DataAnnotations have built in rules for Required fields, RegularExpressions, StringLength, MaxLength, Range, Phone, Email, Url, and CreditCard. Additionally, you can define a method and point to it with the CustomValidation attribute for custom rules. You can find lots of examples and documentation on using DataAnnotations out on the net due to their widespread use.
Implementing ValidatableBindableBase
The idea for this occurred to me because when we were putting together the Prism for Windows Runtime guidance, we wanted to support input validation even though WinRT Bindings did not support validation. To do that we implemented a ValidatableBindableBase class for our bindable objects and added some behaviors and separate controls to the UI to do the display aspects. But the code there had to go out and bind to that error information separately since the Bindings in WinRT have no notion of validation, nor do the input controls themselves.
Now that BindableBase has been added to Prism 5 for WPF, it immediately made me think “I want the best of both – what we did in ValidatableBindableBase, but tying in with the built-in support for validation in WPF bindings”.
So basically we just need to tie together the implementation of INotifyDataErrorInfo with our PropertyChanged support that we get from BindableBase and the error management container we get from ErrorsContainer.
I went and grabbed some code from Prism for Windows Runtime’s ValidatableBindableBase (all the code of the various Prism releases is open source) and pulled out the parts that do the DataAnnotations evaluation. I also followed the patterns we used there for triggering an evaluation of the validation rules when SetProperty is called by the derived data object class from its property set blocks.
The result is an override of SetProperty that looks like this:
protected override bool SetProperty(ref T storage,
T value, [CallerMemberName] string propertyName = null)
{
var result = base.SetProperty(ref storage, value, propertyName); if (result && !string.IsNullOrEmpty(propertyName))
{
ValidateProperty(propertyName);
}
return result;
}
This code calls the base class SetProperty method (which raises PropertyChanged events) and triggers validation for the property that was just set.
Then we need a ValidateProperty implementation that checks for and evaluates DataAnnotations if present. The code for that is a bit more involved:
public bool ValidateProperty(string propertyName)
{
if (string.IsNullOrEmpty(propertyName))
{
throw new ArgumentNullException("propertyName");
} var propertyInfo = this.GetType().GetRuntimeProperty(propertyName);
if (propertyInfo == null)
{
throw new ArgumentException("Invalid property name", propertyName);
} var propertyErrors = new List();
bool isValid = TryValidateProperty(propertyInfo, propertyErrors);
ErrorsContainer.SetErrors(propertyInfo.Name, propertyErrors); return isValid;
}
Basically this code is using reflection to get to the property and then it calls TryValidateProperty, another helper method. TryValidateProperty will find if the property has any DataAnnotation attributes on it, and, if so, evaluates them and then the calling ValidateProperty method puts any resulting errors into the ErrorsContainer. That will end up triggering an ErrorsChanged event on the INotifyDataErrorInfo interface, which causes the Binding to call GetErrors and display those errors.
There is some wrapping of the exposed API of the ErrorsContainer within ValidatableBindableBase to flesh out the members of the INotifyDataErrorInfo interface implementation, but I won’t show all that here. You can check out the full source for the sample (link at the end of the post) to see that code.
Using ValidatableBindableBase
We have everything we need now. First let’s define a data object class we want to bind to with some DataAnnotation rules attached:
public class Customer : ValidatableBindableBase
{
private string _FirstName;
private string _LastName;
private string _Phone; [Required]
public string FirstName
{
get { return _FirstName; }
set
{
SetProperty(ref _FirstName, value);
OnPropertyChanged(() => FullName);
}
} [Required]
public string LastName
{
get { return _LastName; }
set
{
SetProperty(ref _LastName, value);
OnPropertyChanged(() => FullName);
}
} public string FullName { get { return _FirstName + " " + _LastName; } } [CustomValidation(typeof(Customer), "CheckAcceptableAreaCodes")]
[RegularExpression(@"^\D?(\d{3})\D?\D?(\d{3})\D?(\d{4})$",
ErrorMessage="You must enter a 10 digit phone number in a US format")]
public string Phone
{
get { return _Phone; }
set { SetProperty(ref _Phone, value); }
} public static ValidationResult CheckAcceptableAreaCodes(string phone,
ValidationContext context)
{
string[] areaCodes = { "760", "442" };
bool match = false;
foreach (var ac in areaCodes)
{
if (phone != null && phone.Contains(ac)) { match = true; break; }
}
if (!match) return
new ValidationResult("Only San Diego Area Codes accepted");
else return ValidationResult.Success;
}
}
Here you can see the use of both built-in DataAnnotations with Required and RegularExpression, as well as a CustomValidation attribute pointing to a method encapsulating a custom rule.
Next we bind to it from some input controls in the UI:
<TextBox x:Name="firstNameTextBox"
Text="{Binding Customer.FirstName, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" ... />
In this case I have a ViewModel class backing the view that exposes a Customer property with an instance of the Customer class shown before, and that ViewModel is set as the DataContext for the view. I use UpdateSourceTrigger=PropertyChanged so that the user gets feedback on every keystroke about validation rules.
With just inheriting our data object (Customer) from ValidatableBindableBase, using DataAnnotation attributes on our properties we have nice validation all ready and working.
In the full demo, I show some additional bells and whistles including a custom style to show a ToolTip with the first error message when you hover over the control as shown in the screen shot earlier, and showing all errors in a simple custom validation errors summary. To do that I had to add an extra member to the ErrorsContainer from Prism called GetAllErrors so that I could get all errors in the container at once to support the validation summary. You can check that out in the downloadable code.
Calling out to Async Validation Rules
To leverage the async aspects of INotifyDataErrorInfo, lets say you have to make a service call to check a validation rule. How can we integrate that with the infrastructure we have so far?
You could potentially build this into the data object itself, but I am going to let my ViewModel take care of making the service calls. Once it gets back the async results of the validation call, I need to be able to push those errors (if any) back onto the data object’s validation errors for the appropriate property.
To keep the demo code simple, I am not really going to call out to a service, but just use a Task with a thread sleep in it to simulate a long running blocking service call.
public class SimulatedValidationServiceProxy
{
public Task> ValidateCustomerPhone(string phone)
{
return Task>.Factory.StartNew(() =>
{
Thread.Sleep(5000);
return phone != null && phone.Contains("555") ?
new string[] { "They only use that number in movies" }
.AsEnumerable() : null;
});
}
}
Next I subscribe to PropertyChanged events on my Customer object in the ViewModel:
Customer.PropertyChanged += (s, e) => PerformAsyncValidation();
And use the async/await pattern to call out to the service, pushing the resulting errors into the ErrorsContainer:
private async void PerformAsyncValidation()
{
SimulatedValidationServiceProxy proxy =
new SimulatedValidationServiceProxy();
var errors = await proxy.ValidateCustomerPhone(Customer.Phone);
if (errors != null) Customer.SetErrors(() => Customer.Phone, errors);
}
Having a good base class for your Model and ViewModel objects in which you need PropertyChanged support and validation support is a smart idea to avoid duplicate code. In this post I showed you how you can leverage some of the code in the Prism 5 library to form the basis for a simple but powerful standard implementation of async validation support for your WPF data bindable objects.
You can download the full sample project code here.
Reusable async validation for WPF with Prism 5的更多相关文章
- [WPF系列]-Prism+EF
源码:Prism5_Tutorial 参考文档 Data Validation in WPF √ WPF 4.5 – ASYNCHRONOUS VALIDATION Reusable asyn ...
- [Windows] Prism 8.0 入门(下):Prism.Wpf 和 Prism.Unity
1. Prism.Wpf 和 Prism.Unity 这篇是 Prism 8.0 入门的第二篇文章,上一篇介绍了 Prism.Core,这篇文章主要介绍 Prism.Wpf 和 Prism.Unity ...
- WPF & EF & Prism useful links
Prism Attributes for MEF https://msdn.microsoft.com/en-us/library/ee155691%28v=vs.110%29.aspx Generi ...
- Simple Validation in WPF
A very simple example of displaying validation error next to controls in WPF Introduction This is a ...
- Writing a Reusable Custom Control in WPF
In my previous post, I have already defined how you can inherit from an existing control and define ...
- 异步函数async await在wpf都做了什么?
首先我们来看一段控制台应用代码: class Program { static async Task Main(string[] args) { System.Console.WriteLine($& ...
- 【.NET6+WPF】WPF使用prism框架+Unity IOC容器实现MVVM双向绑定和依赖注入
前言:在C/S架构上,WPF无疑已经是"桌面一霸"了.在.NET生态环境中,很多小伙伴还在使用Winform开发C/S架构的桌面应用.但是WPF也有很多年的历史了,并且基于MVVM ...
- C# 一个基于.NET Core3.1的开源项目帮你彻底搞懂WPF框架Prism
--概述 这个项目演示了如何在WPF中使用各种Prism功能的示例.如果您刚刚开始使用Prism,建议您从第一个示例开始,按顺序从列表中开始.每个示例都基于前一个示例的概念. 此项目平台框架:.NET ...
- WPF MVVM,Prism,Command Binding
1.添加引用Microsoft.Practices.Prism.Mvvm.dll,Microsoft.Practices.Prism.SharedInterfaces.dll: 2.新建文件夹,Vie ...
随机推荐
- JAVA中转义字符
JAVA中转义字符 2010年08月11日 星期三 上午 12:22 JAVA中转义字符: 1.八进制转义序列:\ + 1到3位5数字:范围'\000'~'\377' \0:空字符 2.U ...
- [SQL in Azure] Getting Started with SQL Server in Azure Virtual Machines
This topic provides guidelines on how to sign up for SQL Server on a Azure virtual machine and how t ...
- matlab M文件分析工具使用(Code Analyzer and Profiler)
Code Analyzer and Profiler Matlab中,对写在m文件(.m文件)里的代码有分析的工具,可以进行优化,这里做一个简单的介绍. Code Analyzer Code Anal ...
- RSS Reader in PC & iPhone
PC上当然是用feedly web版.但出乎意料的是,iPhone上最好用的居然是safari版QQ邮箱...
- Node和Electron开发入门(四):操作PC端文件系统
一.调用PC端默认方式打开本地文件 在main.js里 // 打开系统本地文件或者网页链接 const {shell} = require('electron'); // Open a local f ...
- pre 标签的使用注意事项
.news-msg{ // padding: 5px; white-space: pre-wrap; word-wrap: break-word; font-family:'微软雅黑'; } < ...
- Python处理大数据
起因 Python处理一下数据,大概有六七个G,然后再存到另外一个文件中,单线程跑起来发现太慢了,数据总量大概是千万行的级别,然后每秒钟只能处理不到20行--遂想怎么提高一下速度 尝试1-multip ...
- 在PL/SQL中调用存储过程--oracle
在oracle10中写好了存储过程,代码如下: CREATE OR REPLACE Procedure Proc_Insert ( sName in varchar2, sAge in int, sE ...
- C#学习笔记(32)——委托改变窗体颜色
说明(2017-11-23 22:17:34): 1. 蒋坤的作业,点击窗体1里面的按钮,出现窗体2:点击窗体2里的按钮,窗体1改变背景色. 2. 做完窗体传值后,这个作业就很简单了. 代码: For ...
- 什么是POP3、SMTP和IMAP?
POP3 POP3是Post Office Protocol 3的简称,即邮局协议的第3个版本,它规定怎样将个人计算机连接到Internet的邮件服务器和下载电子邮件的电子协议.它是因特网电子邮件的第 ...