V4 V5

There is no mechanism for binding a DataTable directly. You write it in a loop — which in exchange lets you decide exactly which column goes where and how it looks.

Tens of thousands of rows are not a problem: writing costs only what you actually wrote (Performance and Memory).

Filling the sheet

using System.Data;
using unvell.ReoGrid.Core;
using unvell.ReoGrid.Core.Style;

// column names on row 1, data from row 2
for (int c = 0; c < table.Columns.Count; c++)
	ws.SetText(0, c, table.Columns[c].ColumnName);

for (int r = 0; r < table.Rows.Count; r++)
	for (int c = 0; c < table.Columns.Count; c++)
	{
		object? v = table.Rows[r][c];
		ws.SetObjectValue(r + 1, c, v == DBNull.Value ? null : v);
	}

SetObjectValue sorts string, double, int, bool, DateTime, decimal and the rest into the right kind. Turn DBNull into null before passing it (CellValue).

A DateTime goes in as an Excel serial number. To display it as a date, set a number format (below).

Write typed when you know the type

// when you know the column's type, the typed Set* calls are faster
for (int r = 0; r < table.Rows.Count; r++)
{
	DataRow row = table.Rows[r];
	ws.SetText(r + 1, 0, (string)row["Name"]);
	ws.SetNumber(r + 1, 1, Convert.ToDouble(row["Amount"]));
	ws.SetObjectValue(r + 1, 2, row["OrderDate"]);      // a DateTime becomes a serial number
}

SetText / SetNumber / SetBoolean skip the type dispatch and are lighter than SetObjectValue. The difference becomes noticeable past 100,000 rows.

Headers and formatting

// set the header row in one go with a row style (never cell by cell)
ws.SetRowStyle(0, new StyleRecord
{
	Bold = true,
	BackgroundColor = 0xFFF3F3F3,
	TextAlign = HAlign.Center,
});

// the amount column gets a column style plus a number format, clamped to the used range
ws.SetColumnStyle(1, new StyleRecord { TextAlign = HAlign.Right });
ws.SetNumberFormat(new RangePosition(1, 1, table.Rows.Count, 1), "#,##0");

Do not set formatting in a cell loop. Use SetRowStyle for the header row and SetColumnStyle to right-align a column. The result looks the same, but doing it per cell materializes one entry for every cell in that row or column (Style Inheritance).

Number formats for a date column work the same way.

ws.SetNumberFormat(new RangePosition(1, 2, table.Rows.Count, 1), "yyyy/mm/dd");

Freezing the header row is a control-side operation.

control.SetFreeze(rows: 1, cols: 0);

Fitting column widths to the content

control.AutoFitSelectedColumns();

It acts on the selected columns, so select everything first to cover them all. It is the same operation as double-clicking a header boundary.

Filling from a List<T>

Nothing changes without a DataTable — just map properties to columns.

for (int i = 0; i < items.Count; i++)
{
    ws.SetText(i + 1, 0, items[i].Name);
    ws.SetNumber(i + 1, 1, items[i].Amount);
    ws.SetObjectValue(i + 1, 2, items[i].OrderDate);
}

You could generate the columns by reflection, but at 100,000 rows the property access dominates. With a lot of rows, write it out as above.

Reading the edits back

// sheet -> DataTable, reading row 1 as the column names
var table = new DataTable();
if (!ws.TryGetUsedRange(out RangePosition used)) return table;

for (int c = used.Col; c <= used.EndCol; c++)
	table.Columns.Add(ws.GetDisplayText(used.Row, c));

for (int r = used.Row + 1; r <= used.EndRow; r++)
{
	DataRow row = table.NewRow();
	for (int c = used.Col; c <= used.EndCol; c++)
		row[c - used.Col] = ws.GetObjectValue(r, c) ?? DBNull.Value;
	table.Rows.Add(row);
}

GetObjectValue returns a string, double, bool or null depending on the cell. For a formula cell you get the computed result; use GetFormula when you want the formula itself.

To pull a value out as a date, convert the serial.

double serial = ws.GetValue(r, c).AsNumber;
DateTime date = DateTime.FromOADate(serial);
Was this article helpful?