2009-04-20

Fixing the DataGridView's CalendarColumn sample

In a DataGridView, it is sometimes convenient to have a column that is a DateTime that allows a user to use the standard DateTimePicker control for choosing dates. Microsoft has a code sample for a CalendarColumn class that does this, and it's more or less the standard.

Except if you actually try to use it in a WinForms application, you'll find it has two rather glaring flaws:

  1. If you try to type in a date, and you don't type in a full two digits for the month or date or four digits for the year and then tab out of the control, your change won't be committed.
  2. If you type in a date or part of a date and tab through to the same column in the next row, and you start typing, the first field highlighted (month, date, year) will be the last field highlighted when you left the last row. (Example: you type in "04/04/2009", tab until the next row's date column, and start typing "04", you'll find you're editing that date's year.)

I found this forum thread that addresses the first concern. ALight's answer involves adding an event handler to the DataGridView's CellValidating event. Because we use grids all over the application, it would've been very inconvenient to alter every grid and add this code. Instead, since our grids are built dynamically, I did it by adding this code to the CalendarColumn class (that inherits DataGridViewColumn):

protected override void OnDataGridViewChanged() {
    base.OnDataGridViewChanged();
    if (this.DataGridView != null) {
        this.DataGridView.CellValidating += new DataGridViewCellValidatingEventHandler(CalendarDataGridView_CellValidating);
    }
}

protected void CalendarDataGridView_CellValidating(object sender, DataGridViewCellValidatingEventArgs e) {
    if (sender is DataGridView) {
        DataGridView dgv = (DataGridView)sender;
        if (e.ColumnIndex >= 0 && e.ColumnIndex < dgv.Columns.Count
            && e.RowIndex >= 0 && e.RowIndex < dgv.Rows.Count) {
            if (dgv.Columns[e.ColumnIndex] is CalendarColumn && dgv.Columns[e.ColumnIndex] == this) {
                if (dgv.EditingPanel != null) dgv.EditingPanel.Select();
            }
        }
    }
}

When the CalendarColumn is added to the grid's Columns collection, its DataGridView property is updated, and this routine fires to bind the method to the grid's CellValidating event. Multiple columns will cause multiple event handlers to be bound, but the dgv.Columns[e.ColumnIndex] == this part of the if statement should ensure that only one instance will run the code for any given column. (Note that I have not tested this in cases where columns may be removed from the grid. It should probably work fine, as the event will still be bound and still fire, but the column will never match.)

To solve the second problem, I did some experimentation with the existing control methods. In Microsoft's sample, there is a comment in the CalendarEditingControl's method PrepareEditingControlForEdit that reads "No preparation needs to be done." I found this to be not quite correct. The following change resets the control so, when you start typing, you start editing on the month, like you'd expect:

public void PrepareEditingControlForEdit(bool selectAll) {
    if (selectAll) base.RecreateHandle();
}

Seems to work so far, anyway.

7 comments:

Ad said...

I need to avoid typing slash as well. If the user enters two digits in month, then the mouse should automiatically move to the Date portion. Pl let me know how to handle this?

Yakko Warner said...

As far as I know, the standard DTP control doesn't support this, so it won't be trivial. You might try seeing if you can hook into the KeyDown or KeyPress events on the control, count characters, and when two characters are typed, do a SendKeys to send the CurrentUICulture.DateTimeFormat.DateSeparator character.

I don't know if it will work, but it's something to try.

Unknown said...

Thanks for this code :) I also noticed that if a cell is null when you go into edit mode an exception is thrown. Added a simple IIF statement to resolve:

In CalendarCell.InitializeEditingControl:

ctl.Value = CType(IIf(Not IsDate(Me.Value), Now, Me.Value), DateTime)

Yakko Warner said...

Thanks for the comment! I added similar code when I expanded this into a checkbox-enabled, nullable CalendarColumn. Good to know I wasn't just being paranoid. ;)

Unknown said...

Thanks for spotting these fixes, good work.

For the Null value, what if i don't want to insert today's date if it's null? What if i'd just like to leave the cell blank and DB value still null?

Thanks

Yakko Warner said...

Check the nullable column example I linked to in my previous comment (it's something I worked on later). It lets you enable the checkbox on the date control to blank out dates and insert DBNull into the underlying data source.

Unknown said...

I have a row with two CalendarCell: First cell not nullable but second one is nullable.
If I click in in nullable cell, I let it unchecked then I click on the first cell. As result, I loose the selected value in combobox (Cell.Value should be selected).

When I have my second cell checked , I don't have this problem.

I cannot understand what's happening. help will be appreciated.

Thanks