TableControl

March 9, 2026 · View on GitHub

Interactive data grid with virtual data support, sorting, inline editing, and multi-selection.

Overview

TableControl renders tabular data with full keyboard and mouse interaction. It supports two data modes: in-memory rows for simple tables, and ITableDataSource for virtual/lazy data binding that handles millions of rows by querying only visible rows on demand.

By default, ReadOnly = true preserves backward-compatible static table rendering. Set .Interactive() to enable selection, navigation, editing, and all interactive features.

Properties

PropertyTypeDefaultDescription
ReadOnlybooltrueWhen true, table is static display only
SelectedRowIndexint-1Index of selected row (-1 = none)
SelectedColumnIndexint-1Index of selected column in cell navigation mode
CellNavigationEnabledboolfalseEnable Tab/Arrow cell-level navigation
MultiSelectEnabledboolfalseEnable Ctrl+Click/Shift+Click multi-selection
CheckboxModeboolfalseShow [x]/[ ] checkboxes (implies MultiSelect)
SortingEnabledboolfalseEnable column sorting by clicking headers
ColumnResizeEnabledboolfalseEnable column resize by dragging borders
InlineEditingEnabledboolfalseEnable F2/Enter/DblClick cell editing
DataSourceITableDataSource?nullVirtual data source for large datasets
BorderStyleBorderStyleSingleBorder style (Single, DoubleLine, Rounded, None)
ShowHeaderbooltrueShow column header row
ShowRowSeparatorsboolfalseShow horizontal lines between rows
Titlestring?nullTitle text above the table
VerticalScrollbarVisibilityScrollbarVisibilityAutoWhen to show vertical scrollbar
HorizontalScrollbarVisibilityScrollbarVisibilityAutoWhen to show horizontal scrollbar
BackgroundColorColor?Color.DefaultBackground color (null to inherit gradient)
ForegroundColorColor?Color.DefaultText color
HeaderBackgroundColorColor?Color.DefaultHeader row background
HeaderForegroundColorColor?Color.DefaultHeader row text color
HasFocusboolfalseWhether table has keyboard focus
IsEditingboolfalseWhether a cell is currently being edited
FilteringEnabledboolfalseEnable inline filtering (press /)
FuzzyFilterEnabledboolfalseEnable fuzzy matching as fallback
AutoHighlightOnFocusbooltrueAuto-select first row on focus

Events

EventArgumentsDescription
SelectedRowChangedEventHandler<int>Row selection changed
SelectedRowItemChangedEventHandler<TableRow?>Selected row object changed
RowActivatedEventHandler<int>Row activated (Enter/double-click)
CellActivatedEventHandler<(int Row, int Column)>Cell activated in cell navigation mode
CellEditCompletedEventHandler<(int Row, int Column, string OldValue, string NewValue)>Cell edit committed
CellEditCancelledEventHandler<(int Row, int Column)>Cell edit cancelled
MouseRightClickEventHandler<MouseEventArgs>Right-click (row/cell selected first)
MouseClickEventHandler<MouseEventArgs>Left click
MouseDoubleClickEventHandler<MouseEventArgs>Double click
GotFocusEventHandlerTable received focus
LostFocusEventHandlerTable lost focus

Creating Tables

Static Table (ReadOnly)

var table = Controls.Table()
    .AddColumn("Name")
    .AddColumn("Value", TextJustification.Right)
    .AddRow("CPU", "42%")
    .AddRow("Memory", "8.2 GB")
    .AddRow("Disk", "256 GB")
    .WithHeaderColors(Color.White, Color.DarkBlue)
    .Rounded()
    .Build();

Interactive Table

var table = Controls.Table()
    .AddColumn("Name")
    .AddColumn("Status")
    .AddColumn("Priority", TextJustification.Right)
    .AddRow("Task 1", "[green]Done[/]", "High")
    .AddRow("Task 2", "[yellow]In Progress[/]", "Medium")
    .AddRow("Task 3", "[red]Blocked[/]", "Low")
    .Interactive()
    .WithCellNavigation()
    .WithSorting()
    .WithHeaderColors(Color.White, Color.DarkBlue)
    .Rounded()
    .OnRowActivated((sender, rowIdx) =>
    {
        // Handle row activation
    })
    .Build();

Interactive DataGrid with Virtual Data

// Implement ITableDataSource for large datasets
public class ProductDataSource : ITableDataSource
{
    private readonly List<Product> _products;

    public int RowCount => _products.Count;
    public int ColumnCount => 4;

    public string GetColumnHeader(int col) => col switch
    {
        0 => "ID", 1 => "Name", 2 => "Price", 3 => "Status", _ => ""
    };

    public string GetCellValue(int row, int col) => col switch
    {
        0 => _products[row].Id.ToString(),
        1 => _products[row].Name,
        2 => $"${_products[row].Price:F2}",
        3 => _products[row].Status,
        _ => ""
    };

    public TextJustification GetColumnAlignment(int col) => col switch
    {
        0 => TextJustification.Right,
        2 => TextJustification.Right,
        _ => TextJustification.Left
    };

    // Optional: fixed column widths (null = auto)
    public int? GetColumnWidth(int col) => col switch
    {
        0 => 6, _ => null
    };

    // Optional: sorting support
    public bool CanSort(int col) => true;
    public void Sort(int col, SortDirection direction) { /* sort _products */ }

    public event EventHandler? DataChanged;
}

// Use it
var dataSource = new ProductDataSource(products);

var grid = Controls.Table()
    .WithTitle("Product Inventory (10,000 rows)")
    .WithDataSource(dataSource)
    .Interactive()
    .WithCellNavigation()
    .WithSorting()
    .WithColumnResize()
    .WithInlineEditing()
    .WithHeaderColors(Color.White, Color.DarkBlue)
    .Rounded()
    .WithVerticalAlignment(VerticalAlignment.Fill)
    .WithHorizontalAlignment(HorizontalAlignment.Stretch)
    .OnCellEditCompleted((sender, e) =>
    {
        dataSource.UpdateRecord(e.Row, e.Column, e.NewValue);
    })
    .Build();

Keyboard Support

Row Navigation

KeyAction
Up/Down ArrowMove selection up/down
Page Up/DownJump by visible row count
HomeSelect first row
EndSelect last row
EnterActivate row (or edit cell if cell nav enabled)
EscapeDeselect cell (back to row mode)

Cell Navigation (when enabled)

KeyAction
TabMove to next cell (wraps to next row)
Shift+TabMove to previous cell (wraps to previous row)
Left/Right ArrowMove between cells in current row
EnterActivate cell / begin edit

Inline Editing (when enabled)

KeyAction
F2Begin editing selected cell
EnterCommit edit
EscapeCancel edit
Left/RightMove cursor within edit buffer
Home/EndMove cursor to start/end
Backspace/DeleteDelete character

Inline Filtering (when enabled)

KeyAction
/Enter filter mode
EnterConfirm filter
EscapeCancel/clear filter
Left/RightMove cursor within filter
Home/EndMove cursor to start/end
Backspace/DeleteDelete character

Multi-Select

KeyAction
Ctrl+ASelect all rows
SpaceToggle checkbox (checkbox mode)

Mouse Support

ActionResult
Left ClickSelect row (and cell if cell nav enabled)
Double ClickActivate row / begin cell edit
Right ClickSelect row/cell, then fire MouseRightClick
Ctrl+ClickToggle row selection (multi-select)
Shift+ClickSelect range (multi-select)
Click HeaderSort by column (toggles Asc/Desc/None)
Mouse WheelVertical scroll
Shift+WheelHorizontal scroll
Drag Column BorderResize column
Click Scrollbar TrackPage up/down or left/right
Drag Scrollbar ThumbSmooth scroll
Click Scrollbar ArrowScroll by one row/column

ITableDataSource

The ITableDataSource interface enables virtual data binding for large datasets. Only visible rows are queried — the control never loads all data into memory.

public interface ITableDataSource
{
    int RowCount { get; }
    int ColumnCount { get; }
    string GetColumnHeader(int columnIndex);
    string GetCellValue(int rowIndex, int columnIndex);

    // Optional (default interface methods):
    TextJustification GetColumnAlignment(int columnIndex) => TextJustification.Left;
    int? GetColumnWidth(int columnIndex) => null;
    Color? GetRowBackgroundColor(int rowIndex) => null;
    Color? GetRowForegroundColor(int rowIndex) => null;
    bool IsRowEnabled(int rowIndex) => true;
    object? GetRowTag(int rowIndex) => null;
    bool CanSort(int columnIndex) => false;
    void Sort(int columnIndex, SortDirection direction) { }

    event EventHandler? DataChanged;
}

When DataSource is set:

  • Internal _rows/_columns lists are ignored
  • Only visible rows are queried via GetCellValue()
  • Column widths auto-measure from visible rows
  • Sorting delegates to DataSource.Sort() if CanSort() returns true
  • DataChanged event triggers re-measure and re-render
  • AddRow()/ClearRows() throw if DataSource is set

Builder Methods

Data

MethodDescription
.AddColumn(header, alignment?, width?)Add a column
.WithColumns(headers...)Add multiple columns by name
.AddRow(cells...)Add a data row
.WithDataSource(source)Set virtual data source

Interactive Features

MethodDescription
.Interactive()Enable interactive mode (ReadOnly = false)
.WithCellNavigation()Enable cell-level Tab/Arrow navigation
.WithMultiSelect()Enable Ctrl+Click/Shift+Click multi-select
.WithCheckboxMode()Enable checkbox multi-select
.WithSorting()Enable click-header sorting
.WithColumnResize()Enable drag-to-resize columns
.WithInlineEditing()Enable F2/Enter cell editing

Appearance

MethodDescription
.WithTitle(text, alignment?)Set title above table
.WithHeaderColors(fg, bg)Set header row colors
.Rounded() / .DoubleLine() / .SingleLine() / .NoBorder()Border style
.WithBorderColor(color)Set border color
.ShowRowSeparators()Show lines between rows
.WithVerticalScrollbar(visibility)Scrollbar visibility (Auto/Always/Never)
.WithHorizontalScrollbar(visibility)Scrollbar visibility (Auto/Always/Never)

Events

MethodDescription
.OnSelectedRowChanged(handler)Wire selection event
.OnRowActivated(handler)Wire row activation event
.OnCellActivated(handler)Wire cell activation event
.OnCellEditCompleted(handler)Wire cell edit commit event
.OnRightClick(handler)Wire right-click event

Examples

File Explorer Table

var table = Controls.Table()
    .AddColumn("Name")
    .AddColumn("Size", TextJustification.Right, 10)
    .AddColumn("Modified", TextJustification.Right, 20)
    .Interactive()
    .WithSorting()
    .Rounded()
    .WithHeaderColors(Color.White, Color.DarkBlue)
    .WithVerticalAlignment(VerticalAlignment.Fill)
    .WithHorizontalAlignment(HorizontalAlignment.Stretch)
    .OnRowActivated((sender, rowIdx) =>
    {
        var row = sender.GetRow(rowIdx);
        var path = row?.Tag as string;
        if (path != null && Directory.Exists(path))
            NavigateTo(path);
    })
    .OnRightClick((sender, args) =>
    {
        ShowContextMenu(sender, args);
    })
    .Build();

Editable Spreadsheet

var table = Controls.Table()
    .AddColumn("A")
    .AddColumn("B")
    .AddColumn("C")
    .AddRow("1", "2", "3")
    .AddRow("4", "5", "6")
    .Interactive()
    .WithCellNavigation()
    .WithInlineEditing()
    .WithColumnResize()
    .OnCellEditCompleted((sender, e) =>
    {
        // e.Row, e.Column, e.OldValue, e.NewValue
        RecalculateFormulas();
    })
    .Build();

Multi-Select with Checkboxes

var table = Controls.Table()
    .AddColumn("Task")
    .AddColumn("Status")
    .AddRow("Write tests", "[yellow]Pending[/]")
    .AddRow("Code review", "[green]Done[/]")
    .AddRow("Deploy", "[red]Blocked[/]")
    .Interactive()
    .WithCheckboxMode()
    .Build();

// Later: get checked rows
var checked = table.GetCheckedRows();

Gradient Background

TableControl preserves gradient backgrounds from parent windows when no explicit background color is set:

var gradient = ColorGradient.FromColors(
    new Color(10, 10, 50),
    new Color(0, 50, 80),
    new Color(40, 10, 60));

var window = new WindowBuilder(ws)
    .WithBackgroundGradient(gradient, GradientDirection.Vertical)
    .AddControls(table)
    .BuildAndShow();

Filtering

When FilteringEnabled = true, press / to enter filter mode. Type a filter expression and press Enter to confirm, or Escape to cancel.

Filter Syntax

SyntaxMeaningExample
textMatch any column containing textapple
col:valueMatch specific columncategory:fruit
col>valueNumeric greater thanprice>500
col<valueNumeric less thanqty<10
term1 term2AND — all terms must matchfruit NYC
a|bOR — any alternative matchesapple|banana
col:a|bOR within a columncategory:fruit|vegetable

Compound Expressions

Space-separated terms are combined with AND (all must match). Pipe-separated alternatives within a term are combined with OR (any must match).

category:electronics price>500          → category contains "electronics" AND price > 500
category:electronics|clothing           → category contains "electronics" OR "clothing"
category:electronics|clothing price>500 → (electronics OR clothing) AND price > 500

When using | with a column prefix, subsequent alternatives without their own prefix inherit the column and operator from the first:

  • category:fruit|vegetable → both target "category" column
  • category:fruit|warehouse:NYC → first targets "category", second targets "warehouse"

Programmatic API

// Simple text filter
table.ApplyFilter("fruit");

// Column-specific filter
table.FilterByColumn(1, "fruit");

// Compound expression
table.ApplyFilter("category:fruit|vegetable price>1.00");

// Clear filter
table.ClearFilter();

// Check filter state
bool isFiltering = table.IsFiltering;
string? filterText = table.ActiveFilterText;

Builder Methods

MethodDescription
.WithFiltering()Enable inline filtering (also sets Interactive)
.WithFuzzyFilter()Enable filtering with fuzzy fallback

Events

EventDescription
FilterAppliedFired when filter is confirmed
FilterClearedFired when filter is cleared
FilterTextChangedFired during live typing

Sorting

When SortingEnabled = true, clicking a column header cycles through: Ascending -> Descending -> None. A sort indicator (up/down triangle) appears in the header.

For in-memory rows, sorting creates an internal index map. For ITableDataSource, sorting delegates to DataSource.Sort() if CanSort() returns true.

Virtual Rendering

Regardless of data mode or ReadOnly state, TableControl always uses virtual rendering: only visible rows are measured and painted. This means 10,000+ row tables render instantly with no performance penalty.

Column widths are computed using sample-based measurement (header + visible rows + a small buffer), cached and invalidated on significant scroll or data changes.

Scrollbars

  • Vertical scrollbar: Appears when rows exceed viewport. Shows up/down arrows, draggable thumb, and clickable track for page scrolling.
  • Horizontal scrollbar: Appears when total column width exceeds viewport. Same interaction model.
  • Both support ScrollbarVisibility.Auto (default), Always, or Never.

See Also


Back to Controls | Back to Main Documentation