« Photography, Picasa, Managing Photographs Remains Hard | Main | Filenames Are Dead »

DataGridView Validation

Windows Forms 2.0 has a new general purpose data display control called a DataGridView. It is intended to be easier to use and more powerful than the original DataGrid control.

The following experience may help you when it comes to adding data validation logic to a rather simple application of the DataGridView control.

An obvious (but WRONG) place to start is the CellValidating event of the DataGridView object. Here is a fairly reusable implementation of a CellValidating event delegate method:

 

    this.grid.CellValidating += new DataGridViewCellValidatingEventHandler(grid_CellValidating);

 

    void grid_CellValidating(object sender, DataGridViewCellValidatingEventArgs e) {

        DataGridViewCell cell = grid.Rows[e.RowIndex].Cells[e.ColumnIndex];

        TypeConverter tc = TypeDescriptor.GetConverter(cell.ValueType);

        try {

            object newValue = tc.ConvertFrom(e.FormattedValue);

            object oldValue = cell.Value;

            try {

                cell.Value = newValue;

            } finally {

                cell.Value = oldValue;

            }

        } catch (Exception ex) {

            grid.Rows[e.RowIndex].ErrorText = ex.Message;

            SystemSounds.Beep.Play();

            e.Cancel = true;

        }

    }

 

There are a few general things to point out about this method:

·        It uses a TypeConverter to determine if the new value (e.FormattedValue) can be converted to the type of value stored in the cell. Using a TypeConverter is a good general approach because FormattedValue isn’t always a string. It can, in theory, be just about any type.

·        It provisionally assigns the new value to the cell’s value to trigger any argument checking done by the underlying DataSource’s property setter. If there is a value range restriction on the data, you should be checking it at the first public interface level, not in some form specific UI code.

·        A finally block is used to restore the current value of the cell after the provisional assignment. There are several things not to like about this technique. The setter can have side effects (not good coding practice, but it happens). The setter can be resource intensive.

·        Validation errors result in exceptions being thrown. The exception’s message is set as the grid row’s ErrorText property and a beep sound is generated. The ErrorText property will cause a small error notification icon to appear in the row header with a tool tip set to the exception’s message.

·        The Validating event doesn’t happen when a value is simply assigned to a grid cell. For example if your paste-from-clipboard method attempts to assign the contents of the clipboard to a grid cell, you’ll have to implement separate validation logic there and validation failure notification logic.

When you use the ErrorText property, don’t forget to reset it after the problem has been fixed. The simplest way to do this is with the Validated event:

    this.grid.CellValidated += new DataGridViewCellEventHandler(grid_CellValidated);

 

    void grid_CellValidated(object sender, DataGridViewCellEventArgs e) {

        grid.Rows[e.RowIndex].ErrorText = String.Empty;

    }

 

The RIGHT place to put your validation logic is in the DataSource’s property setters, throw an ArgumentValueException for bad values, and use the DataError event instead of the CellValidating event to handle the exceptions:

    this.grid.DataError += new DataGridViewDataErrorEventHandler(grid_DataError);

 

    DateTime lastDataError = DateTime.MinValue;

    void grid_DataError(object sender, DataGridViewDataErrorEventArgs e) {

        e.Cancel = true;

        e.ThrowException = false;

 

        // A single error may cause secondary calls to this handler, ignore these.

        if (grid.Rows[e.RowIndex].ErrorText != String.Empty && null == e.Exception) return;

 

        TimeSpan sinceLast = DateTime.Now - lastDataError;

        lastDataError = DateTime.Now;

 

        string message;

        if (null == e.Exception)

            message = "Bad value.";

        else {

            message = e.Exception.Message;

            if (e.Exception is System.Reflection.TargetInvocationException)

                if (e.Exception.InnerException is FormatException)

                    message = "Bad value format. Check the type of value entered.";

        }

        grid.Rows[e.RowIndex].ErrorText = message;

        if (sinceLast.TotalSeconds < 1.0)

            MessageBox.Show(message + "\r\n\r\nPress Ctrl-Z to restore previous value.", "Bad Value");

        else

            SystemSounds.Beep.Play();

    }

 

The DataError event has a richer (more complicated) calling context than the CellValidating event, but this is a good thing. It allows you to be consistent and focused about your error handling and notification.

A few comments about the grid_DataError method:

·        The e.Context field of the event arguments is a DataGridViewDataErrorContext, which is a flags style enumerator that tells you the general context of the data error. You may want to specialize your handling based on the context in production situations.

·        In addition to the Cancel property there is a ThrowException property. Generally you can set these to true and false respectively because you’re going to handle the problem and take care of notifying the user.

·        A single error may cause a secondary call to the handler. This is the kind of thing that may get fixed by the time 2.0 goes into production. For now, ignore the second call by noticing that the exception property is null and an error message has already been set.

·        Many exception messages are fairly cryptic from an ordinary user’s perspective. Do whatever massaging is necessary for your application.

·        In addition to setting the ErrorText property for a small visual indicator of an error condition, either play a beep sound or, if this error followed the last by less than a second, put up a dialog to display the message directly and require the user to acknowledge it.

Hope this helps you…

 

 

 

Comments

I noticed that is a bug involving DataError event from DataGridView, that is easy to reproduce. Put a DataGridView and a button with an event handler for the Click event on a Windows Form and the handler for the DataError event contains e.Cancel=true. Then, if you edit a row in the grid in order to produce a data error and click on the button without moving the cursor on an other row in the grid, the code hangs.

The e.cancel doesn't work in dataGridView1_DataError if you throw an exception from your buisiness object. In the RC you still get 2 exceptions like you noted in the beta.

Post a comment

(If you haven't left a comment here before, you may need to be approved by the site owner before your comment will appear. Until then, it won't appear on the entry. Thanks for waiting.)