Click here to Skip to main content
6,822,123 members and growing! (17,115 online)
Email Password   helpLost your password?
Platforms, Frameworks & Libraries » Windows Presentation Foundation » General     Intermediate License: The Code Project Open License (CPOL)

Total View Validation

By Sacha Barber

Cross business object validation a more centric view
C# (C#3.0), .NET (.NET3.0, .NET3.5), WPF, CEO, Architect, Dev
Revision:5 (See All)
Posted:23 Jan 2009
Views:32,022
Bookmarked:109 times
Unedited contribution
Prize winner in Competition "Best overall article of January 2009"
printPrint   add Share
      Discuss Discuss   Broken Article?Report  
54 votes for this article.
Popularity: 8.19 Rating: 4.73 out of 5

1

2
3 votes, 5.6%
3
6 votes, 11.1%
4
45 votes, 83.3%
5

Introduction

WPF is a fairly new technology (ok its been around a while) so its still growing and new techniques and idea 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 its not perfect, and that it is limited in certain areas.

What this article will attempt to outline is 1 possible solution to the inbuilt WPF frameworks 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 1 field using a value from another object using the standard mechanisms.
  2. Some companies 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 framwork 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 business object one validate 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 one 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. So 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 lets 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 decided 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 ViewModels (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 vlidate button is clicked which will call a Validate() method inside the associated ViewModel (Window1ViewModel). When the ViewModel is asked to validate, it simply passed 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 what ever 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 beaty 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 lets 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 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 append 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:

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 knew 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 the 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 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 pretty much perform any cross business object validation we like for the current View. And to prove that here are a couple of screen shots.

Here is a Single pop for a single TextBoxEx object.

And here is what I get for all 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 collegue (Colin Eberhardt) has informed me that as of .NET 3.5 SP1, this is actually possibly using the standard WPF mechanisms of BindingGroups. As .NET 3.5 SP1 was such a monster I am not that surprised I missed this. Anywayt 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)

About the Author

Sacha Barber


Member
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 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

Occupation: Software Developer (Senior)
Location: United Kingdom United Kingdom

Other popular Windows Presentation Foundation articles:

Article Top
You must Sign In to use this message board.
FAQ FAQ 
 
Noise Tolerance  Layout  Per page   
 Msgs 1 to 25 of 39 (Total in Forum: 39) (Refresh)FirstPrevNext
GeneralUsing in a DataTemplate and DataGrid PinmemberChrDressler10:24 27 Dec '09  
GeneralRe: Using in a DataTemplate and DataGrid PinmvpSacha Barber22:00 27 Dec '09  
GeneralWhere should validation reside? PinmemberAsher Barak21:18 29 Jul '09  
GeneralRe: Where should validation reside? PinmvpSacha Barber23:08 29 Jul '09  
GeneralRe: Where should validation reside? PinmemberAsher Barak3:05 30 Jul '09  
GeneralRe: Where should validation reside? PinmvpSacha Barber3:06 30 Jul '09  
GeneralIDataErrorInfo is for reporting errors PinmemberDarren Pruitt9:01 28 Jan '09  
GeneralRe: IDataErrorInfo is for reporting errors PinmvpSacha Barber11:13 28 Jan '09  
GeneralAn non-view model alternative PinmemberColin Eberhardt4:35 26 Jan '09  
GeneralRe: An non-view model alternative PinmvpSacha Barber5:47 26 Jan '09  
GeneralRe: An non-view model alternative PinmvpSacha Barber5:57 26 Jan '09  
GeneralRe: An non-view model alternative PinmemberColin Eberhardt6:20 26 Jan '09  
GeneralRe: An non-view model alternative PinmvpSacha Barber7:12 26 Jan '09  
GeneralRe: An non-view model alternative PinmemberPhilipp Sumi5:54 28 Jan '09  
GeneralRe: An non-view model alternative PinmvpSacha Barber6:02 28 Jan '09  
GeneralCommon model validation PinmemberHabos3:13 26 Jan '09  
GeneralRe: Common model validation PinmvpSacha Barber3:19 26 Jan '09  
GeneralNice article... PinmvpPete O'Hanlon10:26 25 Jan '09  
GeneralRe: Nice article... PinmvpSacha Barber22:44 25 Jan '09  
GeneralMessage Automatically Removed PinmemberProJester19:03 25 Jan '09  
GeneralGreat Job and work around for control extension PinmemberSE_GEEK12:24 24 Jan '09  
GeneralRe: Great Job and work around for control extension PinmvpSacha Barber22:13 24 Jan '09  
GeneralRe: Great Job and work around for control extension PinmemberSE_GEEK7:16 25 Jan '09  
GeneralRe: Great Job and work around for control extension PinmemberSE_GEEK7:56 25 Jan '09  
GeneralRe: Great Job and work around for control extension PinmvpSacha Barber8:01 25 Jan '09  

General General    News News    Question Question    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

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

PermaLink | Privacy | Terms of Use
Last Updated: 23 Jan 2009
Editor:
Copyright 2009 by Sacha Barber
Everything else Copyright © CodeProject, 1999-2010
Web20 | Advertise on the Code Project