In recent code, I've been trying to avoid displaying validation
errors as message boxes, but display something in-line. The .NET
Framework provides an
ErrorProvider component which does just
this. One of the disadvantages of this control is that it
displays an icon indicating error state - which means you need a
chunk of white space somewhere around your control, which may
not always be very desirable.
This article describes how to create a custom error provider component that uses background colours and tool tips to indicate error state.
Note: I don't use data binding, so the provider implementation I demonstrate below currently has no support for this.
Create a new
Component class and implement the
IExtenderProvider interface. This interface is used to add
custom properties to other controls - it has a single method
CanExtend that must return true for a given source object if
it can extend itself to said object.
In this example, we'll offer our properties to any control.
However, you can always customize this to work only with certain
control types such as
Implementing Custom Properties
Unlike how properties are normally defined, you need to create
get and set methods for each property you wish to expose. In our
case, we'll be offering
Error as an example, the methods would be
SetError. Both methods need to have a parameter for the source
object, and the set also needs a parameter for the property
Note: I named this property
Errorso I could drop in replace the new component for the .NET Framework one without changing any code bar the control declaration. If you don't plan on doing this, you may wish to name it
ErrorTextor something more descriptive!
In this example, we'll store all our properties in dictionaries, keyed on the source control. If you want to be more efficient, rather than using multiple dictionaries you could use one tied to a backing class/structure but we'll keep this example nice and simple.
Below is the implementation for getting the value.
Getting the value is straightforward, we attempt to get a custom value from our backing dictionary, if one does not exist then we return a default value.
It's also a good idea to decorate your get methods with
DefaultValue attributes. The
attribute allows you to place the property in the
(otherwise it will end up in the Misc group), while the
DefaultValue attribute does two things. Firstly, in designers
such as the
PropertyGrid, default values appear in a normal
type face whilst custom values appear in bold. Secondly, it
avoids cluttering up auto generated code files with assignment
statements. If the default value is an empty string, and the
property is set to that value, no serialization code will be
generated. (Which is also helpful if you decide to change
default values, such as the default error colour later on)
Next, we have our set method code.
As we want "unset" values to be the empty string, we have a quick null check in place to convert nulls to empty strings. If a non-empty string is passed in, we update the source control to be in it's "error" state. If it's blank, then we clear the error.
Above you can see the code to display an error. First we store the original background colour of the control if we haven't previously saved it, and then apply the error colour. And because users still need to know what the actual error is, we add a tool tip with the error text. Finally, we store the control in an internal list - we'll use that later on.
Clearing the error state is more or less the reverse. First we try and set the background colour back it what it's original value, and we remove the tool tip.
Checking if errors are present
Personally speaking, I don't like the built in
event as it prevents focus from shifting until you resolve the
error. That is a pretty horrible user experience in my view
which is why my validation runs from change events. But then,
how do you know if validation errors are present when submitting
data? You could keep track of this separately, but we might as
well get our component to do this.
When an error is shown, we store that control in a list, and then remove it from the list when the error is cleared. So we can add a very simple property to the control to check if errors are present:
At present the error list isn't exposed, but that would be easy enough to do if required.
If you now drop this component onto a form and try and use it, you'll find nothing happens. In order to get your new properties to appear on other controls, you need to add some attributes to the component.
For each new property you are exposing, you have to add a
ProviderProperty declaration to the top of the class
containing the name of the property, and the type of the objects
that can get the new properties.
With these attributes in place (and assuming you have correctly
your new component should now start adding properties to other
controls in the designer.
In this component validation is done from event handlers - you
can either use the built in
Control.Validating event, or use
the most appropriate change event of your source control. For
example, the demo project uses the following code to validate
The only thing you need to remember is to clear errors as well as display them!
As mentioned at the start of the article, the sample class doesn't support data binding.
Also, while you can happily set custom error background colours
at design time, it probably won't work so well if you try and
set the error text at design time. Not sure if the original
ErrorProvider supports this either, but it hasn't been
specifically coded for in this sample as my requirements are to
use it via change events of the controls. For this reason, when
clearing an error (or all errors), the text dictionary is always
updated, but the background colour dictionaries are left alone.
As usual, this code should be tested before being used in a production application - while we are currently using this in almost-live code, it hasn't been thoroughly tested and may contain bugs or omissions.
The sample project below includes the full source for this example class, and a basic demonstration project.
- 2013-01-01 - First published
- 2020-11-21 - Updated formatting
Like what you're reading? Perhaps you like to buy us a coffee?