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的更多相关文章

  1. [WPF系列]-Prism+EF

      源码:Prism5_Tutorial   参考文档 Data Validation in WPF √ WPF 4.5 – ASYNCHRONOUS VALIDATION Reusable asyn ...

  2. [Windows] Prism 8.0 入门(下):Prism.Wpf 和 Prism.Unity

    1. Prism.Wpf 和 Prism.Unity 这篇是 Prism 8.0 入门的第二篇文章,上一篇介绍了 Prism.Core,这篇文章主要介绍 Prism.Wpf 和 Prism.Unity ...

  3. WPF & EF & Prism useful links

    Prism Attributes for MEF https://msdn.microsoft.com/en-us/library/ee155691%28v=vs.110%29.aspx Generi ...

  4. Simple Validation in WPF

    A very simple example of displaying validation error next to controls in WPF Introduction This is a ...

  5. 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 ...

  6. 异步函数async await在wpf都做了什么?

    首先我们来看一段控制台应用代码: class Program { static async Task Main(string[] args) { System.Console.WriteLine($& ...

  7. 【.NET6+WPF】WPF使用prism框架+Unity IOC容器实现MVVM双向绑定和依赖注入

    前言:在C/S架构上,WPF无疑已经是"桌面一霸"了.在.NET生态环境中,很多小伙伴还在使用Winform开发C/S架构的桌面应用.但是WPF也有很多年的历史了,并且基于MVVM ...

  8. C# 一个基于.NET Core3.1的开源项目帮你彻底搞懂WPF框架Prism

    --概述 这个项目演示了如何在WPF中使用各种Prism功能的示例.如果您刚刚开始使用Prism,建议您从第一个示例开始,按顺序从列表中开始.每个示例都基于前一个示例的概念. 此项目平台框架:.NET ...

  9. WPF MVVM,Prism,Command Binding

    1.添加引用Microsoft.Practices.Prism.Mvvm.dll,Microsoft.Practices.Prism.SharedInterfaces.dll: 2.新建文件夹,Vie ...

随机推荐

  1. IOS UIView自动调整尺寸

    自动尺寸调整行为 当您改变视图的边框矩形时,其内嵌子视图的位置和尺寸往往也需要改变,以适应原始视图的新尺寸.如果视图的autoresizesSubviews属性声明被设置为YES,则其子视图会根据au ...

  2. 【Unity】第8章 GUI开发

    分类:Unity.C#.VS2015 创建日期:2016-04-27 一.简介 前面的章节中实际上已经多次使用了GUI,只不过用法都比较简单,这一章系统地介绍Unity 5.x自带的GUI(称为Uni ...

  3. PC-Lint概念与基本操作

    1.   PC-Lint工具介绍 PC-Lint for C/C++是由Gimpel软件公司于1985年开发的代码静态分析工具,它能有效地发现程序语法错误.潜在的错误隐患.不合理的编程习惯等. C语言 ...

  4. (原创)舌尖上的c++--相逢

    引子 前些时候,我在群里出了一道题目:将变参的类型连接在一起作为字符串并返回出来,要求只用函数实现,不能借助于结构体实现.用结构体来实现比较简单: template<typename... Ar ...

  5. (原创)用c++11打造好用的variant(更新)

    关于variant的实现参考我前面的博文,不过这第一个版本还不够完善,主要有这几个问题: 内部的缓冲区是原始的char[],没有考虑内存对齐: 没有visit功能. 没有考虑赋值构造函数的问题,存在隐 ...

  6. 【Acm】八皇后问题

    八皇后问题,是一个古老而著名的问题,是回溯算法的典型例题. 其解决办法和我以前发过的[算法之美—Fire Net:www.cnblogs.com/lcw/p/3159414.html]类似 题目:在8 ...

  7. 【Linux技术】磁盘的物理组织,深入理解文件系统

    磁盘即是硬盘,由许多块盘片(盘面)组成,每个盘片的上下两面都涂有磁粉,磁化后可以存储信息数据.每个盘片的上下两面都安装有磁头,磁头被安装在梳状的可以做直线运动的小车上以便寻道,每个盘面被格式化成有若干 ...

  8. kindeditor自定义插件插入视频代码

    kindeditor自定义插件插入视频代码 1.添加插件js 目录:/kindeditor/plugins/diy_video/diy_video.js KindEditor.plugin('diy_ ...

  9. axios 的应用

    vue更新到2.0之后,作者就宣告不再对vue-resource更新,而是推荐的axios,前一段时间用了一下,现在说一下它的基本用法. 首先就是引入axios,如果你使用es6,只需要安装axios ...

  10. 如何去掉文件里的^M

    起因 csv文件用Python处理之后,有的地方跟着一个^M,特别好奇,以为是处理过程中产生的,后来想了想不是. 解决办法 尝试使用replace替换掉,但是失败了 查询原因,谷歌一番,发现是Wind ...