Creating a custom ErrorProvider component for use with Windows Forms applications
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.
Getting Started
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 TextBoxBase
, ListBoxControl
etc.
bool IExtenderProvider.CanExtend(object extendee)
{
return extendee is Control;
}
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
and ErrorBackColor
properties.
Using Error
as an example, the methods would be GetError
and
SetError
. Both methods need to have a parameter for the source
object, and the set also needs a parameter for the property
value.
Note: I named this property
Error
so 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 itErrorText
or 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.
[Category("Appearance"), DefaultValue("")]
public string GetError(Control control)
{
string result;
if(control == null)
throw new ArgumentNullException("control");
if(!_errorTexts.TryGetValue(control, out result))
result = string.Empty;
return result;
}
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
Category
and DefaultValue
attributes. The Category
attribute allows you to place the property in the PropertyGrid
(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.
public void SetError(Control control, string value)
{
if(control == null)
throw new ArgumentNullException("control");
if(value == null)
value = string.Empty;
if(!string.IsNullOrEmpty(value))
{
_errorTexts[control] = value;
this.ShowError(control);
}
else
this.ClearError(control);
}
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.
protected virtual void ShowError(Control control)
{
if(control == null)
throw new ArgumentNullException("control");
if(!_originalColors.ContainsKey(control))
_originalColors.Add(control, control.BackColor);
control.BackColor = this.GetErrorBackColor(control);
_toolTip.SetToolTip(control, this.GetError(control));
if (!_erroredControls.Contains(control))
_erroredControls.Add(control);
}
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.
public void ClearError(Control control)
{
Color originalColor;
if (_originalColors.TryGetValue(control, out originalColor))
control.BackColor = originalColor;
_errorTexts.Remove(control);
_toolTip.SetToolTip(control, null);
_erroredControls.Remove(control);
}
Checking if errors are present
Personally speaking, I don't like the built in Validating
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:
public bool HasErrors
{
get { return _erroredControls.Count != 0; }
}
At present the error list isn't exposed, but that would be easy enough to do if required.
Designer Support
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.
[ProvideProperty("ErrorBackColor", typeof(Control)), ProvideProperty("Error", typeof(Control))]
public partial class ErrorProvider : Component, IExtenderProvider
{
...
With these attributes in place (and assuming you have correctly
created <PropertyName>Get
and <PropertyName>Set
methods,
your new component should now start adding properties to other
controls in the designer.
Example Usage
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
integer inputs:
private void integerTextBox_TextChanged(object sender, EventArgs e)
{
Control control;
string errorText;
int value;
control = (Control)sender;
errorText = !int.TryParse(control.Text, out value) ? "Please enter a valid integer" : null;
errorProvider.SetError(control, errorText);
}
private void okButton_Click(object sender, EventArgs e)
{
if (!errorProvider.HasErrors)
{
// submit the new data
this.DialogResult = DialogResult.OK;
this.Close();
}
else
this.DialogResult = DialogResult.None;
}
The only thing you need to remember is to clear errors as well as display them!
Limitations
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.
Final words
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.
Update History
- 2013-01-01 - First published
- 2020-11-21 - Updated formatting
Downloads
Filename | Description | Version | Release Date | |
---|---|---|---|---|
ErrorProviderTest.zip
|
Sample project demonstrating creating a custom error provider that changes a control's background colour in addition to providing a tool tip. |
01/01/2013 | Download |
Leave a Comment
While we appreciate comments from our users, please follow our posting guidelines. Have you tried the Cyotek Forums for support from Cyotek and the community?
Comments
Henrik
#
Thank you! Great example.