V4 V5

An Excel table (a ListObject) is an object, not formatting, and V5 treats it as one. A TableDefinition holds a range, a header row count and a style name; each cell’s colour is worked out from where it sits inside that range, every time the sheet is drawn.

Because the colours are never baked into the cells:

  • a 100,000-row table costs one object,
  • inserting a row in the middle re-bands the rest instead of breaking the pattern,
  • a cell painted by hand punches through the banding,
  • and the file gets a real xl/tables/tableN.xml part rather than a scattering of ad-hoc fills.

The basics

using unvell.ReoGrid.Core;
using unvell.ReoGrid.Core.Tables;

// format A1:D100 as a table, first row as the header (default TableStyleMedium2)
var table = ws.AddTable("A1:D100");

// column names come from the header row
string first = table.Columns[0].Name;

// Excel's "Convert to Range" — the styling was never in the cells, so it just stops
ws.RemoveTable(table.Name);

_ = first;

AddTable takes an A1-style address or a defined name, or a RangePosition. The first row becomes the header and the column names are read from it — blanks and duplicates are made unique (Column3, Qty2), because Excel refuses a table whose column names collide.

Style and options

var table = ws.AddTable(RangePosition.Parse("A1:D100"), new AddTableOptions
{
	Name = "Sales",
	Style = "TableStyleMedium7",   // one of the 60 built-ins; the name survives I/O verbatim
	ShowRowStripes = true,         // banded rows (default true)
	ShowFirstColumn = true,        // emphasise the first column
	ShowFilterButton = true,       // filter dropdowns on the header row (default false)
});

// flip the switches later — Style is a record struct, so change it with `with`
table.Style = table.Style with { ShowColumnStripes = true, ShowRowStripes = false };
OptionDefaultMeaning
NameTable1, Table2, …Unique-in-workbook internal name. It is an identifier — a letter or _ first, then letters, digits, . and _ (no spaces, and nothing that reads as a cell reference like Q1) — because Excel spells it in structured references
StyleTableStyleMedium2Built-in style name. An unknown one is never renamed (see below)
HeaderRowCount10 or 1 — an Excel table has no multi-row header
TotalsRowCount00 or 1. The row is coloured and round-trips, but the aggregates are not implemented
ShowRowStripestrueBanded rows
ShowColumnStripesfalseBanded columns
ShowFirstColumn / ShowLastColumnfalseEmphasise the first / last column
ShowFilterButtonfalseFilter dropdowns on the header row (creates the sheet’s auto-filter)

ShowFilterButton is off by default because a worksheet holds one auto-filter. On by default, creating a second table would silently take the buttons off the first.

Banded rows

A table’s stripes count rows the way sheet-wide banding does — visible rows only — so a filtered table stays evenly striped instead of showing two clean rows side by side. Inside a table the table’s own banding wins, and the sheet’s stripe stops at the table’s edge.

Excel keeps banding inside the table style, which is why this is the striping that round-trips through xlsx (a sheet-wide stripe has no home in the format, and stays reogrid-json only).

Fill order

Strongest first:

  1. Conditional format
  2. The cell’s own fill (the cell → row → column style chain)
  3. The table style
  4. The sheet stripe (outside the table)

The table joins the inheritance chain as Root ◁ Table ◁ Column ◁ Row ◁ Cell. Being weaker than anything the cell says for itself is what lets you paint one row yellow and break the banding just there. Borders work the same way: a hand-drawn border replaces the table’s edge by edge.

Querying

bool any = ws.HasTables;

// the table covering a cell, or null
TableDefinition? at = ws.GetTableAt(5, 2);

// by name (case-insensitive, as in Excel)
TableDefinition? byName = ws.GetTable("Sales");

// the rows carrying data — the range less its header and totals rows
RangePosition data = at?.DataRange ?? default;

// what the table contributes to a cell (the layer below the cell's own style)
var contributed = ws.GetTableStyle(5, 2);

// hand-drawn borders layered over the table's own
CellBorders borders = ws.GetEffectiveBorders(5, 2);

_ = (any, byName, data, contributed, borders);

Built-in styles, and your own

Excel stores only the name — an application is expected to know what TableStyleMedium7 looks like. V5 generates all 60 from the name: the built-ins run in rows of seven, a neutral column followed by the six theme accents, so the name gives up both the family and the accent. The palette is then derived with the same HSL tint curve OOXML uses, which lands on the Office swatches (“Lighter 40%” and friends).

An unknown name renders with the default palette but is never renamed, so a file carrying a style V5 does not know can be opened, saved and reopened in Excel looking exactly as it did.

// every built-in name (Light 1–21 / Medium 1–28 / Dark 1–11)
foreach (string name in TableStyles.BuiltinNames())
{
	TableStyleDefinition def = TableStyles.Resolve(name);
	_ = (def.HeaderFill, def.RowStripeFill, def.WholeBorderColor);
}

// register a palette of your own (this may also override a built-in name)
TableStyles.Register(new TableStyleDefinition
{
	Name = "BrandTable",
	HeaderFill = 0xFF1F3864,
	HeaderColor = 0xFFFFFFFF,
	HeaderBold = true,
	RowStripeFill = 0xFFEAF0F8,
	WholeBorderColor = 0xFF1F3864,
});

Inserting and deleting rows and columns

A table follows the data it describes: inserting above it moves it, inserting inside it grows it, and deleting a column cuts that column out of the column list too. A table whose every row is deleted goes with them.

I/O

FormatWhat is written
reogrid-jsontables (reogrid-web’s JsonTableDefinition shape)
XLSXxl/tables/tableN.xml + the [Content_Types].xml override + the sheet’s _rels + <tableParts>, numbered workbook-wide

Not there yet

  • Totals-row aggregates (SUBTOTAL) — the row is coloured and round-trips, nothing computes
  • Structured references (=Table1[Column])
  • Auto-expand when an adjacent cell is edited
Was this article helpful?