10. Validating Data Input and Handling Errors

An important part of any data-driven application is ensuring data consistency and handling errors when they occur. Ensuring data consistency requires a combination of data input validation and concurrency protections at the data access level. Validation means ensuring that any user input meets the application’s expectations for what that data should contain. Data concurrency issues arise when two or more users or pieces of code can access and modify the same data at the same time. Whenever dealing with data, errors can occur at a number of levels, and you need to be prepared to deal with those errors in a way that minimizes the impact on users and yet ensures the consistency and correctness of the data. This chapter describes the mechanisms that are built into the .NET Framework for dealing with validation and error handling, and mentions some ways to go beyond what is provided out of the box.

Unfortunately, error handling is one of those arenas where it is difficult to generalize very much. Every application is different, and how it needs to react to errors will be different based on the particular type of error, who the users are, and what the application requirements say the application should do in the face of certain kinds of errors. Error handling is definitely something you want to be thinking about upfront and designing for all along the way. You need to anticipate where things could go wrong and what to do about them when they do. Once you know what errors you need to protect against, the validation and error handling mechanisms in Windows Forms provide a standardized way that you can detect input errors and display them to the user. Failing to validate data early in your application processing can have severe performance and reliability impacts by transmitting data across the network that will cause failures or inconsistency downstream.

Windows Forms Validation

Validation is all about checking input from the user and making sure it is valid, based on some criteria that you, as the application designer, determine. Examples of validation checks include

•    Checking for required input in a particular field

•    Checking whether input data is a valid numeric value

•    Checking for input to be within some range of values

•    Checking for particular patterns within an input string

•    Invoking complex business rules that check dependencies between other business entities and values in your application

Validation can and should occur at multiple levels in a large application. The first line of defense is to check the input from users at the point where they put it in—at the form and control level. However, in a large application, that data is often passed down into a business layer and/or data access layer through some remote technology such as a Web service. There could be any amount of intervening code through which the values have to pass before they are persisted to a data store. Data validation should occur when the user inputs data, and it should also occur at boundary crossings, such as when the data is submitted from a smart client application into the middle tier that provides the business services for that application.

How much intelligence is embedded in the presentation tier (Windows Forms) application is a design decision, but in general, in a layered application architecture, you want to keep your presentation layer or tier as thin as possible. You will want to enforce validation checks at the form level that are relatively invariant, but defer complex checks that may vary over time to the business layer. Regardless of how much validation logic you put at the forms level, there is a rich infrastructure built into Windows controls and forms to allow you to perform the checking that you need when you need it.

As with most aspects of Windows Forms programming, validation is based around controls and events. Data-bound controls raise events when validation occurs, giving you an opportunity to write custom code to perform the validation. In addition to events that are raised by the controls, an ErrorProvider control that is part of the Framework makes it straightforward to notify the user of any validation problems in a standardized and easy way to understand, from the perspective of both the end user and the programmer who hooks it up. Complex controls like the DataGridView control have built-in support for displaying validation errors in-situ to notify the user of a problem right where it occurs. There is built-in support at the forms level to cascade checks for validation up and down the control hierarchy. This prevents you from needing to write a ton of code to check and make sure the controls on a form are all happy with the data that they are containing. All of these things come together to give you a number of ways and opportunities to make sure that only good data gets into your application, and that when errors do occur, you can give clear indications to users to help them correct the problem.

Handling Validation Events

Every Windows Forms control exposes two events, Validating and Validated, which they inherit from the base Control class. When or whether these events ever fire depends on the design of the derived control, certain properties in the container control to which a control belongs, and what programmatic code is invoked with respect to validation. The Validating event is intended to fire immediately after input has been completed, but before it has been accepted as valid. The Validated event fires after the input has been accepted as valid.

The Validating event is the one you will handle most often for data-binding scenarios. When a control decides that input is complete, typically because the focus is shifting to another control on the form, it should fire the Validating event. This event is of type CancelEventHandler, which takes an event argument of type CancelEventArgs. The CancelEventArgs class contains a single Boolean property named Cancel that you can set to signal that the event being fired shouldn’t be completed. Setting Cancel to true is a signal back to the control that validation failed in the code that handles the event.

For example, say you want to write some code in a login form that checks a username field. A simple example that you could write to confirm that some value was entered would be to subscribe to the Validating event for the TextBox control that takes the username with the following handler:

private void OnUsernameValidating(object sender,
   CancelEventArgs e)
{
   if (string.IsNullOrEmpty(m_UsernameTextBox.Text))
   {
      e.Cancel = true;
      MessageBox.Show("Username is a required field");
   }
}

This code is part of the form that contains the m_UsernameTextBox control and gets invoked by the form when the focus switches from that control to some other control on the form. The code uses the IsNullOrEmpty method on the String class to check whether the text box is empty. If so, it sets the CancelEventArgs argument’s Cancel property to true. Setting this argument to true is a signal to the validation infrastructure of Windows Forms that validation has failed on that control, which will terminate the validation process by default. The default value of Cancel is false, which allows the Validation process to continue. A message box is then shown to give the user some (crude) feedback about what the problem is.

By default, a couple of things happen when you set the CancelEventArgs.Cancel property to true in your Validating event handler. The first is that the focus won’t leave that control, forcing the user to correct the problem before being able to move on to input data in other controls (see Figure 10.1). This may be a good thing in many situations, because it makes it so users can’t get too far out of context from where they made an input error before correcting it. This will also prevent the Validated event from firing since the validation process didn’t complete.

FIGURE 10.1: Control Validation Process

Control Validation Process

However, there are several problems with this approach. First, you may not always want to force users to correct their errors immediately; you may want to let them complete an entire form of entries and just force them to resolve any problems before submitting or saving the data. This allows rapid data entry for people who spend their days repeatedly filling out the same form over and over. In those cases, if they are tabbing from field to field, they don’t have to constantly look at the form to see if the focus wasn’t allowed to shift to the next control because of a validation failure.

Another problem with this approach is that if users try to close a form that has validation errors that are being handled by canceling the Validating event, by default they won’t be able to close the form. The act of clicking on another control, such as the window frame buttons (the X button), causes a focus change, which triggers validation, which fails and sets the focus back onto the control that failed. Finally, this approach requires that the control first obtain the focus, then give up the focus to another control before the validation process will be invoked.

Luckily .NET 2.0 introduces the AutoValidate property on the Form class that lets you specify exactly what the behavior should be when a validation error occurs at the control level. This property is discussed later in this chapter.

DataGridView Validation Events

The DataGridView control is a Windows Forms control derived from the Control base class, and it is a complex data container. The DataGridView control lets you handle validation at several levels. The grid itself is a control, and thus raises Validating and Validated events when the focus shifts away from the grid. However, usually you will want to handle validating a little closer to the data that is being validated, either at the individual row or cell level. To allow this, the DataGridView fires RowValidating and RowValidated events each time the selected row changes within the grid. Likewise, as the focus shifts from cell to cell within the grid, CellValidating and CellValidated events fire as well.

These events follow the same pattern as the control validating events, letting you cancel validation by setting their event argument Cancel property to true. In the case of the RowValidating event, the event argument type is DataGridViewCellCancelEventArgs; for CellValidating, the event argument type is DataGridViewCellValidatingEventArgs. Both of these types give you access to the current RowIndex and ColumnIndex, and they have a Cancel property that can be set to true to cancel validation. The DataGridView control is designed to keep the focus on the current cell if validation fails.

As you may remember from earlier in the book, each time you shift focus in a data-bound grid, a CellParsing event fires for the cell you are leaving, and a CellFormatting event fires for the cell you have moved to. These events let you modify the displayed data as it goes out to and comes in from the data source, respectively. With respect to validation, you should be aware that the CellValidating event fires before the CellParsing event. So the validation logic you apply in a handler for the CellValidating event should validate against the display patterns for the cell, which don’t necessarily map directly to the storage patterns for the corresponding data member in the data source. If you are doing conversions or formatting for display purposes, and want to validate the data before it gets pushed back into the underlying data source but after the parsing process has occurred, you will want to call that logic in the CellParsing event handler, not the CellValidating event handler.

Validation Up the Control Hierarchy

The ContainerControl class (which Form and UserControl derive from) defines a Validate method that will validate the currently focused control in the container, and then will walk up the control hierarchy to each of its ancestors in the control tree, validating each of them as well. For a typical dialog-style form, each control is a child of the form, so the only ancestor of every control on the form is the form itself. The Form class does nothing in response to validation itself, because it doesn’t directly contain input data. If you have a form that contains other container controls, such as user controls or split containers, then the container control will be the immediate ancestor of any controls it contains, and the form will be the ancestor for the container control.

The Validate method was often used in .NET 1.1 applications to let you programmatically check whether all the controls on the form were valid. However, because the Validate method only checks the currently focused control and its ancestors, you had to check all controls on the form to iterate through the Controls collection on the form, set the focus to each one, and then call Validate. This approach was tedious and problematic, so a better approach was needed. You can still call Validate on the form or on a user control to programmatically invoke validation on the currently focused control if needed, but you will probably want to use the new ValidateChildren method more often.

If you choose to use the Validate method, it returns true if validation is successful, which again is determined by whether the focused control, or any of its ancestor controls, sets the Cancel property on the Validating event argument to true. If the focused control or any control up the control hierarchy from that control votes no by setting Cancel to true, then the Validate method will return false, and your code should take appropriate measures to make users aware of the problem, and you will usually want to prevent them from moving on until the problem is corrected.

Displaying Validation Errors with the ErrorProvider Control

In the example of handling the Validating event at the beginning of this chapter, I used the crude approach of popping up a message box when a validation error occurred. Although this works, it is extremely disruptive to most users to use pop-up dialogs for something like this. Windows Forms 1.0 included the ErrorProvider control that provides a standard and less obtrusive way to notify a user of an error.

The error provider (the ErrorProvider class) control is a special kind of control, called an extender provider control, that lets you add properties to other controls on the form from a single instance of the extender provider. When you use an error provider control, you only need to add one to your form, and it shows up in the nonvisual components tray at the bottom of the designer (see Figure 10.2).

FIGURE 10.2: ErrorProvider Control in Designer

ErrorProvider Control in Designer

The error provider control maintains a mapping of error messages associated with each control on the form. If you set an error message for a control that isn’t null or an empty string, the error provider extender control will draw an error icon next to that control and will also display a tooltip when you hover the mouse over the error icon, as shown in Figure 10.3. You set an error message for a control by calling the SetError method on the error provider control instance in your form. The SetError method takes two arguments: a reference to the control for which you are setting the error, and the error message to set.

FIGURE 10.3: ErrorProvider Control in Action

ErrorProvider Control in Action

Typically, you will set the error provider error message in response to the Validating event discussed earlier. For example, if you want to validate the user’s password when the focus leaves the password text box, you could have a handler for the Validating event that looks like this:

private void OnPasswordValidating(object sender,
   CancelEventArgs e)
{
   if (!string.IsNullOrEmpty(m_UsernameTextBox.Text) &&
!CheckPasswordForUser())
   {
      m_ErrorProvider.SetError(m_PasswordTextBox, "Password is
incorrect");
   }
   else

   {
     m_ErrorProvider.SetError(m_PasswordTextBox, null);
   }
}

This handler will be invoked by default when the focus shifts from the password text box to some other control on the form. The first thing the code does is to see if the username text box is empty. If so, it doesn’t have enough information to make a decision about the password. If the username has been provided, it calls a helper method to check the user’s password. This method could go out to a database or look up the user in some other credential store. If the username has been provided, and the password checks out, then the error provider error message is set to null for the password text box control. Otherwise, it is set to an appropriate error message. The error provider will display the notification icon adjacent to the text box, as shown in Figure 10.3, and will use the provided error message for the tooltip.

The icon shown in Figure 10.3 is the default icon used by the control, but you can customize this by setting the Icon property to an instance of a System.Drawing.Icon object. Additionally, the default behavior is for the icon to blink initially at a blink rate of 250 milliseconds to grab the user’s attention. These settings can be easily changed through the BlinkStyle and BlinkRate properties. If you want different behavior for different controls on the form, you will need to use separate instances of the error provider control with the appearance properties set statically, and then only set error messages for a specific control against the appropriate instance of the error provider.

You can also tie the error provider to a data source and let it extract any error information from the data source. The ErrorProvider class exposes DataSource and DataMember properties that let you tie it into a data source the same way as other bound controls on the form. If an error gets set through the data source, and the data items in the data source implement the IDataErrorInfo interface (as described in Chapter 7 and later in this chapter), then the error provider will display as described here next to any controls that are bound to the data items that have errors in the data source. You can do this in lieu of explicitly calling SetError on the error provider for each control in the Validating event handler, as long as the data source objects support providing their own error information. However, you may still want to supplement the error messages that the data source provides by calling SetError on the error provider when you detect certain kinds of validation errors in your forms, because the data source doesn’t have context information about where and how it is being used that your form code would be aware of.

DataGridView Error Displays

Once again, the DataGridView requires special treatment with respect to displaying error information because of the complexity of data it is capable of displaying. The DataGridView control has built-in support for displaying error information at both the row and cell levels. The way it works is quite simple. Just like working with an error provider at the form level, you set the ErrorText property on either a row or a cell. When you do, an error provider-like icon will appear on the row or cell, with its tooltip set to whatever error text you set (see Figure 10.4).

FIGURE 10.4: Row and Cell Errors in a DataGridView

Row and Cell Errors in a DataGridView

Typically you will want to set only one or the other. As you can see in Figure 10.4, the icon shows up in the row header when you set the ErrorText property on a row, and in the far right side of a cell when you set it on a cell.

These same error indications will be displayed by the DataGridView if there are errors returned from the data source itself, instead of being set directly on the grid’s cells or rows. Data source errors are discussed later in this chapter.

DataGridView DataError Event

There are a lot of different places that things can go wrong with bound data in a DataGridView control. The data that goes into the grid could come from direct input from the user if you allow the grid to support editing and adding new rows, or it could be programmatically changed behind the scenes. You could have complex cell types that have some error in their processing or presentation of their values.

The DataError event on the DataGridView lets you provide centralized processing code for handling errors of many different types that can occur to the data within a DataGridView control or from the underlying data source. The event passes an event argument of type DataGridViewDataErrorEventArgs, which carries a bunch of context information about the error along with it. This event argument type has the properties shown in Table 10.1.

TABLE 10.1: DataGridViewDataErrorEventArgs Class Properties

Image

TABLE 10.2: DataGridViewDataErrorContexts Flags Enumeration Values

Image

For example, if a property’s get or set blocks in a custom data object that is bound to a row in the DataGridView control throw an exception, the DataError event will fire with a context value of Display or Commit, respectively.

As an example, consider if you bound a DataGridView control to a binding source, which was bound to a List<SimpleDataItem>. The SimpleDataItem class is defined as:

class SimpleDataItem
{
   private int m_SomeVal;

   public int SomeVal
   {
      get { throw new ArgumentException("foo"); }
      set { m_SomeVal = value; }
   }

   private string m_SomeVar;

   public string Var
   {
      get { return m_SomeVar; }
      set { m_SomeVar = value; }
   }
}

Then assume you added a handler to your form for the DataError event on the grid as follows:

private void OnDataError(object sender,
   DataGridViewDataErrorEventArgs e)
{
   string msg =
      string.Format(
         "DataError occurred: {0} {1} DataErrorContext: {2}",
         e.Exception.GetType().ToString(),e.Exception.Message,
         e.Context);
   MessageBox.Show(msg);
}

As the DataGridView attempted to render the contents of the SomeVal property for each row, you would get the MessageBox shown in Figure 10.5.

FIGURE 10.5: DataError Event Handling

DataError Event Handling

The context value may be combined with one of the other flags if additional context can be determined by the grid. There are lots of other subtle things that can go wrong internally in the grid as it is parsing input values and trying to put them into the data source as edited values. These too can raise the DataError event, letting you trap the information and either log it or possibly conditionally handle the problem based on the context argument.

Controlling Validation Behavior with the AutoValidate Property

By default, Windows Forms 2.0 applications will behave just like previous versions if you set the Cancel property of the Validating event’s CancelEventArgs argument to true—this prevents the focus from leaving that control and terminates the validation process, so Validated won’t fire. This is done for backward-compatibility reasons.

However, a new property has been introduced to the ContainerControl class that will let you modify this behavior if desired. If you plan to perform validation programmatically for the entire form, or just don’t want to force users to correct problems one control at a time, you can change the value of the AutoValidate property for the form. This property takes a value of the AutoValidate enumeration type, which can be set to any of the values described in Table 10.3. The default value for this property is EnablePreventFocusChange, which models the .NET 1.1 behavior.

TABLE 10.3: AutoValidate Enumeration Values

Image

Typically, you will set AutoValidate equal to EnableAllowFocusChange or Disable if you plan to perform manual validation at a form level using the ValidateChildren method, which is described in the next section.

Validation down the Control Hierarchy

As mentioned earlier, the Validate method performs validation on the currently focused control and each of its ancestors. However, a more common need is to validate all of the controls on the form in response to some event, such as the user pressing a Save or Submit button. In .NET 2.0 this becomes extremely easy, and you have a number of options to control the scope of what happens as part of the validation process.

If you want to perform validation for all the controls on a form, or a subset based on some common attributes, you can call the ValidateChildren method of the ContainerControl class. The Form, UserControl, and several other classes derive from ContainerControl, so they inherit this validation functionality. There are two overloads of the ValidateChildren method, one with no argument and one with a ValidationConstraints enumeration value (see Table 10.4).

TABLE 10.4: ValidationConstraints Enumeration Values

Image

The ValidationConstraints enumeration is a flags enumeration, which means its values can be combined with Boolean operators. So if you want to validate all child controls on a form that are selectable, visible, and tab stops, you would call ValidateChildren like this:

ValidateChildren(ValidationConstraints.Selectable |
   ValidationConstraints.Visible |
   ValidationConstraints.TabStop);

Extended Validation Controls

The good thing about the validation events model and the ErrorProvider control is that they are simple and straightforward to use. The bad thing about them is that to cover every control in a complex form requires a ton of separate event handlers, and each one has to contain the custom code that looks at the value being validated and makes a decision about its validity and what to do about it.

Validation code can do any number of things, but it typically takes on certain patterns for most cases. The following are the four common validation checks that you will often need.

•    Required input validation: Checks to make sure some value was put in to a specific control, without making a judgment about whether it is an appropriate value at the UI level.

•    Input range validation: Checks the input value against a range of acceptable values.

•    Input comparison validation: Compares the values in two or more input controls and ensures that they all have the same value.

•    Input pattern validation: Checks the input to ensure it complies with some textual pattern, such as a Social Security Number or phone number having dashes in the right place, a string having the appropriate case or length, and so on. You can easily perform that kind of checking using the power of regular expressions.

If you have done any ASP.NET development, you know that all of these kinds of validation are covered by a set of Web server validation controls that you can place on a Web page to perform input validation against other controls on the page. ASP.NET includes a RequiredFieldValidator, CompareValidator, RangeValidator, and RegularExpressionValidator to cover the four kinds of validation described earlier in this section. There is also a ValidationSummary control that lets you display the error messages caused by validation failures all together in one location on the page, rather than needing to reserve space by each control to display the error information. Unfortunately, Windows Forms doesn’t include any corresponding controls for you out of the box.

Several great articles have been written by Billy Hollis and Michael Weinhardt describing techniques for extending the validation capabilities of Windows Forms in .NET 1.1. Many of the things they describe in their articles are still applicable in .NET 2.0, such as the creation of additional controls or components to simplify the process of validating input from controls on Windows Forms. Billy Hollis describes an approach in his article in MSDN Online that involves creating an extender provider control that covers all four forms of validation mentioned previously (see the article “Validator Controls for Windows Forms” at http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnadvnet/html/vbnet04082003.asp).

Michael Weinhardt takes a different approach in his series of three articles and uses classes derived from Component to provide individual controls for the four forms of validation mentioned, as well as providing a mechanism to centralize the validation of controls on a form (see his article “Extending Windows Forms with a Custom Validation Component Library” at http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnforms/html/winforms03162004.asp). The approach described in parts 2 and 3 of Weinhardt’s article is somewhat obsolete because of the introduction of the ValidateChildren method and the AutoValidate property in .NET 2.0. However, his approach for required field, range, comparison, and regular expression validation more closely models the ASP.NET approach, so if you have to build both Web and Windows applications, you might find his approach more to your liking, although Billy Hollis’ approach taps the power of Windows Forms extender provider controls in a clever way that is more consistent with the ErrorProvider approach. Hollis’ approach also reduces the number of additional components that you have to add to a form for each control on the form. I strongly suggest that you check out both of these approaches to decide which of them is to your liking.

Capturing Data Errors on Data Sets

Data sets and their child data objects, such as data tables and rows, have built-in support for keeping track of data errors. The DataRow class exposes a string RowError property that you can set to an error message to indicate that an error has occurred somewhere in that row and give descriptive information about the error.

To be more specific, you should also set error information associated with the individual data fields that are the cause of the error if appropriate. To do that, you call the SetColumnError method on the DataRow class. This method has three overloads.

•    The first takes a DataColumn reference and an error message as a string.

•    The second takes a column index as an integer and an error message as a string.

•    The third takes a column name as a string and the error message as a string.

The result of calling any of these overloads is to associate an error message with a particular column value within the row (a field). The following is an example of setting errors against a row:

private void OnCauseDataError(object sender, EventArgs e)
{
   northwindDataSet.Customers[0].SetColumnError(1,
      "Manual column error");
   northwindDataSet.Customers[0].RowError = "Row error";
}

Typically, these errors are going to be set by ADO.NET in response to an error when executing queries against the database, but you can set them manually, as shown in this example, for advanced scenarios.

When any errors have been set within a data set that is the data source for a DataGridView control, even if indirectly through a BindingSource component, then the grid will display row error and cell error icons based on those errors, as described earlier in this chapter. This is because the DataRowView class that is the actual source of display data for a grid implements the IDataErrorInfo interface to make this error information readily accessible to any data-bound control without having to know the object model for the specific data source type to discover errors within that data source.

If an error has been set for any column in a row, or for the row itself, then the table itself is considered to have errors, as does the data set that contains it. If you know you are programming against a data set, you can use the data set’s object model to extract the error information within that data set. Both the DataSet and DataTable classes expose a HasErrors Boolean property that you can check to see if there are any rows with errors. If HasErrors returns true on the data set, you can iterate over the data set’s Tables collection to determine which table has problems by checking the HasErrors property on each table.

For a table that returns true from HasErrors, if you want to programmatically explore what errors occurred, you can call the GetErrors method on that DataTable object, which will return an array of DataRow objects containing the rows that have errors. You can check the RowError property on each row for a nonempty string. Additionally, you can call the GetColumnsInError method to get back an array of DataColumns, which you can then use to call GetColumnError to extract the individual error messages. An example of some code to perform this drill-down process and display the errors in a separate grid is shown in Listing 10.1 and is contained in the DataErrorInfo sample in the chapter’s download code.

LISTING 10.1: Discovering Data Source Errors


private class DataErrorInfo
{
   private string m_TableName;
   private int m_RowNumber;
   private int m_ColumnNumber;
   private string m_ErrorMessage;


   public string TableName
   {
      get { return m_TableName; }
      set { m_TableName = value; }
   }

   public int RowNumber
   {

      get { return m_RowNumber; }
      set { m_RowNumber = value; }
   }

   public int ColumnNumber
   {
      get { return m_ColumnNumber; }
      set { m_ColumnNumber = value; }
   }

   public string ErrorMessage
   {
      get { return m_ErrorMessage; }
      set { m_ErrorMessage = value; }
   }

   internal DataErrorInfo(string tableName, int rowNo,
      int columnNo, string errorMsg)
   {
      m_TableName = tableName;
      m_RowNumber = rowNo;
      m_ColumnNumber = columnNo;
      m_ErrorMessage = errorMsg;
   }
}

private void OnDisplayDataSourceErrors(object sender, EventArgs e)
{
   if (northwindDataSet1.HasErrors)
   {
      List<DataErrorInfo> errors = new List<DataErrorInfo>();
      foreach (DataTable table in northwindDataSet1.Tables)
      {
         if (table.HasErrors)
         {
            string tableName = table.TableName;
            DataRow[ ] errorRows = table.GetErrors();
            for (int rowIndex = 0; rowIndex < errorRows.Length;
               rowIndex++)
            {
               DataRow errorRow = errorRows[rowIndex];
               int tableRowIndex = table.Rows.IndexOf(errorRow);
               if (!string.IsNullOrEmpty(errorRow.RowError))
               {
                  errors.Add(new DataErrorInfo(tableName,
                     tableRowIndex,-1, errorRow.RowError));
            }
            DataColumn[ ] colErrors = errorRow.GetColumnsInError();
            for (int colIndex = 0; colIndex < colErrors.Length;

            colIndex++)
         {
            DataColumn errorCol = colErrors[colIndex];
            int tableColumnIndex =
               table.Columns.IndexOf(errorCol);
            string errorMsg =
               errorRow.GetColumnError(errorCol);
            errors.Add(new DataErrorInfo(tableName,
               tableRowIndex,tableColumnIndex, errorMsg));
         }
       }
     }
   }
   m_ErrorsGrid.DataSource = errors;
 }

}


Providing Error Information from Custom Objects with IDataErrorInfo

If you are using custom business objects for data binding as described in the last chapter, you may want to have those objects be responsible themselves for determining what is valid data and what is not. For example, if you had a PurchaseOrder class, that class could contain logic that determined what a valid range was for purchase dates (such as not allowing ones to be entered before the company was in business). Or perhaps the validation logic could involve complex business logic that determined if the order is being placed by a particular kind of sales associate on a particular day of the week, and if so, then the price of all items should be discounted 10 percent. In that case, the prices input should be compared to the catalog prices for the order items and the discount applied based on that price. Whatever the case, there needs to be a standardized way for data-bound business objects to notify data-bound controls when there is a validation error at the object level.

Once again, interfaces come to the rescue. The IDataErrorInfo interface (introduced in Chapter 7) is designed exactly for this scenario. If the individual data items (objects) in your data collections implement the IDataErrorInfo interface, then data-bound controls can use that interface to determine if an error has occurred, what that error is, and which property on the object the error is related to.

The IDataErrorInfo interface has two members: a string property named Error, and an indexer (named Item) that takes a string parameter and returns a string. The parameter takes a property or column name within the data object and is expected to return an error corresponding to that property, if there is one. This corresponds to the way the DataGridView displays errors. As discussed in an earlier section, the DataGridView can display errors both at the row level (data object) and at the cell level (property or column on a data object). The Error property on the IDataErrorInfo interface corresponds to the row-level error, and the error messages returned from the Item indexer correspond to cell-level errors.

As an example, I will make some minor modifications to the Customer class that was used in Listing 9.1. Specifically, I’ll add some bounds checking on the CustomerId property to prevent a value greater than 99,999 from being entered. I’ll also implement the IDataErrorInfo interface on the class so that it can support reporting errors to bound controls. The code modifications to the Customer class are shown in Listing 10.2.

LISTING 10.2: Customer Class Supporting IDataErrorInfo


public class Customer : IEditableObject,
   INotifyPropertyChanged, IDataErrorInfo
{
   // Other member variables...
   private string m_Error = string.Empty;
   private Hashtable m_PropErrors = new Hashtable();

   public int CustomerId
   {
      get
      {
         return m_CustomerId;
      }
      set
      {
         if (value > 99999)
         {
            m_PropErrors["CustomerId"] =
                "Maximum Customer ID is 99999";
            m_Error = "Customer data may be invalid";
         }
         else
         {

         m_CustomerId = value;
         FirePropertyChangedNotification("CustomerId");
      }
   }
 }

 string IDataErrorInfo.Error
 {
    get
    {
       return m_Error;
    }
 }

 string IDataErrorInfo.this[string columnName]
 {
    get
    {
       return (string)m_PropErrors[columnName];
    }
 }

 // Other members...
}


In addition to the functionality described in Chapter 9, the Customer class now adds an implementation of the IDataErrorInfo interface with some simple error handling as an example. To support this, the class needs somewhere to store the error information until a client (bound control) asks for it. For the object-level error information exposed by the Error property of the IDataErrorInfo interface, the m_Error string member provides a storage location. An empty or null string is treated as if there is no object-level error. For the per-property errors supported by the indexer on the IDataErrorInfo interface, the m_PropErrors member stores the error messages in a hash table, indexed on the property name.

If some code in the class decides a data error has occurred, it can populate the Error text, a property’s error text, or both. This is what the property set block for the CustomerId property does. If the CustomerId that is set is greater than 99,999, the property set block sets the error text for both the object and the CustomerId property, and these errors will be detected by any controls bound to an instance of the object when that property value is exceeded through the IDataErrorInfo implementation.

Figure 10.6 shows a sample application running with the errors kicking in. In the sample application, the grid and the text boxes are bound to a BindingSource component that is bound to a CustomerCollection containing two instances of a Customer object. There is also an ErrorProvider control on the form that is bound to the same BindingSource with the error provider’s DataSource property. When a user tries to type in a CustomerId greater than 99,999 in either the grid or the Customer ID text box, the property set block assigns the corresponding errors within the object. The property set block also raises the PropertyChanged event, which signals any bound controls to check for errors. The bound controls use the IDataErrorInfo interface to look for errors for any properties they are bound to on the object, as well as an error at the object level itself. If errors are found, the bound controls can update their displays accordingly. As a result of the errors set inside the Customer object in the CustomerId property, the grid immediately displays the error icons and tooltips on the row of the grid for the affected object, and any other bound controls, such as the text boxes, get an error provider icon displayed next to them, because the error provider checks for these errors in the same way as the grid.

FIGURE 10.6: IDataErrorInfo Errors Exposed

IDataErrorInfo Errors Exposed

Data Concurrency Resolution

Data concurrency issues can surface in a number of situations, particularly in distributed, multi-user systems. The basic problem with data concurrency is that when working with disconnected data, like you do most of the time through ADO.NET, the same data can be edited by two different users or pieces of code at the same time. When those changes are persisted back to the data store, you have to decide whose changes to keep and what to do about conflicts.

As described in Appendix D, when a concurrency error occurs in ADO.NET, a DBConcurrencyException is thrown in the data access layer. In large-scale enterprise systems, these errors often will be trapped in the business layer or below it, and handled in some automated fashion, possibly raising some other kind of error to the presentation layer for display to the user. In smaller systems, you may allow the DBConcurrencyException to propagate all the way up to the presentation layer for handling there. It is also possible that you will choose to have the business layer raise a custom exception in response to a concurrency error.

Whichever approach you take, there is no “built-in” functionality for resolving these errors or presenting them to a user. The sophistication of users varies widely across different application domains, and thus the degree of complexity that you can present to users to resolve such issues varies accordingly, so this precludes a general-purpose solution that would be applicable to everyone. If your users are fairly sophisticated, you might present a UI that lets them pick between the values that they are submitting and the values that are currently in the database when concurrency exceptions occur, as shown in Figure 10.7.

FIGURE 10.7: Resolving Concurrency Errors

Resolving Concurrency Errors

For less sophisticated users, you might just reject their submission and force them to re-enter things. You will have to analyze how often this might occur and the impact on users, but sometimes forcing them to do extra work by re-entering their data is less confusing and stressful for them than trying to figure out what they are supposed to do with a more complex interface.

The optimistic concurrency that is used for the queries generated by Visual Studio is designed for the most general case possible. As a result, if a table adapter is generated for a table in a database, the queries for updates and deletes in a table adapter check the table’s current value for every column in a row that is being updated against the value that was there when the row was originally retrieved. This can be horribly inefficient, and a recommended practice for real-world applications is to include a column in your transactional tables that is of type timestamp, rowversion, or datetime that you can use to detect when the row has been updated without having to carry along twice as many parameters for every update.

Where Are We?

Data input validation and error handling are an important part of any data application. Even though I have waited until the last chapter to address the topic, don’t wait until the end of your project to think about what to do about errors. You should be designing for this and integrating the code throughout the project. The Windows Forms Validation Framework provides a great starting point and mechanisms for handling input validation in a consistent way that gives a good user experience. You can build on this by adding custom validators that save repetitive coding for things like required fields, comparisons, range checking, and pattern checking. Validation errors aren’t the only kind of data errors that can occur in your application, so you need to be prepared to deal with data concurrency errors, formatting and parsing errors, and other kinds of errors. Your business objects may contain error checking themselves, and if you are binding those objects to controls, you will want to surface that information in a consistent way with the IDataErrorInfo interface.

Key takeaways from this chapter are:

•    Handle the Validating event and set the event argument Cancel property to true if you detect a validation error.

•    Use the ErrorProvider control to present in-situ error information to the user in a standardized way.

•    Look into the validation control libraries created by Michael Weinhardt and Billy Hollis in their MSDN Online articles for reusable controls for required fields, range checking, field comparisons, and regular expression validation.

•    Implement the IDataErrorInfo interface on custom business objects to provide rich error information to bound controls.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.197.135