Click here to Skip to main content
Click here to Skip to main content

Total View Validation

, 23 Jan 2009
Rate this:
Please Sign up or sign in to vote.
Cross business object validation: a more centric view.
Prize winner in Competition "Best overall article of January 2009"

Introduction

WPF is a fairly new technology (OK, it's been around a while), so it's still growing and new techniques and ideas are developing all the time. As developers, we are expected to provide users with usable systems. As part of a usable system, we must provide input validation of some sort. Now, WPF does come with a standard mechanism for this, but as you shall see, it's not perfect, and that it is limited in certain areas.

What this article will attempt to outline is a possible solution to the inbuilt WPF shortcomings. It does assume you know a little bit about WPF and validation in general, but it should cover most ideas in enough detail that WPF newbies should be OK.

What Does This Article Attempt to Solve

As I stated, this article will attempt to show you an alternative validation concept that fills the gaps with the standard WPF validation mechanism.

But what's so wrong with the standard WPF validation mechanism? Well, typically, the inbuilt WPF validation mechanism provides means of validating a single object. So it provides bounds checking for that object if you will. So how does this work? Well, although that's now the main push for this article, it is worth a quick mention...so here goes.

The Standard WPF Validation Works Likes This:

In the .NET 3.0 days, you would have typically done something like this:

<Binding Source="{StaticResource data}" Path="Age"
    UpdateSourceTrigger="PropertyChanged">
    <Binding.ValidationRules>
        <ExceptionValidationRule/>
        </Binding.ValidationRules>
</Binding>

Where you add in ValidationRules to your binding.

Which was OK, but if you wanted a specific type of validation, you had to either use the standard ValidationRule or create your own, which may look like this:

class FutureDateRule : ValidationRule
{
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        DateTime date;
        try
        {
            date = DateTime.Parse(value.ToString());
        }
        catch (FormatException)
        {
            return new ValidationResult(false, "Value is not a valid date.");
        }
        if (DateTime.Now.Date > date)
        {
            return new ValidationResult(false, "Please enter a date in the future.");
        }
        else
        {
            return ValidationResult.ValidResult;
        }
    }
}

Ouch, painful.

Luckily, in .NET 3.5, things changed for the better. We were then able to use the IDataErrorInfo interface directly on our business objects, which works like this:

public class Person : IDataErrorInfo
{

    private int age;

    public int Age
    {
        get { return age; }
        set { age = value; }
    }

    public string Error
    {
        get
        {
            return null;
        }
    }

    public string this[string name]
    {
        get
        {
            string result = null;
            if (name == "Age")
            {

                if (this.age < 0 || this.age > 150)
                {
                    result = 
                      "Age must not be less than 0 or greater than 150.";
                }
            }
            return result;
        }
    }
}

And the XAML (binding code really) would look something like this:

<Binding Source="{StaticResource data}" Path="Age"
 UpdateSourceTrigger="PropertyChanged"
 ValidatesOnDataErrors="True"   />

This is also a tad painful, as we have to write loads of rules in the business objects. I have seen several people try to ease the pain of this mechanism using various techniques such as:

You can read more about the standard WPF validation mechanisms using these links:

The big problems that I personally have with all these standard methods are:

  1. You can only validate the object that you are binding to. Now, that is OK in some cases, but my business has pretty complicated rules for data, that span across multiple objects. This is where it all falls down using the standard WPF validation techniques. I am totally unaware of how I could cross-validate a field using a value from another object using the standard mechanisms.
  2. Some companies of mine included don't really gel with the idea of having business rules inside our business object; this seems to bloat the objects somehow. We prefer lean mean lite-weight business objects. Rocky Lhotka CSLA framework advocates the use of business objects holding their own rules, and he is a very, very smart guy, so maybe it is not all that bad. It is down to personal taste, I guess.

However, there is no denying that item 1 above just can not be solved by having each business object hold its own validation rules. I mean, how would you validate a business object if it needed to do so with some knowledge of another object. Even using some of the new WPFism Design Patterns such as MVVM, we may end up with a ViewModel holding a number of business objects that drive a single View. There will be a high probability that there may actually be some cross-business object validation required in these cases.

This led me to have a think about it a bit, and it led me to actually ditch what I had thought of as a good thing; so I ditched the use of the IDataErrorInfo interface all together. But why did I do that?

So What's Your Idea?

Well, I personally think that validation logic doesn't actually belong in the objects themselves. I decided to move those rules out of the business objects. So where could I do my validating? I have to do it somewhere, right? Right indeed, so what I decided to do was stick to using a couple of WPF goodies such as:

  • Model View View Model Design Pattern
  • INotifyPropertyChanged, for happy binding

And then I decided to do the following:

  1. Remove all validation out of my business objects into a central validating object, which knew about all the relevant objects by having access to a ViewModel
  2. The ViewModel would make itself known to the validator
  3. The ViewModel would hold objects that drives the View
  4. The View would bind to the ViewModel
  5. The ViewModel would hold a list of all validation errors
  6. A new set of controls were made (I have only done TextBox, but the idea applies to any control you choose) that when bound would accept a parameter, which indicated which list of business rules it was interested in seeing validation failures for

That is pretty much it. So how does this look in code?

Well, let's see an example app (this is the demo app attached, but you can change it to suit your actual needs):

Ok, So How Does it Work

Well, quite simply, it works as follows. The actual business objects (Person classes) simply have properties that notify bindings of changes via the INotifyPropertyChanged interface mechanism. There is a single ViewModel (Window1ViewModel) object per window (you may decide to have a number of View Models to govern your page, that is up to you), which holds a number of business objects (Person classes) that the View (Window1) needs to bind to. So basically, the View will bind directly to the ViewModel (this is the MVVM pattern). The ViewModel also holds a validator (Window1Validator) which is responsible for running all business logic for a given ViewModel. As such, the validator (Window1Validator) needs to know about the ViewModel (Window1ViewModel), so that it can examine the ViewModel's (Window1ViewModel) business objects (Person classes) values, that are currently driving the View (Window1).

So, what happens next is that the View (Window1) will at some stage have to validate its contents. For the demo, that moment occurs when a validate button is clicked, which will call a Validate() method inside the associated ViewModel (Window1ViewModel). When the ViewModel is asked to validate, it simply passes the call on to its internal validator (Window1Validator). As the internal ViewModel held validator knows about the ViewModel (Window1ViewModel), it is just a case of running through whatever business validation logic you want.

When you do run through the business validation logic, a ViewModel (Window1ViewModel) held ObservableCollection<ValidationFailure> is added to, where the Key property of the ValidationFailure key will be some unique name. This could typically be the name of the property you are validating, such as "Age". The beauty of this approach is that you have access to all the objects that drive the View, which allows cross object validation. This is something that the standard WPF validation framework just doesn't cater for (to the best of my knowledge).

So once we rattle through all the validation logic, we end up with a bunch of broken rules in the form of a ObservableCollection<ValidationFailure> within the ViewModel (Window1ViewModel), that can be used to bind to. As each ValidationFailure key will be some unique name, we can strip out only the ones that match a particular field on the View, which is done by using a ValueConverter, or choose to view all of the ObservableCollection<ValidationFailure> for the entire View.

Roughly, that is how it all works, so time for some code.

The Code Examples

So I guess it is time to start examining the code. Well, let's start with the business objects themselves. The demo uses the following business object:

/// <summary>
/// A simple Person domain object
/// </summary>
public class Person : DomainObject
{
    #region Data
    private String firstName = String.Empty;
    private String lastName = String.Empty;
    private Boolean isAbleToVote = false;
    private Int32 age = 0;
    #endregion

    #region Public Properties
    public String FirstName
    {
        get { return firstName;  }
        set
        {
            firstName = value;
            NotifyChanged("FirstName");
        }
    }

    public String LastName
    {
        get { return lastName; }
        set
        {
            lastName = value;
            NotifyChanged("LastName");
        }
    }


    public Boolean IsAbleToVote
    {
        get { return isAbleToVote; }
        set
        {
            isAbleToVote = value;
            NotifyChanged("IsAbleToVote");
        }
    }


    public Int32 Age
    {
        get { return age; }
        set
        {
            age = value;
            NotifyChanged("Age");
        }
    }
    #endregion
}

Where this class inherits from a base class called DomainObject, which looks like this:

/// <summary>
/// The class all domain objects must inherit from. 
/// 
/// INotifyPropertyChanged : Provides change notification
/// to allow WPF bindings to work, without the 
/// need to inherit from a WPF specifio class.
/// So this will work even with WinForms/ASP .NET
/// </summary>
[Serializable()]
public abstract class DomainObject : INotifyPropertyChanged
{
    #region Data
    protected int id;
    #endregion

    #region Ctor
    /// <summary>
    /// Constructor.
    /// </summary>
    public DomainObject()
    {
    }
    #endregion

    #region Public Properties

    /// <summary>
    /// Gets or sets the Address primary key value.
    /// </summary>
    public int ID
    {
        get { return id; }
        set
        {
            id = value;
            NotifyChanged("ID");
        }
    }
    #endregion

    #region INotifyPropertyChanged Implementation

    /// <summary>
    /// Occurs when any properties are changed on this object.
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;


    /// <summary>
    /// A helper method that raises the PropertyChanged event for a property.
    /// </summary>
    /// <param name="propertyNames">The names
    ///             of the properties that changed.</param>
    protected virtual void NotifyChanged(params string[] propertyNames)
    {
        foreach (string name in propertyNames)
        {
            OnPropertyChanged(new PropertyChangedEventArgs(name));
        }
    }

    /// <summary>
    /// Raises the PropertyChanged event.
    /// </summary>
    /// <param name="e">Event arguments.</param>
    protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, e);
        }
    }

    #endregion
}

As you can see, the DomainObject class simply provides the INotifyPropertyChanged interface implementation. Next, let us examine the ViewModel code which looks like this for the attached demo code:

/// <summary>
/// View model for Window1
/// </summary>
public class Window1ViewModel : ViewModelBase
{
    #region Data
    private Window1Validator window1Validator = null;
    private Person currentPerson1 = new Person();
    private Person currentPerson2 = new Person();
    private ObservableCollection<ValidationFailure>
        validationErrors = new ObservableCollection<ValidationFailure>();

    private ICommand validateCommand;
    #endregion

    #region Ctor

    public Window1ViewModel()
    {
        //initialise validator for this view model
        window1Validator = new Window1Validator(this);

        //wire up command
        validateCommand = new SimpleCommand
        {
            CanExecuteDelegate = x => true,
            ExecuteDelegate = x => Validate()
        };
    }

    #endregion

    #region Public Properties

    /// <summary>
    /// Return an ICommand that can execute the search
    /// </summary>
    public ICommand ValidateCommand
    {
        get { return validateCommand; }
    }


    public ObservableCollection<ValidationFailure> ValidationErrors
    {
        get { return validationErrors; }
        set
        {
            validationErrors = value;
            NotifyChanged("ValidationErrors");
        }
    }

    public Person CurrentPerson1
    {
        get { return currentPerson1; }
        set
        {
            currentPerson1 = value;
            NotifyChanged("CurrentPerson1");
        }
    }

    public Person CurrentPerson2
    {
        get { return currentPerson2; }
        set
        {
            currentPerson2 = value;
            NotifyChanged("CurrentPerson2");
        }
    }

    #endregion

    #region Public Methods
    public void Validate()
    {
        window1Validator.Validate();
    }
    #endregion

}

Again, this class inherits from a base class that offers several useful things such as the INotifyPropertyChanged interface implementation. It can also be seen that this example ViewModel holds several business objects that will be used by the View to bind to. This ViewModel is then used to bind against in the View, where the DataContext of the View is set to be this ViewModel. Let us have a look at the View next.

/// <summary>
/// Demonstrates an alternative way fo validation across all objects within a
/// View, rather than using IDataErrorInfo which must be done on your actual
/// DomainObject and can only validate internal properties.
/// </summary>
public partial class Window1 : Window
{
    private Window1ViewModel window1ViewModel = new Window1ViewModel();

    public Window1()
    {
        InitializeComponent();
        this.DataContext = window1ViewModel;
    }
}

As you can see, the View is using the Window1ViewModel ViewModel as its DataContext, which allows controls on the View to bind to the ViewModel directly. I am not going to bore you with all the XAML for the View, but will just show you a typical binding for one of the ViewModel held business objects.

<!-- FirstName Property-->
<StackPanel Orientation="Horizontal" Margin="5">
    <Label Content="FirstName" Width="100" />
    <local:TextBoxEx x:Name="txtFirstName1" Width="150" 
       Height="25" FontSize="12"  FontWeight="Bold" 
       ValidationErrors="{Binding Path=ValidationErrors, Mode=OneWay, 
          Converter={StaticResource ValidationErrorsLookupConv}, 
          ConverterParameter='FirstName1'}"
       Text="{Binding Path=CurrentPerson1.FirstName, UpdateSourceTrigger=PropertyChanged}"
       Foreground="Black" HorizontalAlignment="Center" 
       HorizontalContentAlignment="Center"    />
</StackPanel>

So you can see that we have a specialised TextBox (TextBoxEx) which is being bound to the ValidationErrors property of the current DataContext of the View (which is really the Window1ViewModel ViewModel). It can also be seen that we are using a ValueConverter (ValidationErrorsLookupConverter) where we feed a parameter value into the ValueConverter.

To understand that mechanism, we need to first understand how the ValidationErrors property works within the Window1ViewModel ViewModel, so let us examine that now.

Basically, what happens is when a new ViewModel (Window1ViewModel) is constructed, it creates a new validator object (Window1Validator) which is used to perform all validation on the ViewModel (Window1ViewModel). The validator object (Window1Validator) simply runs through a chunk of really boring rules code, and appends a new ValidationFailure object to the list of ObservableCollection<ValidationFailure> held within the ViewModel (Window1ViewModel).

Let us examine the code for the validator object (Window1Validator); it is pretty dull stuff, but that is the nature of validation code.

/// <summary>
/// Validation code for Window1
/// </summary>
public class Window1Validator
{
    #region Data
    private Window1ViewModel window1ViewModel { get; set; }
    #endregion

    #region Ctor
    public Window1Validator(Window1ViewModel window1ViewModel)
    {
        this.window1ViewModel = window1ViewModel;
    }
    #endregion

    #region Public Methods
    public void Validate()
    {
        ObservableCollection<ValidationFailure> localValidationErrors= 
            new ObservableCollection<ValidationFailure>();

        #region Validate CurrentPerson1

        if (window1ViewModel.CurrentPerson1.Age < 0)
            localValidationErrors.Add(
                new ValidationFailure("Age1", 
                    "Person 1 Age cant be < 0"));

        if (window1ViewModel.CurrentPerson1.Age > 65 
            && window1ViewModel.CurrentPerson1.IsAbleToVote)
            localValidationErrors.Add(
                new ValidationFailure("Age1", 
                    "Person 1 Age, You can't vote > 65"));

        if (window1ViewModel.CurrentPerson1.FirstName == String.Empty)
            localValidationErrors.Add(
                new ValidationFailure("FirstName1", 
                    "Person 1 FirstName can't be empty"));

        if (window1ViewModel.CurrentPerson1.LastName == String.Empty)
            localValidationErrors.Add(
                new ValidationFailure("LastName1", 
                    "Person 1 LastName can't be empty"));
        

        #endregion

        #region Validate CurrentPerson2

        if (window1ViewModel.CurrentPerson1.Age < 18 
            && window1ViewModel.CurrentPerson2.Age == 0)
            localValidationErrors.Add(
                new ValidationFailure(
                    "Age2", 
                    "Person 2 Age cant be < 0 if Person1 Age < 18"));

        if (window1ViewModel.CurrentPerson2.Age > 65 
            && window1ViewModel.CurrentPerson2.IsAbleToVote)
            localValidationErrors.Add(
                new ValidationFailure("Age2", 
                    "Person 2, You can't vote > 65"));

        if (window1ViewModel.CurrentPerson2.FirstName == String.Empty)
            localValidationErrors.Add(
                new ValidationFailure("FirstName2", 
                    "Person 2 FirstName can't be empty"));

        if (window1ViewModel.CurrentPerson2.LastName == String.Empty)
            localValidationErrors.Add(
                new ValidationFailure("LastName2", 
                    "Person 2 LastName can't be empty"));


        #endregion

        window1ViewModel.ValidationErrors = localValidationErrors;
    }
    #endregion
}

The important part to note here is that the ViewModel (Window1ViewModel) holds a complete list of all the ValidationFailures that occurred, and that each ValidationFailure has a key that can be used to grab only those ValidationFailures that are related to a specific TextBox. Which is exactly what happens within the ValueConverter (ValidationErrorsLookupConverter) where we fed a parameter value into the ValueConverter, which is used to obtain only those ValidationFailures that pertain to the TextBox that has the correct key (value converter parameter value), which will be used to filter the list of all the ValidationFailures to only those that match the key (value converter parameter value).

Recall this binding for a single TextBoxEx within the XAML:

I am not going to bore you with all the XAML for the View, but will just show you a typical binding for one of the ViewModel held business objects.

<!-- FirstName Property-->
<StackPanel Orientation="Horizontal" Margin="5">
    <Label Content="FirstName" Width="100" />
    <local:TextBoxEx x:Name="txtFirstName1" Width="150" 
      Height="25" FontSize="12"  FontWeight="Bold" 
      ValidationErrors="{Binding Path=ValidationErrors, Mode=OneWay, 
                        Converter={StaticResource ValidationErrorsLookupConv}, 
                        ConverterParameter='FirstName1'}"
      Text="{Binding Path=CurrentPerson1.FirstName, UpdateSourceTrigger=PropertyChanged}"
      Foreground="Black" HorizontalAlignment="Center" 
      HorizontalContentAlignment="Center"    />
</StackPanel>

See the ValidationErrors part, and the ConverterParameter='FirstName1', that is the part that enables us to grab only those validation errors that we need for the current TextBox.

This may become clearer when you see the ValueConverter (ValidationErrorsLookupConverter).

/// <summary>
/// Obtains a sub set of all validation errors from the
/// Bound object (ViewModel) that matches a particular key
/// for the actual bound control
/// </summary>
[ValueConversion(typeof(ObservableCollection<ValidationFailure>), 
    typeof(ObservableCollection<ValidationFailure>))]
public class ValidationErrorsLookupConverter : IValueConverter
{
    #region IValueConverter implementation
    public object Convert(object value, Type targetType, 
                  object parameter, CultureInfo culture)
    {
        if (value != null)
        {
            ObservableCollection<ValidationFailure> validationLookup =
          (ObservableCollection<ValidationFailure>)value;

            List<ValidationFailure> failuresForKey = 
                (   
                    from vf in validationLookup
                    where vf.Key.Equals(parameter.ToString())
                    select vf
                ).ToList();

            return new ObservableCollection<ValidationFailure>(failuresForKey);

        }

        return null;
    }

    public object ConvertBack(object value, Type targetType, 
                  object parameter, CultureInfo culture)
    {
        throw new NotImplementedException("Can't convert back");
    }
    #endregion
}

The last step was in having a specialised TextBox (TextBoxEx) which knows how to show its own list of validation errors in a popup. Now, this is just what I chose to do, you may think up something different, but that is what I chose to do.

I should also point out that generally I am against creating specialised controls that inherit from System.Windows.Controls, as most extra behavior can be added via attached properties, but this just didn't seem to fit in this case, as I wanted the popup within the XAML to be triggered by mouse moves etc.

Josh Smith will more than likely say, oh you just do this, then that, and there you have it. I will just smile back at the man and go "That's nice to know, thanks Josh".

Since writing this article, a reader, "SE_GEEK", has proposed a new control which inherits from ContentControl instead of TextBox; you can read more about that approach using the forum link: GlobalWPFValidation.aspx?msg=2895494#xx2895494xx.

Anyway, here is the code for the specialised TextBox (TextBoxEx) which knows how to show its own list of ValidationFailures within a popup. I have only done a specialised TextBox (TextBoxEx) for the attached demo code, but you could apply the same idea to any of the standard controls. At work, we have actually done that with CheckBox/ComboBox and other controls, and it works very well.

/// <summary>
/// A Simple extentended TextBox that supports our 
/// custom validation collection & logic
/// </summary>
[TemplatePart(Name = "PART_PopupErrors", Type = typeof(Popup))]
[TemplatePart(Name = "PART_Close", Type = typeof(Button))]
public class TextBoxEx : TextBox
{
    #region Data
    private Popup errorPopup = null;
    private Button cmdClose = null;
    #endregion

    #region Ctor
    public TextBoxEx() : base()
    {
        this.MouseEnter += (s, e) =>
        {
            if (errorPopup != null && !IsValid)
            {
                errorPopup.IsOpen = false;
                errorPopup.IsOpen = true;
            }
        };
    }
    #endregion

    #region Private Methods

    private void ErrorPopup_MouseUp(object sender, 
    System.Windows.Input.MouseButtonEventArgs e)
    {
        if (errorPopup.IsOpen)
            errorPopup.IsOpen = false;
    }

    private void CmdClose_Click(object sender, RoutedEventArgs e)
    {
        Button button = e.OriginalSource as Button;
        Popup pop = button.Tag as Popup;
        if (pop != null)
            pop.IsOpen = false;
    }

    #endregion

    #region DPs
    #region IsValid

    /// <summary>
    /// IsValid Dependency Property
    /// </summary>
    public static readonly DependencyProperty IsValidProperty =
        DependencyProperty.Register("IsValid", typeof(bool), typeof(TextBoxEx),
            new FrameworkPropertyMetadata((bool)true));

    /// <summary>
    /// Gets or sets the IsValid property. This dependency property 
    /// indicates ....
    /// </summary>
    public bool IsValid
    {
        get { return (bool)GetValue(IsValidProperty); }
        set { SetValue(IsValidProperty, value); }
    }

    #endregion

    #region NewStyle

    /// <summary>
    /// NewStyle Dependency Property
    /// </summary>
    public static readonly DependencyProperty NewStyleProperty =
        DependencyProperty.Register("NewStyle", typeof(Style), typeof(TextBoxEx),
            new FrameworkPropertyMetadata((Style)null,
                new PropertyChangedCallback(OnNewStyleChanged)));

    /// <summary>
    /// Gets or sets the NewStyle property.
    /// </summary>
    public Style NewStyle
    {
        get { return (Style)GetValue(NewStyleProperty); }
        set { SetValue(NewStyleProperty, value); }
    }

    /// <summary>
    /// Handles changes to the NewStyle property.
    /// </summary>
    private static void OnNewStyleChanged(DependencyObject d, 
    DependencyPropertyChangedEventArgs e)
    {
        ((TextBoxEx)d).Style = e.NewValue as Style;
    }


    #endregion

    #region ValidationErrors

    /// <summary>
    /// ValidationErrors Dependency Property
    /// </summary>
    public static readonly DependencyProperty ValidationErrorsProperty =
        DependencyProperty.Register("ValidationErrors",
            typeof(ObservableCollection<ValidationFailure>), 
        typeof(TextBoxEx),
            new FrameworkPropertyMetadata(
        (ObservableCollection<ValidationFailure>)null,
                new PropertyChangedCallback(OnValidationErrorsChanged)));

    /// <summary>
    /// Gets or sets the ValidationErrors property.
    /// </summary>
    public ObservableCollection<ValidationFailure> ValidationErrors
    {
        get { return (ObservableCollection<ValidationFailure>)
        GetValue(ValidationErrorsProperty); }
        set { SetValue(ValidationErrorsProperty, value); }
    }

    /// <summary>
    /// Handles changes to the ValidationErrors property.
    /// </summary>
    private static void OnValidationErrorsChanged(DependencyObject d, 
        DependencyPropertyChangedEventArgs e)
    {
        ObservableCollection<ValidationFailure> failures =
            e.NewValue as ObservableCollection<ValidationFailure>

        TextBoxEx thisObj = (TextBoxEx)d;

        if (failures == null)
            thisObj.IsValid = true;
        else thisObj.IsValid = failures.Count == 0 ? true : false;
    }



    #endregion

    #endregion

    #region Overrides

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        errorPopup = this.Template.FindName("PART_PopupErrors", this) as Popup;
        if (errorPopup != null)
            errorPopup.MouseUp += new MouseButtonEventHandler(ErrorPopup_MouseUp);

        cmdClose = this.Template.FindName("PART_Close", this) as Button;
        if (cmdClose != null)
            cmdClose.Click += new RoutedEventHandler(CmdClose_Click);
    }

    #endregion
}

And, here is the XAML that provides an actual Style for the TextBoxEx control:

<Style TargetType="{x:Type local:TextBoxEx}">
    <Setter Property="SnapsToDevicePixels" Value="true"/>
    <Setter Property="OverridesDefaultStyle" Value="true"/>
    <Setter Property="FocusVisualStyle"    Value="{x:Null}"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:TextBoxEx}">
                <Grid>
                    <!-- POPUP-->
                    <Popup x:Name="PART_PopupErrors"
                       PlacementTarget="{Binding ElementName=Bd}"   
                       Placement="Relative"
                       AllowsTransparency="True"
                       PopupAnimation="Slide"
                       HorizontalOffset ="20"
                       StaysOpen="False"
                       VerticalOffset="20">
                        <Border HorizontalAlignment="Stretch" 
                            BorderBrush="WhiteSmoke"
                            Background="Red"
                            Margin="0"
                            Width="250"
                            VerticalAlignment="Stretch"
                            Height="120"
                            Opacity="0.97"
                            BorderThickness="2" 
                            CornerRadius="3" >
                            <Border HorizontalAlignment="Stretch" 
                                BorderBrush="Red"
                                Background="White"
                                Margin="0"
                                VerticalAlignment="Stretch"
                                Opacity="1"
                                BorderThickness="5" 
                                CornerRadius="0" >

                                    ......
                                    ......
                                    ......
                                    ......

                                    <ScrollViewer Grid.Row="1" Margin="0" 
                                          HorizontalScrollBarVisibility="Auto" 
                                          VerticalScrollBarVisibility="Auto">

                                        <ItemsControl Margin="0" 
                                          BorderThickness="0"  
                                          ItemsSource="{Binding 
                                            RelativeSource={RelativeSource Mode=FindAncestor, 
                                            AncestorType={x:Type local:TextBoxEx}}, 
                                            Path=ValidationErrors}"/>

                                    </ScrollViewer>
                                </Grid>
                            </Border>
                        </Border>
                    </Popup>

                    <!-- Control-->
                    <Border SnapsToDevicePixels="true" x:Name="Bd"
                        Height="{TemplateBinding Height}" 
                            Width="{TemplateBinding Width}"
                        VerticalAlignment="Center"
                        Background="{TemplateBinding Background}" 
                        CornerRadius="2"
                        BorderBrush="Black" 
                        BorderThickness="2">
                        <ScrollViewer x:Name="PART_ContentHost" 
                          SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" 
                          TextElement.FontSize="{TemplateBinding FontSize}"  
                          VerticalAlignment="Center" 
                          VerticalContentAlignment="Center" 
                          Margin="2,0,0,0"/>
                    </Border>
                </Grid>
                <ControlTemplate.Triggers>

                    <Trigger Property="IsValid" Value="False">
                        <Setter Property="BorderBrush" 
                                TargetName="Bd" Value="Red"/>
                    </Trigger>

                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

And that is pretty much it. With this mechanism, we are able to perform pretty much any cross business object validation we like for the current View. And to prove that, here are a couple of screenshots:

Here is a Single pop for a single TextBoxEx object:

And here is what I get for all the errors for the current View:

That's it

That is all I have to say. I hope this article has helped you a little. If you liked it, could you please be kind enough to leave a vote or a message? Thanks.

Amendment

Since publishing this article, a colleague (Colin Eberhardt) has informed me that as of .NET 3.5 SP1, this is actually possible using the standard WPF mechanisms of BindingGroups. As .NET 3.5 SP1 is such a monster, I am not that surprised I missed this. Anyway, Colin has written an excellent post on this, and you can read all about it over at his blog. Here is a link: BindingGroups for Total View Validation.

License

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

Share

About the Author

Sacha Barber
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)
 
- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence
 
Both of these at Sussex University UK.
 
Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
GeneralMy vote of 5 PinmemberMario Majcica27-Sep-11 22:14 
Questionnews in .NET 4.0 PinmemberKoshiguruma4-Aug-11 0:16 
AnswerRe: news in .NET 4.0 PinmvpSacha Barber4-Aug-11 0:26 
GeneralDisplayed text belongs in the View PinmemberMartin Joel21-Jul-10 4:18 
GeneralRe: Displayed text belongs in the View PinmvpSacha Barber21-Jul-10 4:51 
GeneralUsing in a DataTemplate and DataGrid PinmemberChrDressler27-Dec-09 9:24 
GeneralRe: Using in a DataTemplate and DataGrid PinmvpSacha Barber27-Dec-09 21:00 
QuestionWhere should validation reside? PinmemberAsher Barak29-Jul-09 20:18 
AnswerRe: Where should validation reside? PinmvpSacha Barber29-Jul-09 22:08 
You missed the point here.
 
This article allows you to validate across objects, whilst the standard WPF mechanism of IDataErrorInfo only allows validating inside one object. That was not good enough for us in some cases.
 
I am not against it, this is just a different way.
 
Sacha Barber
  • Microsoft Visual C# MVP 2008/2009
  • Codeproject MVP 2008/2009
Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue
 
My Blog : sachabarber.net

GeneralRe: Where should validation reside? PinmemberAsher Barak30-Jul-09 2:05 
GeneralRe: Where should validation reside? PinmvpSacha Barber30-Jul-09 2:06 
GeneralIDataErrorInfo is for reporting errors PinmemberDarren Pruitt28-Jan-09 8:01 
GeneralRe: IDataErrorInfo is for reporting errors PinmvpSacha Barber28-Jan-09 10:13 
GeneralAn non-view model alternative PinmemberColin Eberhardt26-Jan-09 3:35 
GeneralRe: An non-view model alternative PinmvpSacha Barber26-Jan-09 4:47 
GeneralRe: An non-view model alternative PinmvpSacha Barber26-Jan-09 4:57 
GeneralRe: An non-view model alternative PinmemberColin Eberhardt26-Jan-09 5:20 
GeneralRe: An non-view model alternative PinmvpSacha Barber26-Jan-09 6:12 
GeneralRe: An non-view model alternative PinmemberPhilipp Sumi28-Jan-09 4:54 
GeneralRe: An non-view model alternative PinmvpSacha Barber28-Jan-09 5:02 
GeneralCommon model validation PinmemberHabos26-Jan-09 2:13 
GeneralRe: Common model validation PinmvpSacha Barber26-Jan-09 2:19 
GeneralNice article... PinmvpPete O'Hanlon25-Jan-09 9:26 
GeneralRe: Nice article... PinmvpSacha Barber25-Jan-09 21:44 
GeneralMy vote of 2 PinmemberProJester125-Jan-09 8:03 
GeneralGreat Job and work around for control extension PinmemberSE_GEEK24-Jan-09 11:24 
GeneralRe: Great Job and work around for control extension PinmvpSacha Barber24-Jan-09 21:13 
GeneralRe: Great Job and work around for control extension PinmemberSE_GEEK25-Jan-09 6:16 
GeneralRe: Great Job and work around for control extension PinmemberSE_GEEK25-Jan-09 6:56 
GeneralRe: Great Job and work around for control extension PinmvpSacha Barber25-Jan-09 7:01 
GeneralRe: Great Job and work around for control extension PinmemberSE_GEEK25-Jan-09 9:50 
GeneralRe: Great Job and work around for control extension PinmvpSacha Barber25-Jan-09 21:45 
GeneralGood Job! PinmemberTonyJ24-Jan-09 10:36 
GeneralRe: Good Job! PinmvpSacha Barber24-Jan-09 21:07 
GeneralRe: Good Job! PinmemberTonyJ25-Jan-09 16:49 
GeneralRe: Good Job! PinmvpSacha Barber25-Jan-09 21:47 
GeneralRe: Good Job! PinmemberTonyJ27-Jan-09 7:52 
GeneralRe: Good Job! PinmvpSacha Barber27-Jan-09 21:52 
GeneralI liked it.. PinmemberRajesh Pillai24-Jan-09 4:05 
GeneralRe: I liked it.. PinmvpSacha Barber24-Jan-09 6:06 
GeneralSuper cool! [modified] PinmemberSundar Subramaniam24-Jan-09 1:58 
GeneralRe: Super cool! PinmvpSacha Barber24-Jan-09 6:06 
GeneralVery nice Interesting PinmvpAbhijit Jana24-Jan-09 0:36 
GeneralRe: Very nice Interesting PinmvpSacha Barber24-Jan-09 6:05 

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

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

| Advertise | Privacy | Mobile
Web02 | 2.8.140814.1 | Last Updated 24 Jan 2009
Article Copyright 2009 by Sacha Barber
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid