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.
| Member | What it does |
|---|---|
Paint | Draws the cell. Returning true skips the default drawing |
HitRegion | Returns the hovered region number, delivered to Paint as ctx.HoverRegion |
OnClick | Returns what the click means. RaiseButton() raises CellButtonClicked |
WriteConfig | The 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 work — DATEDIF, 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.