Click here to Skip to main content
15,119,285 members
Articles / Desktop Programming / WPF
Tip/Trick
Posted 6 Feb 2015

Stats

23.5K views
470 downloads
23 bookmarked

WPF Base ViewModel

Rate me:
Please Sign up or sign in to vote.
4.14/5 (3 votes)
6 Feb 2015CPOL4 min read
ViewModel base for all ViewModels. Validation system with DataAnnotations and IDataErrorInfo

Introduction

In this tip, I will try to present a view model that can act as base for all our view models using WPF, or other technologies. The ideas behind can also be useful in case of entities, DTOs, or other type of object. The goal of this view model is to make our life easier with very common operations that we have to do when developing user interfaces.

Background

You have to be familiar with WPF and C# to understand the code and additionally, you have to know about MVVM pattern to understand the goal better.

Using the Code

The first thing that our ViewModel should do is let us work with WPF in implementing INotifyPropertyChanged.

Additionally, many times, I face the situation in which I would like to know if the ViewModel has been modified, and even binding it. An example could be to show/hide save or cancel buttons, mark something in red to let the user know that he has a pending change to save, ...

Of course, we always need a validation system, as easy as possible and the most close to .NET validation standard systems.

Another common point, but this is something that can be discussed, is to know when a ViewModel is busy, getting data, saving,,...

And the most important, all of these operations should work via Binding and should be extremely easy to use.

Architecture Overview

We will simply have a couple of interfaces (segregation), one for validating and the other to represent the view model itself. The result is that IViewModel should implement INotifyPropertyChanged, IValidatable and IDataErrorInfo. The ViewModel will be an abstract class that inherits from Validatable, which implements nearly the whole validation system.

Image 1

Modified View Model

Based on the property IsModified and a bit special handling of the property changed event, we will be able to know automatically when a ViewModel is modified.

C#
protected virtual void RaisePropertyChanged([CallerMemberName] string property = "")
{
    RaisePropertyChanged(false, property);
}

protected virtual void RaisePropertyChanged(bool causesModification,
    [CallerMemberName] string property = "")
{
    IsModified = IsModified || causesModification;

    if (PropertyChanged != null)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(property));
    }
}

I use the CallerMemberAttribute to avoid specifying the property name (you will need .NET 4.5 if I´m not wrong). By default, a property changed does not modify the view model, you can switch this behaviour if you want and set the default to true. Whenever we want to modify our view model, we should use it in this way.

C#
RaisePropertyChanged(true);

Usually, when we fill our view models, at the end, we will set the IsModified property to false.

Validation

To have an extensible system, the validation will be done by DataAnnotations attributes, that are used in many other .NET components.

To integrate it with WPF and validate the controls automatically, we will use IDataErrorInfo, also common part of the .NET.

Sometimes, DataAnnotations is not enough to validate, so we will add a custom way to validate complex stuff. To accomplish this, we will need an additional attribute, that we call ValidatableProperty.

C#
[System.AttributeUsage(AttributeTargets.Property)]
public class ValidatableProperty : Attribute
{

}

The base class to validate is the following:

C#
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace ViewModel
{
    public abstract class Validatable : IValidatable
    {

        /// <summary>
        /// Gets if the entity has errors.
        /// </summary>
        [Browsable(false)]
        public virtual bool IsValid
        {
            get { return string.IsNullOrWhiteSpace(Error); }
        }

        /// <summary>
        /// Gets the first error found.
        /// </summary>
        public virtual string Error
        {
            get
            {
                string retVal = string.Empty;

                PropertyInfo[] properties = this.GetType().GetProperties();

                foreach (PropertyInfo info in properties)
                {
                    if (info.Name != "Error" && info.Name != "IsValid")
                    {
                        retVal = GetError(info.Name);

                        if (!string.IsNullOrWhiteSpace(retVal))
                        {
                            break;
                        }
                    }
                }

                return retVal;
            }
        }

        /// <summary>
        /// Error calculation from IDataErrorInfo
        /// Gets the first error for the given property
        /// </summary>
        /// <param name="columnName">property name</param>
        /// <returns></returns>
        public abstract string this[string columnName] { get; }

        /// <summary>
        /// With this method you can apply custom validation if it is needed for a complex and
        /// really special type of validation that can´t be perform or is not generic at all
        /// with a custom data annotation validation attribute
        /// </summary>
        /// <param name="propertyName">The property that requires validation</param>
        /// <returns>The error message</returns>
        protected virtual string OnCustomPropertyValidation(string propertyName)
        {
            return string.Empty;
        }

        /// <summary>
        /// Gets the first error found (if any) from the given property name
        /// </summary>
        /// <param name="propertyName"></param>
        /// <returns></returns>
        protected virtual string GetError(string propertyName)
        {
            string retVal = string.Empty;

            PropertyInfo propertyInfo = this.GetType().GetProperty(propertyName);
            var results = new List<ValidationResult>();

            if (propertyInfo.GetCustomAttribute<ValidatableProperty>() != null)
            {
                var result = Validator.TryValidateProperty(
                                          propertyInfo.GetValue(this, null),
                                          new ValidationContext(this, null, null)
                                          {
                                              MemberName = propertyName
                                          },
                                          results);

                if (!result)
                {
                    retVal = results.First().ErrorMessage;
                }
                else
                {
                    retVal = OnCustomPropertyValidation(propertyInfo.Name);
                }
            }

            return retVal;
        }
    }
}

As you see, the method GetError gets by reflection all ValidatableProperties and validates the DataAnnotations on it. Additionally, if there is no error, it will call the custom "OnCustomPropertyValidation" that we can override in our view models.

Finally, we have to implement the indexer required by IDataErrorInfo, that I leave abstract to implement it in our base view model. In this way, we can trigger a property change of the property IsValid, and for example, disable a save button.

C#
public override string this[string columnName]
{
    get
    {
        string retVal = GetError(columnName);

        RaisePropertyChanged(false, "IsValid");

        return retVal;
    }
}

An example could be:

C#
[ValidatableProperty]
[Required]
public string UserName
{
    get
    {
        return _userName;
    }
    set
    {
        if (_userName != value)
        {
            _userName = value;
            RaisePropertyChanged(true);
        }
    }
}

Loading Flag

To avoid having a big article, I will simply leave it here as a property you can set in your "async" methods. It could also be split to separate base view models or interfaces, that contain methods for loading data, saving,.. but the goal here is to remark this flag just as an idea. How we usually use it, is by binding it to a busy control that becomes visible when the loading is set to true. In this way, we don´t need to disable the whole UI, just those parts that are asynchronously getting data, or saving or so on.

A XAML example

XML
<Window x:Class="MyWPFApplication.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <BooleanToVisibilityConverter x:Key="boolToVisibility"/>
    </Window.Resources>
    <StackPanel Width="150">   
        
        <TextBox Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged, 
        ValidatesOnDataErrors=True}"
                Margin="5"/>
        <Button Content="Save" 
                Visibility="{Binding IsModified, Converter={StaticResource boolToVisibility}}"
                IsEnabled="{Binding IsValid}"/>
        <Button Content="Cancel" 
                Visibility="{Binding IsModified, Converter={StaticResource boolToVisibility}}"/>
    </StackPanel>
</Window>

Conclusions

This is, in my opinion, how a view model should look like and the basis it should contain. I also want to remark the validation system as it is extremely comfortable and powerful. We also have similar approaches for our entities and DTOs, but with some additional properties, like an Id. Please let me know your opinion and don´t forget to download the full source (from the link at the top of the tip) with a little example to understand it better.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

ogomerub
Engineer
Austria Austria
No Biography provided

Comments and Discussions

 
QuestionMinor optimization for GetError Pin
Austin Mullins9-Feb-15 13:23
MemberAustin Mullins9-Feb-15 13:23 
I'm not sure how helpful this is, but I've heard that assembly reflections are computationally expensive. To that end, it seems like an easy minor optimization would be to overload GetError with a version that takes a PropertyInfo variable as its argument. This way, you could avoid calling this.GetType().GetProperty() after you already got the info variable. Here's what I mean:

C#
/// <summary>
        /// Gets the first error found (if any) from the given property name
        /// </summary>
        /// <param name="propertyName">The name of the validatable property</param>
        /// <returns>The first error found (if any)</returns>
        protected virtual string GetError(string propertyName)
        {
            PropertyInfo propertyInfo = this.GetType().GetProperty(propertyName);
            return GetError(propertyInfo);
        }
        
        /// Gets the first error found (if any) from the given property info
        /// </summary>
        /// <param name="propertyInfo">The <see cref="PropertyInfo"/> object returned
        ///    by a previous call to <c>this.GetType().GetProperties()</c></param>
        /// <returns>The first error found (if any)</returns>
        protected virtual string GetError(PropertyInfo propertyInfo)
        {
            string retVal = string.Empty;
            var results = new List<ValidationResult>();

            if (propertyInfo.GetCustomAttribute<ValidatableProperty>() != null)
            {
                var result = Validator.TryValidateProperty(
                                          propertyInfo.GetValue(this, null),
                                          new ValidationContext(this, null, null)
                                          {
                                              MemberName = propertyInfo.Name
                                          },
                                          results);

                if (!result)
                {
                    retVal = results.First().ErrorMessage;
                }
                else
                {
                    retVal = OnCustomPropertyValidation(propertyInfo.Name);
                }
            }

            return retVal;
        }


Then this new overload could be called from the IsError getter.
AnswerRe: Minor optimization for GetError Pin
ogomerub10-Feb-15 7:44
Memberogomerub10-Feb-15 7:44 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.