Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

An Error Provider for Windows Presentation Foundation

0.00/5 (No votes)
19 Jun 2006 1  
An implementation of an ErrorProvider for Windows Presentation Foundation.

Sample Image - wpfErrorProvider.png

Introduction

For the past few months, I've been working on a shared source accounting application called Trial Balance. Trial Balance is a personal project of mine, and is designed to be a demonstration of how I think developers should approach creating a rich client application using the Windows Presentation Foundation.

Whilst developing the new UI for Trial Balance, one of the hurdles I ran into recently was the lack of an ErrorProvider control, similar to what there is in Windows Forms.

Under Windows Forms, if you have a group of controls (e.g., text boxes) that are data-bound to a given data source, you can drag an ErrorProvider component onto the form and set its DataSource to the same data source the text boxes use. The ErrorProvider will then automagically display any errors on your objects, with no need to write validation code on the UI.

In this article, I'll demonstrate my version of the ErrorProvider, written specifically for the Windows Presentation Foundation. I'm posting this because I expect a lot of people will be wondering how to emulate this behaviour. I've also implemented a Strategy Pattern for displaying the errors, to keep the provider as reusable as possible.

I'd like to point out right now that this isn't anywhere near finished, and should be considered a "proof of concept". Please report any issues or suggestions as a comment on this article.

Before I get started though, thanks go to Mike Brown from the MSDN WPF forums who showed me the use of the WPF LogicalTreeHelper class. This class is the one that makes it easy to recurse through the layers of WPF elements on a window. Thanks Mike!

Using Paul's WPF ErrorProvider

Here's a very basic example of some XAML that makes use of the ErrorProvider:

<Window x:Class="PaulStovell.Samples.WpfValidation.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:validation="clr-namespace:PaulStovell.Samples.WpfValidation" 
    Title="WpfErrorProvider" Height="300" Width="300"
    >
    <StackPanel>
        <validation:ErrorProvider x:Name="_errorProvider" />


        <StackPanel>
        <StackPanel Orientation="Horizontal">
            <TextBlock>Name:</TextBlock>
            <TextBox  Text="{Binding Path=Name}" />
        </StackPanel>

        <StackPanel Orientation="Horizontal">
            <TextBlock>Description:</TextBlock>
            <TextBox  Text="{Binding Path=Description}" />
        </StackPanel>
        </StackPanel>
    </StackPanel>
</Window>

The ErrorProvider itself is a FrameworkElement, so it can be used inside your XAML. The only constraint so far is that the ErrorProvider element must be "lower down" in the stack of framework controls. I'll explain why lower down.

The ErrorProvider gets its error messages from its DataContext. This needs to be a class that implements the System.ComponentModel's IDataErrorInfo interface, as well as INotifyPropertyChanged. You can look at the included Account.cs class to see a rough implementation of these, or read my (much longer) Delegates and Business Objects article for a more in-depth discussion of the topic.

By default, the DataContext of the ErrorProvider would be set to the data context of its parent (or its parents' parent, or its parents' par...), so to use it, you should really only need to place it onto the right place on your Window. However, you could also assign the DataContext property explicitly if you like.

Extending the Error Provider

The demonstration code I've uploaded contains three classes. The first is the ErrorProvider class itself. The second is an interface called IErrorDisplayStrategy. This class defines a couple of methods that you'll need to implement to create your own ways of displaying errors:

  • bool CanDisplayForElement(FrameworkElement element)
  • void DisplayError(FrameworkElement element, string errorMessage)
  • void ClearError(FrameworkElement element)

When you add the ErrorProvider to your form, it maintains a list of IErrorDisplayStrategy objects. When it needs to display an error for a given WPF control, it will consult this list, looking for a strategy that will work on the given element.

The third class I've included is TextBoxErrorDisplayStrategy. This is an implementation of IErrorDisplayStrategy, designed to handle TextBoxes:

public class TextBoxErrorDisplayStrategy : IErrorDisplayStrategy {
    private Dictionary<FrameworkElement, Brush> _savedBrushes;
    private Dictionary<FrameworkElement, string> _savedToolTips;
    private Color _errorBorderColor;
    private static readonly Color _errorBorderColorDefault = 
                   Color.FromRgb(0xFF, 0x42, 0x2F);
    
    public TextBoxErrorDisplayStrategy() {
        _savedBrushes = new Dictionary<FrameworkElement, Brush>();
        _savedToolTips = new Dictionary<FrameworkElement, string>();
        _errorBorderColor = _errorBorderColorDefault;
    }
    
    public Color ErrorBorderColor {
        get { return _errorBorderColor; }
        set { _errorBorderColor = value; }
    }
    
    public bool CanDisplayForElement(FrameworkElement element) {
        return element is TextBox;
    }
    
    public void DisplayError(FrameworkElement element, string errorMessage) {
        TextBox textBox = (TextBox)element;
    
        if (!_savedBrushes.ContainsKey(element)) {
            _savedBrushes.Add(element, 
               (Brush)textBox.GetValue(TextBox.BorderBrushProperty));
        }
        if (!_savedToolTips.ContainsKey(element)) {
            _savedToolTips.Add(element, 
              (string)textBox.GetValue(TextBox.ToolTipProperty));
        }
    
        textBox.SetValue(TextBox.BorderBrushProperty, 
                         new SolidColorBrush(_errorBorderColor));
        textBox.SetValue(TextBox.ToolTipProperty, errorMessage);
    }

    public void ClearError(FrameworkElement element) {
        TextBox textBox = (TextBox)element;
    
        if (_savedBrushes.ContainsKey(element)) {
            textBox.SetValue(TextBox.BorderBrushProperty, 
                             _savedBrushes[element]);
            _savedBrushes.Remove(element);
        }
        if (_savedToolTips.ContainsKey(element)) {
            textBox.SetValue(TextBox.ToolTipProperty, 
                             _savedToolTips[element]);
            _savedToolTips.Remove(element);
        }
    }
}

When told to display an error for a TextBox, it simply changes the border color and sets a tool tip. It gets a little complicated because it needs to maintain a list of changes so that they can be rolled back when the errors are cleared.

Using the Strategy Pattern means you can manipulate the element any way you like, and restore it any way you like. This is a big advantage over the standard Windows Forms ErrorProvider, which simply gives you an icon and tooltip.

The ErrorProvider Class

Externally, the ErrorProvider exposes these methods:

  • AddDisplayStrategy() - call this to make the error provider aware of other IErrorDisplayStrategy classes. Alternatively, you can subclass the ErrorProvider and override the CreateDefaultDisplayStrategies method.
  • RemoveDisplayStrategy()
  • Validate() - this method checks all the bound controls on the form. I'll discuss this more below.
  • Clear() - removes all displayed errors.
  • GetFirstInvalidElement() - this is something the Windows Forms ErrorProvider can't do. Calling this method simply gets the first FrameworkElement on the page that has an error displayed. This method is useful because you can simply call it, then call the Focus() method on the element, to direct the user's attention to the next error they need to fix.

Calling Validate() works like this:

  1. The ErrorProvider goes through every FrameworkElement on its parent control recursively, reflecting on them and looking for DependancyProperties.
  2. When it finds a dependency property, it looks for any data bindings that it might have. If it has one, and the data context for the element is the same as the data context for the ErrorProvider, it then calls the ClearError() method on all of the known IErrorDisplayStrategies. This means, it cleans up all errors first.
  3. It remembers that list of bindings that it came across while clearing the errors. Since each binding has a Path which points to a property, it uses that Path as the argument to the IDataErrorInfo indexer that is implemented on the bound object (our data context, in this case). This returns an error string.
  4. It knows the framework element that the binding belongs to, so it cycles through the known IErrorDisplayStrategies, looking for one that matches the type of framework element it needs. When it finds one, it tells it to display the error.

Known issues, bugs, and complaints

Again, I'd like to stress that this code isn't complete. It's hardly tested, and it does have issues. Here are a few I know of already:

  1. It's slow. If one property changes, and you have 10 bound properties, all 10 will be re-checked. I could do some weird caching to try and reduce that, but I'll worry about that when I actually feel the performance degrading. I believe the Windows Forms ErrorProvider works the same way.
  2. It's slow. The recursion over every framework element isn't nice. Again, I could cache them the first time, but you might get strange behaviour if you dynamically add controls to the window.
  3. It's not checked for thread safety.
  4. It's not optimised.
  5. It expects you're using data binding, and has no way to set an error explicitly. I'll probably add that when I come across a need for it.
  6. It's slow.

If anyone comes up with a better way to implement these things, I'd like to see it. I'd probably prefer if you could code up your own solution and blog it, or make a CodeProject article, rather than sending me lots of little code snippets to integrate. If you make an alternative, or improve on the approach, I'll happily add a link in this article.

Thanks for reading, and I hope you found this demo useful!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here