Sorting, filtering and search

Sorting and filtering are index projections in front of your data: they reorder and hide row numbers and never copy an item. The user drives them from the header; your code reaches the same state through the view and grid.Api.Filters.

Sorting

A click on a header cycles ascending, descending, off; Shift+click adds the column as a further level. A column sorts only if it has a sort key — the typed factories set one, Column.Text needs .Sortable(key). The key is what is compared, so it can differ from what is shown.

Sort keys
Column.Text<Order>("Customer", o => o.Customer)
      .WithKey("customer")
      .Sortable(o => o.Customer);                              // compared as strings, in the sort culture

Column.Text<Order>("Order no.", o => $"A-{o.Id}")
      .WithKey("number")
      .Sortable(o => o.Id)                                     // shown as text, sorted as a number
      .SortWith<int>((a, b) => (a % 1000).CompareTo(b % 1000)); // or by your own rule
Sorting from code
int amount = view.ColumnIdOfKey("amount");
int region = view.ColumnIdOfKey("region");

view.SortBy(region, SortDirection.Ascending);
view.SortBy(amount, SortDirection.Descending, additive: true);   // second level, like Shift+click
var state = view.SortState;                                       // (ElementId, Direction) per level
view.ClearSort();

Text is compared in a sort culture, not ordinally, so umlauts and accented letters land where a reader expects them. The culture can be set per view, per grid through GridContext, or process-wide:

Sort culture
view.SortCulture = CultureInfo.GetCultureInfo("sv-SE");   // this view: Swedish collation
SortFilterSource.SortCulture = CultureInfo.GetCultureInfo("de-DE");   // process-wide default

grid.CanUserSortColumns = false takes the gesture away and leaves sorting by code.

The filter row

Under the headers, a drawn row of filter boxes — one per filterable column. Typing filters after a short pause (300 ms), not on every keystroke. Each column has an operator, chosen by its data type: text columns offer Contains (the default), StartsWith, EndsWith, Equals and NotEquals; number and date columns offer Equals (the default), NotEquals, GreaterThan, GreaterOrEqual, LessThan and LessOrEqual, and compare values, not text. While any filter is active, the corner at the left of the filter row clears all of them.

AsDataGrid() switches the filter row on, WithFilterRow() does it alone, and ShowFilterRow on the control toggles it at runtime. Every column with display text is filterable by default; .NotFilterable() takes a column out.

Value filters

The funnel in a header opens a checklist of the column's distinct values with their counts, like a spreadsheet's auto-filter. Very long lists are cut and say so — search inside the pane to reach the rest. For date columns the list nests year, month and day, or year, quarter, month and day (DateFilterGrouping), which the user can switch in the pane.

With a server source, the values and counts come from the server, not from the pages that happen to be loaded.

Filtering from code

On the view
int customer = view.ColumnIdOfKey("customer");
int amount = view.ColumnIdOfKey("amount");
int region = view.ColumnIdOfKey("region");

view.SetColumnFilter(customer, "nord");                      // the filter-row text
view.SetFilterOperator(amount, FilterOperator.GreaterThan);
view.SetColumnFilter(amount, "1000");
view.SetValueFilter(region, ["North", "East"]);              // the funnel's checklist
view.SetQuickFilter(o => ((Order)o).Status != Status.OnHold); // your own predicate, ANDed

view.ClearAllFilters();                                      // column, value and quick filter

SetQuickFilter is for filters the grid does not draw — a toolbar toggle, a search box of your own. It is combined with the column filters by AND. HasAnyFilter, ActiveFilterCount and VisibleDataCount answer a status line.

Saving and restoring filters

GridFilterState is the complete filter state as a plain object: filter-row texts and operators, value lists and the search text, keyed by column key. It does not depend on column order or visibility, so it survives a user moving columns.

Api.Filters
GridFilterState saved = grid.Api.Filters.Capture();   // texts, operators, value lists, search
grid.Api.Filters.Clear();
grid.Api.Filters.Apply(saved);                        // back, in one batch

var state = new GridFilterState();
state.Texts["customer"] = "nord";                     // keyed by column key, not position
state.Values["region"] = ["North"];
grid.Api.Filters.Apply(state);

grid.Api.Filters.Changed += () => Log("filters changed");

Columns without a key cannot be addressed and are left out. On the controls, FilterState is the same object as a bindable property. To keep filters together with columns, sorting and grouping across sessions, use layout persistence (Pro).

Ctrl+F opens a search bar drawn over the grid. Matches are highlighted in the cells, the bar counts them, and its arrows move to the previous and next hit.

Search from code
view.SetSearchTerm("berlin");              // what Ctrl+F types into the search bar
int hits = view.SearchMatchCount;
var next = view.MoveSearch(+1);            // (Row, ElementId) of the next hit, or null
if (next is { } hit)
{
    grid.Api.Commands.EnsureSearchMatchVisible(hit);
}

Search reads what a cell shows. For a cell that draws something other than text — a composed cell, a badge — .Searchable(item => text) says what to match. An application with its own search handles Api.Events.SearchRequested; Ctrl+F then opens yours.

Ctrl+K opens a palette that searches across the rows and jumps to the one you pick. WithQuickSearch tells it what to show for a hit:

The quick-search palette
view.WithQuickSearch(
    id: o => $"A-{o.Id}",
    title: o => o.Customer,
    detail: o => $"{o.Region}, {o.Amount:N2}");

grid.OpenQuickSearch();                    // what Ctrl+K does

If the application subscribes to Api.Events.QuickSearchRequested, Ctrl+K raises that instead and opens nothing of its own.