Introduction
An important part of any data-driven application is ensuring data validation and handling errors when they occur. Validation means that any data input meets the application requirements in order to ensure consistency and correctness of the data.
There is a deep discussion about when, where and how this validation should be done. Should this validation be done on the client-side or the server-side in a client/side application? Should this validation be done in the business layer or in the middleware component of a multi-layer application? Some checks are business rules and thus conceptually they must be done in the business layer of the multi-layer application or in the server-side of a client/server application. But the key components, which the end-users interact with, are those which reside in the presentation layer. Therefore, this is the first place where the validation should be done. This eliminates the possibility the end-users will incorrectly enter the values. The refining of the data is done later, when the data is passed to another layer for processing.
In this article, I will cover the principles and techniques built into Microsoft.NET for dealing with validation and error handling in the client-side. The example is based on a Windows Forms control developed by me which is based on the TextBox control and handles validation in a standardized way in order to detect input errors and display them to users.
Validation in Windows Forms
Validations concerns about checking data entry and making sure it is valid based on the criteria of the application requirements. In Windows Forms programming, validation is based around controls and events. Data-bound controls raise events when validation occurs, giving the opportunity to write validation logic. As well as the ErrorProvider component notifies the user of any validation problems. In complex controls like DataGridView control, we have built-in support for displaying validation errors as well. This prevents you to write a lot of validation code.
Every control exposes two events: Validating and Validated. The Validating event is intended to fire immediately after input has been completed but not accepted as valid. The Validated event is fired when the input is accepted as valid. The Validating event is the one that handles most of the validation logic. This event defines an event argument (CancelEventArgs) which contains the Boolean property named Cancel to set a signal that the event being fired should not be completed. Setting this property to True is a signal back to the control that the validation failed in the code that handles the event and this results that the focus won't leave the control, forcing the user to enter a correct value. Setting this property to False allows the validation process to continue.
A problem with this approach is that if the users enter an invalid value and tries to close the application, by default, they won't be able to do it. In .NET 2.0, we have the AutoValidate property on the Form class that lets you specify exactly what the behavior should be when the validation error occurs at the control level. The AutoValidate property may have four values: Disable, EnablePreventFocusChange, EnableAllowFocusChange, Inherit. Disable value disables automatic validation. If you set EnablePreventFocusChange value and Cancel is set to true, focus will remain on the control that failed the validation. If you set the EnableAllowFocusChange value and Cancel is set to true, focus is allowed to change to other control. And finally, Inherit value means that the control's behavior is determined by the AutoValidate setting of his parent.
You need to display information related to error discovered in the Validating event, then we need to use the ErrorProvider control that provides a standard way to notify the user of an error. The ErrorProvider control is an extender provider that lets you add properties to other controls. When you use the ErrorProvider, you only need to add one to your form. It maintains a mapping of error messages associated to each control in the form. If you set an error message for a control, then an error icon is shown next to the control and will also display a tooltip when you hover the mouse over the error icon. You set the error message for a control by calling the SetError method on the error provider instance.
You can validate down in the Control hierarchy by using the Validate and ValidateChildren method on container controls such as Forms.
In order to capture errors in DataSet and DataTable objects, you need to query the HasErrors Boolean property to check if there are rows with errors. If HasErrors is set to true, then you must iterate over the data set's Tables collection to determine which table has data with errors by checking the HasErrors Boolean property of each table. For a table that returns true to HasError property, if you want to explore for the errors, then you can call the GetErrors method on the DataTable object, which will return an array of DataRow objects containing the rows with errors. You can check the RowError property for each row to get additional information as well as to call GetColumnsInError method to get back an array of DataColumns, which you can then use to call the GetColumnError to extract the individual error message.
Developing the ValidatingTextBox control
Now we're going to develop the ValidatingTextBox. Because an instance of TextBox control manages its Text property as a string, the result value can be anything. Thus the ValidatingTextBox extends TextBox class adding new functionality to the base control, such as the responsibility of checking the format of the input text according to the intended data type to be associated. This new control (ValidatingTextBox) might also check whether the input is required or not. Finally, this new features of the ValidatingText are generic enough and they allow implementing a number of basic requirements.
First of all, create a new Class Library project, and a delete the default class, and finally add a new class ValidatingTextBox which inherits from the base TextBox control as well as a references to the System.Windows.Forms and System.Drawing assemblies.
We add four attributes and the underlying properties to the class. One to set the desired data type the control is going to validate, the second one to set whether or not to check if the end-user has entered a data value, the third one is a reference to a ErrorProvider control which shows the error message to the end-user and the fourth one is the custom error message(see Listing 1).
private DbType m_dtDataType = DbType.String;
private bool m_bIsRequired = false;
private ErrorProvider m_erErrorProvider = null;
private string m_strErrorMessage = null;
public DbType DataType
{
get
{
return this.m_dtDataType;
}
set
{
this.m_dtDataType = value;
}
}
public bool IsRequired
{
get
{
return this.m_bIsRequired;
}
set
{
this.m_bIsRequired = value;
}
}
public ErrorProvider ErrorProvider
{
get
{
return this.m_erErrorProvider;
}
set
{
this.m_erErrorProvider = value;
}
}
public string ErrorMessage
{
get
{
return this.m_strErrorMessage;
}
set
{
this.m_strErrorMessage = value;
}
}
Listing 1
Once the user has entered a value to the control and moves the focus from the control, then the Validating event is fired. Here, I evaluate the input value according to a correct value based on the data type selected by the property DataType. In order to check the correct format of the input value, then I use the TryParse method against the target data type. If this method returns TRUE, then the input text is correct formatted against the target data type, otherwise the input value does not conform to the constraints of the target data type. If an error is found, then it's notified to the end-user using the instance of ErrorProvider control (m_erErrorProvider). The code for the custom Validating event is shown in Listing 2.
protected override void OnValidating(System.ComponentModel.CancelEventArgs e)
{
bool bValidated = false;
bool bHasValue = !String.IsNullOrEmpty(this.Text.Trim());
if (bHasValue)
{
if ((this.m_dtDataType == DbType.Date) || (this.m_dtDataType == DbType.DateTime)|| (this.m_dtDataType == DbType.Time))
{
DateTime dummy = DateTime.Today;
bValidated = DateTime.TryParse(this.Text.Trim(), out dummy);
}
else
if(this.m_dtDataType == DbType.Int16)
{
short dummy = 0;
bValidated = Int16.TryParse(this.Text.Trim(), out dummy);
}
else
if (this.m_dtDataType == DbType.UInt16)
{
ushort dummy = 0;
bValidated = UInt16.TryParse(this.Text.Trim(), out dummy);
}
else
if (this.m_dtDataType == DbType.Int32)
{
int dummy = 0;
bValidated = Int32.TryParse(this.Text.Trim(), out dummy);
}
else
if (this.m_dtDataType == DbType.UInt32)
{
uint dummy = 0;
bValidated = UInt32.TryParse(this.Text.Trim(), out dummy);
}
else
if (this.m_dtDataType == DbType.Int64)
{
long dummy = 0;
bValidated = Int64.TryParse(this.Text.Trim(), out dummy);
}
else
if (this.m_dtDataType == DbType.UInt64)
{
ulong dummy = 0;
bValidated = UInt64.TryParse(this.Text.Trim(), out dummy);
}
else
if (this.m_dtDataType == DbType.Byte)
{
byte dummy = 0;
bValidated = Byte.TryParse(this.Text.Trim(), out dummy);
}
else
if (this.m_dtDataType == DbType.SByte)
{
sbyte dummy = 0;
bValidated = SByte.TryParse(this.Text.Trim(), out dummy);
}
else
if ((this.m_dtDataType == DbType.Currency) || (this.m_dtDataType == DbType.Decimal))
{
Decimal dummy = new Decimal(0);
bValidated = Decimal.TryParse(this.Text.Trim(), out dummy);
}
else
if (this.m_dtDataType == DbType.Double)
{
double dummy = 0;
bValidated = Double.TryParse(this.Text.Trim(), out dummy);
}
else
if (this.m_dtDataType == DbType.Single)
{
float dummy = 0;
bValidated = Single.TryParse(this.Text.Trim(), out dummy);
}
else
if((this.m_dtDataType == DbType.AnsiString)||(this.m_dtDataType == DbType.AnsiStringFixedLength)||(this.m_dtDataType == DbType.String)||(this.m_dtDataType == DbType.StringFixedLength))
{
bValidated = true;
}
}
else
if (!this.IsRequired)
{
bValidated = true;
}
if (bValidated)
{
if (this.m_erErrorProvider != null)
{
this.m_erErrorProvider.SetError(this, "");
}
this.BackColor = System.Drawing.Color.White;
e.Cancel = false;
}
else
{
if (this.m_erErrorProvider != null)
{
this.m_erErrorProvider.SetError(this, this.m_strErrorMessage);
}
this.BackColor = System.Drawing.Color.Yellow;
e.Cancel = true;
}
}
Listing 2
Conclusion
This article explained how to handle validation in Windows Forms application using my ValidatingTextBox control which can be used to implement a lot of requirements in common business scenarios.