V4 V5

There is no built-in date picker. Building one shows how a custom cell type cooperates with the host’s UI.

What we are building: a cell that displays a date and, when its calendar button is clicked, opens the host’s date-selection dialog.

The design has two halves.

  • The cell type (core side) — drawing the date and hit-testing the button. No UI dependency
  • The host side — catching CellButtonClicked, showing a dialog, writing the value back

The core has no UI, so it cannot show a calendar itself. The cell type only reports that it was pressed; the host puts the dialog on screen.

Implementing the cell type

using unvell.ReoGrid.Core;
using unvell.ReoGrid.Core.CellTypes;
using unvell.ReoGrid.Core.Style;

/// <summary>Shows a date and asks the host for a new one via the calendar button on the right.</summary>
public sealed class DatePickerCellType : CellTypeDescriptor
{
	private const double ButtonWidth = 18;
	private const uint Accent = 0xFF2D8CF0;      // the same water blue the built-in cell types use
	private const uint HoverTint = 0xFFEAF4FE;

	private readonly string _format;

	public DatePickerCellType(string format = "yyyy/MM/dd") => _format = format;

	public override string TypeName => "datepicker";

	// we draw the date text ourselves
	public override bool ReplacesText => true;

	// typing a date directly is allowed too (like a date cell in Excel)
	public override bool AllowsTextEditing => true;

	public override bool Paint(in CellTypePaintContext ctx)
	{
		// the value is an Excel serial number; treat 0 as "empty" and draw nothing
		double serial = ctx.Value.AsNumber;
		string text = serial > 0
			? DateTime.FromOADate(serial).ToString(_format)
			: "";

		var style = ctx.MakeTextStyle(HAlign.Left, VAlign.Middle);
		ctx.Graphics.DrawText(text, ctx.X + 3, ctx.Y, ctx.Width - ButtonWidth - 6, ctx.Height,
			ctx.TextColor, style);

		// draw the calendar button on the right (with a background while hovered)
		double bx = ctx.X + ctx.Width - ButtonWidth;
		if (ctx.HoverRegion == 1)
			ctx.Graphics.FillRectangle(bx, ctx.Y + 1, ButtonWidth, ctx.Height - 2, HoverTint);

		// an outlined square: fill the outside, then punch the middle back out
		double gy = ctx.Y + ctx.Height / 2 - 5;
		ctx.Graphics.FillRectangle(bx + 4, gy, 10, 10, Accent);
		ctx.Graphics.FillRectangle(bx + 5, gy + 3, 8, 6, 0xFFFFFFFF);

		return true;
	}

	// only the button is hover region 1; everything else is 0
	public override int HitRegion(double localX, double localY, double width, double height)
		=> localX >= width - ButtonWidth ? 1 : 0;

	public override CellTypeClickResult OnClick(in CellTypeClickContext ctx)
		=> ctx.LocalX >= ctx.Width - ButtonWidth
			? CellTypeClickResult.RaiseButton()   // hand it to the host's CellButtonClicked
			: CellTypeClickResult.Unhandled;

	public override void WriteConfig(IDictionary<string, object?> config)
		=> config["format"] = _format;
}

Four things matter.

MemberWhat it does
PaintDraws the cell. Returning true skips the default drawing
HitRegionReturns the hovered region number, delivered to Paint as ctx.HoverRegion
OnClickReturns what the click means. RaiseButton() raises CellButtonClicked
WriteConfigThe settings to store in reogrid-json

AllowsTextEditing is true, so a date can also be typed straight in without the button. ReplacesText is true, so the cell’s text is what Paint drew rather than the default.

The only drawing available is the basics — FillRectangle, DrawLine, DrawText, FillEllipse and friends. That constraint is what keeps it platform-independent, and the built-in cell types are drawn within the same limits (A Custom Drawing Layer).

Assigning it

ws.SetCellType(RangePosition.Parse("C2:C100"), new DatePickerCellType());

// the value is a date serial; the cell type owns the display format, so no style is needed
ws.SetNumber(1, 2, new DateTime(2026, 8, 19).ToOADate());

The value is an Excel serial number (days since 1899-12-30). Convert with DateTime.ToOADate() / DateTime.FromOADate().

Because it is a serial, formulas just workDATEDIF, TODAY()-C2 and the rest. Storing a string would rule that out.

Showing the dialog on the host side

grid.CellButtonClicked += (s, e) =>
{
    var ws = grid.ActiveWorksheet;
    if (ws.GetCellType(e.Cell.Row, e.Cell.Col) is not DatePickerCellType) return;

    using var dlg = new DatePickerForm();     // your own dialog
    if (dlg.ShowDialog(this) != DialogResult.OK) return;

    ws.RecordCell("Pick a date", e.Cell.Row, e.Cell.Col,
        () => ws.SetNumber(e.Cell.Row, e.Cell.Col, dlg.Value.ToOADate()));
    grid.RefreshExternal();
};

CellButtonClicked is shared with the button cell type, so when you have several kinds of button, tell them apart with GetCellType as above.

On WinForms a small window holding a MonthCalendar is enough; on WPF, a DatePicker.

Getting it into the history

// host side: catch CellButtonClicked, show a date dialog, write the result back
ws.RecordCell("Pick a date", row, col, () => ws.SetNumber(row, col, picked.ToOADate()));

Wrapping in RecordCell makes it undoable with Ctrl+Z. Calling SetNumber unwrapped changes the value but records nothing (Undo and Redo).

Making it savable

To restore it from reogrid-json, register the type name.

// let it be restored from reogrid-json
CellTypeRegistry.Register("datepicker",
	config => new DatePickerCellType(config.GetString("format") ?? "yyyy/MM/dd"));

Whatever WriteConfig wrote arrives as config. Register once, at application start-up — without it, those cells come back with no cell type.

The simpler alternative — a dropdown

When the possible dates are a short list (month-end dates, say), DropdownCellType is enough and needs no custom type at all.

ws.SetCellType(1, 2, new DropdownCellType(["2026-08-31", "2026-09-30", "2026-10-31"]));

A custom cell type can open a dropdown too, by returning CellTypeClickResult.OpenDropdown(options) from OnActivate. The control shows the popup, so there is no host-side code.

Was this article helpful?