zoukankan      html  css  js  c++  java
  • 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.

  • 相关阅读:
    vue 中 过滤filter 和 foreach的使用
    vuex 获取数据
    动态设置样式 calc计算
    vue-echarts-v3 使用 dataZoom属性 相关笔记
    访存加速-Speed-up of Memory Access Intensive Program
    台式机新添内存条无法开机?
    ADB的版本问题
    1184. Distance Between Bus Stops
    485. Max Consecutive Ones
    448. Find All Numbers Disappeared in an Array
  • 原文地址:https://www.cnblogs.com/itelite/p/4159800.html
Copyright © 2011-2022 走看看