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 theErrorProvider
:<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 theErrorProvider
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)
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 TextBox
es: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, theErrorProvider
exposes these methods:AddDisplayStrategy()
- call this to make the error provider aware of otherIErrorDisplayStrategy
classes. Alternatively, you can subclass theErrorProvider
and override theCreateDefaultDisplayStrategies
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 FormsErrorProvider
can't do. Calling this method simply gets the firstFrameworkElement
on the page that has an error displayed. This method is useful because you can simply call it, then call theFocus()
method on the element, to direct the user's attention to the next error they need to fix.
Validate()
works like this:- The
ErrorProvider
goes through everyFrameworkElement
on its parent control recursively, reflecting on them and looking forDependancyProperties
. - 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 theClearError()
method on all of the knownIErrorDisplayStrategies
. This means, it cleans up all errors first. - 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 thatPath
as the argument to theIDataErrorInfo
indexer that is implemented on the bound object (our data context, in this case). This returns an error string. - 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:- 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. - 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.
- It's not checked for thread safety.
- It's not optimised.
- 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.
- It's slow.
Thanks for reading, and I hope you found this demo useful!
Comments
Post a Comment