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.

A simple application demonstrating the custom provider
A simple application demonstrating the custom provider

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.

csharp
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 it ErrorText 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.

csharp
[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.

csharp
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.

csharp
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.

csharp
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:

csharp
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.

csharp
[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:

csharp
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

Like what you're reading? Perhaps you like to buy us a coffee?

Donate via Buy Me a Coffee

Donate via PayPal


Files


Comments

# Henrik

Thank you! Great example.

Reply