2009-09-09

A nullable DataGridView CalendarColumn

A while back, I managed to fix a couple bugs with the sample DataGridView CalendarColumn control that made it much more usable. Today, I came across one more issue. It's pretty well known that the DateTimePicker, despite having support for a checkbox that lets you turn a date on or off, does not directly support "null" as a valid value. There are a bunch of ways to get around this, but what I came across was a need to support this inside of a DataGridView.

I started with the task of making the CalendarColumn configurable in such a way as to be able to turn the checkbox on or off at will (well, at least, at the moment of construction). That part's easy; I added a new constructor to CalendarColumn to take an "isNullable" flag:

public CalendarColumn() : base(new CalendarCell()) { }
public CalendarColumn(bool isNullable) : base(new CalendarCell(bool isNullable)) { }

Then, I added a class-level variable to my CalendarCell class, and initialized it in the constructor:

public class CalendarCell {
    private bool isNullable = false;

    public CalendarCell() : base() { 
        this.Style.Format = "d"; 
    }

    public CalendarCell(bool isNullable) : this() {
        this.isNullable = isNullable;
    }
[...]

The next modification comes in InitializeEditingControl. Immediately after getting the reference to CalendarEditingControl ctl is set:

    ctl.ShowCheckBox = this.isNullable;
    if (this.Value != null && this.Value != DBNull.Value) {
        if (this.Value is DateTime) {
            ctl.Value = (DateTime)this.Value;
        } else {
            DateTime dtVal;
            if (DateTime.TryParse(Convert.ToString(this.Value), out dtVal)) ctl.Value = dtVal;
        }
        if (this.isNullable) ctl.Checked = true;
    } else if (this.isNullable) {
        ctl.Checked = false;
    }

(Note that I added a little checking around the value setting area, because I'm paranoid like that.)

Next, DefaultNewRowValue:

public override object DefaultNewRowValue { get { if (this.isNullable) return null; else return DateTime.Now; } }

Here's what I found out you don't change. The ValueType property always returns typeof(DateTime). If you change this to typeof(DateTime?), what ends up happening is, if you bind to a DataTable, it attempts to insert an actual null into the table. Because (for reasons I have yet to believe necessary) null and DBNull are two completely separate and incompatible things, this fails. Apparently, by leaving the value type as non-nullable, a null value will get translated appropriately in the binding.

We're not quite done yet. In the CalendarEditingControl class, the EditingControlFormattedValue property needs to be updated:

public object EditingControlFormattedValue {
    get {
        if (this.ShowCheckBox && !this.Checked) {
            return String.Empty;
        } else {
            if (this.Format == DateTimePickerFormat.Custom) {
                return this.Value.ToString();
            } else {
                return this.Value.ToShortDateString();
            }
        }
    }
    set {
        string newValue = value as string;
        if (!String.IsNullOrEmpty(newValue)) {
            this.Value = DateTime.Parse(newValue);
        } else if (this.ShowCheckBox) {
            this.Checked = false;
        }
    }
}

And presto, a checkable, nullable DateTimePicker column that works in a DataGridView bound to a DataTable.

3 comments:

Jefftoaster said...

Yakko,

Thanks for the code - it helped TREMENDOUSLY! I'm using the grid non-bound.

I made a minor change to mine:
public class CalendarColumnNullable : DataGridViewColumn
{
public CalendarColumnNullable()
: base(new CalendarCellNullable())
...
}

public class CalendarCellNullable : CalendarCell
{
public CalendarCellNullable()
{
this.isNullable = true;
}
}

Mark Cranness said...

I had to also override CalenderCellNullable.Clone to get this to work (on .NET 2.0).

public override object Clone() {
CalenderCellNullable clone = (CalenderCellNullable)base.Clone();
clone.isNullable = this.isNullable;
return clone;
}

What am I missing in the original that made it work for you without the code above?

Yakko Warner said...

Good call, I didn't think about Clone.

I'm not sure why it would work for me without and it wouldn't work for you. All I could guess was that something in your implementation ended up using Clone, but mine did not. What that "something" is, though, I couldn't begin to guess.