NE.Standard

Items

An items host is any component that renders a keyed collection through a template: ItemsViewComponent, TableComponent and TreeComponent are the general-purpose ones, and MenuComponent, TabsViewComponent and the SelectComponent/RadioGroupComponent/SearchComponent inputs are all items hosts too — they add a menu, a tab strip driven by a collection, or options on top of the same collection machinery.

Static vs bound items

SetItems/AddItem/AddItems hold a plain, author-declared list — data that never changes per session and renders fully on the server, cached alongside the view itself:

new TableComponent()
    .SetItems(DemoDeploymentRow.CreateDeployments())
    .AddTextColumn("Service", nameof(DemoDeploymentRow.Service));

BindItems(path, scope) points the collection at a RecursiveCollection on the controller instead. It is generated for every items host from the Items property, so its scope defaults follow the same rule as any other binding — Root unless you pass Relative.

new ItemsViewComponent()
    .BindItems(nameof(SomeController.Rows))
    .SetTemplate(rowTemplate);

Every item is keyed

An item collection — static or bound — must hold IBindableItems, each with a stable string Id; the compiler refuses anything else. Wrap a plain value with UIValueItem<T>, whose Id is derived from the value itself (so the same value cannot appear twice), or UIOptionValue<T>, which also fills Title so Select/Search/RadioGroup render options with no template of their own. Id is declared [RecursiveMember(false)] — it registers a path segment but generates no setter, because a notifying Id would let an item be re-keyed after insertion and orphan every path built from the old key.

The built-in row shapes live under src/Core/NE.Standard.UI.Components/BuiltIns/Models/: TextItem, OptionItem, MenuItem, TabItem, BadgeItem, ButtonItem, TreeNode, KeyValueActionItem, BreadcrumbItem. Every styling property on them starts null, so an item says only what its author gave it and falls back to the template drawing it; Visibility and Enabled are the exception and default to Visible/true.

Item templates

SetTemplate/AddTemplateVariant (declared on ItemsComponentBase<TComponent, TItem, TTemplate>) set the default template and named variants; TemplateKeyProperty (on TemplatedComponentBase) names the item property whose value picks a variant by name — MenuItem.Kind choosing between the header, separator, check, select and plain entry templates is the built-in example. FallbackTemplateKey names the variant used when the value names none. Patching the property named by TemplateKeyProperty re-renders that one item through a collection Replace, so any client-side state the row held is gone — that is the mechanism, not a bug.

Inside a template, bindings default to Root like everywhere else — .BindTitle(path, UIBindingScope.Relative) is what reaches the item's own properties, and it is almost always what you want inside a row:

new StackPanelComponent()
    .AddChild(new TextComponent()
        .BindTitle(nameof(DemoRowItem.Title), UIBindingScope.Relative)
    );

There is no "current item index" argument — an item is addressed by key. UIAction.ArgCurrentItemKey(name) and UIAction.ArgCurrentItem(name) pass the row's key or the row itself as a command argument:

.OnRowClickWithItemKey(nameof(Controller.OpenRow))

RowItemsComponentBase<TComponent, TItem, TRow> (the base of TableComponent, TreeComponent and KeyValueActionComponent) wraps the whole row in a TRowDefaultRowTemplate for all three — and exposes OnRowClick/OnRowClickWithItem/OnRowClickWithItemKey, OnRowOpen/OnRowOpenWithItemKey (Enter, or a double click) and OnRowRemove/OnRowRemoveWithItemKey (Delete on a removable row). ItemsViewComponent carries the parallel vocabulary directly — OnItemClick/OnItemClickWithItem/OnItemClickWithItemKey, OnItemOpen/OnItemOpenWithItemKey and OnItemRemove/OnItemRemoveWithItemKey — so a plain list, a table and a tree all raise a row's click, open and remove the same way.

Grouping, filtering and sorting

GroupedItemsComponentBase adds SetGroupTemplate for a header drawn above each group. FilterBy and SortBy (on ItemsComponentBase) add rules evaluated against an item property:

.FilterBy(filterBoxId, IInputComponent.ValueProperty, nameof(DemoDeploymentRow.Service))
.SortBy(nameof(ITabItemModel.Order))
  • FilterBy(source, itemProperty, operator, activeOperator, activeValue) ties a filter to another component's property (or, with no source, to a constant); the default operator is UIComparisonOperator.LikeIgnoreCase.
  • SortBy(source, itemProperty, direction, activeOperator, activeValue, priority) — several sorts combine by ascending priority.
  • With no source, both are unconditionally active.

They compile onto UIItemsView (_filters/_sorts) and run client-side on a plain or virtualized host — filtering hides rows via a class rather than removing them, and fails open: an item whose value never reached the client stays visible rather than silently disappearing. A windowed host resolves rules on the server instead (see below), because the client only ever holds one window of a source that may hold thousands.

Selection

ISelectableItemsComponent (blocked onto ItemsViewComponent, TableComponent, TreeComponent) adds:

  • SelectionModeNone (default), One, Many.
  • SelectedKey — two-way, the chosen key under One.
  • SelectedKeys — two-way, the chosen keys in choice order under Many.
  • ISelectionStyleComponent.SelectionStyle — what a chosen row looks like; unset parts fall back to the control's own default.

A bound SelectedKey under Many (or SelectedKeys under One/None) compiles with a warning (CompiledView.Warnings), logged rather than refused. Nothing prunes SelectedKey/SelectedKeys when the item they name leaves the collection — a controller that removes a chosen item clears the key itself.

Item abilities and whole-control switches

IItemAbilitiesModelCanSelect, CanDrag, CanRemove, CanRename, CanShowContextMenu, all bool? and unset meaning "may" — lets one item opt out of what its host otherwise allows. The built-in row templates bind each to the matching property of IItemAbilitiesComponent on the row, so a flag flipped on a live item reaches its row like any other bound property.

The host-wide switches sit beside these per-item flags and gate the mechanism itself:

  • TreeComponent: Draggable, Renamable, RenameOnDoubleClick, Removable, ShowFoldChevron.
  • TabsViewComponent: Draggable (reordering by dragging captions), Renamable, Removable, ShowOverflow.
  • IRowHoverableComponent.RowHoverable — whether rows highlight under the pointer, on ItemsViewComponent, TableComponent and TreeComponent alike.

Off, the control keeps no chrome for the mechanic at all — Removable = false on a tab strip means no caption shows a close and no room is kept for one, not merely that closing fails.

Hosting modes

IItemsHostComponent.HostMode (ItemsViewComponent, TableComponent) is one of Plain, Virtualized, Windowed — never set directly:

  • Plain (default) — every item is real DOM, rules run over the children.
  • Virtualized() — the client holds every item's value and keeps only the rows in view (plus overscan) in the document; rules run over the values and produce the projected order. Calling it on a host already bound to a source throws.
  • BindSource(path, scope) — binds to a windowed source (below) and puts the host in Windowed mode; calling it on an already-virtualized host throws, and vice versa.
  • WindowSize sets how many items one window holds (default 50); not bindable, read once by the client.
new ItemsViewComponent()
    .BindSource(nameof(Controller.Rows))
    .SetWindowSize(50)
    .FilterBy(filterBoxId, IInputComponent.ValueProperty, nameof(DemoRowItem.Title));

Windowed sources

A windowed host's items come from UIItemSourceBase<TItem> (src/Core/NE.Standard.UI/Data/), for a collection too large to hold whole — a hundred thousand rows, a conversation read backwards. Subclass it and implement one member:

internal sealed class DemoRowsSource : UIItemSourceBase<DemoRowItem>
{
    protected override Task<UIItemWindow<DemoRowItem>> GetWindowAsync(
        UIItemWindowRequest request, CancellationToken cancellationToken)
    {
        // read `request.Anchor`, `request.Count`, `request.Query`, return the window that answers it
    }
}
  • UIItemWindowRequest carries Anchor (UIItemAnchor.Start/.End/.At(offset)/.Before(key)/.After(key)), Count, Mode (Replace or Extend, joining the read to the existing window and trimming the far side), and Query.
  • UIItemsQuery (Filters/Sorts, as UIItemFilterTerm/UIItemSortTerm) is what the host's filter/sort rules resolved to, evaluated against the controller value behind each rule's Source — a rule source must be bound on a windowed host, refused at compile time otherwise, because an unbound control lives only in the browser and the server could never see it. An inactive rule contributes no term, so an empty filter box means "everything."
  • UIItemWindow<TItem> is the answer: Items, plus Offset, TotalCount, HasMoreBefore, HasMoreAfter.
  • UIComparisonEvaluator (src/Core/NE.Standard.UI/Items/UIComparisonEvaluator.cs) applies a UIItemsQuery in memory, for a source with nothing better than a scan — a source over a database translates the terms into its own query instead.

The UIItemSourceBase<TItem> protected helpers — Append, Prepend, Remove, Invalidate — mutate the realized window and its TotalCount/HasMoreBefore/HasMoreAfter bookkeeping directly; there is no event to raise, changing Items is the change notification. A source is itself RecursiveObservable and typically lives as a [RecursiveMember(false)] property on the controller:

[RecursiveMember(false)]
public DemoRowsSource Rows { get; } = new();

Read the first window in OnInitializeAsync so the page paints with rows already in it — the client can read further windows itself, but nothing paints before the first read completes.

The table's columns

TableComponent.AddColumn(caption, template, width, alignment, key) registers a UITableColumn and a template variant keyed column:{key} (key defaults to a 1-based ordinal). AddTextColumn(caption, propertyPath, width, alignment, key) is the shorthand for a text cell, binding a DefaultTextTemplate relatively to the row:

new TableComponent()
    .SetItems(rows)
    .AddTextColumn("Service", nameof(DemoDeploymentRow.Service))
    .AddTextColumn("Replicas", nameof(DemoDeploymentRow.Replicas), UIGridUnit.Absolute(96), UITextAlignment.End)
    .AddColumn("Status", badgeTemplate, UIGridUnit.Absolute(120));

Columns is read-only and render-time only (IsBindable = false) — the columns are how the table is built, not state. ShowHeader, Striped, ShowColumnSeparators, ShowRowSeparators, ResizableColumns (client-side only; widths are never sent back) round out the table's own switches. Sorting by header, editing and paging are not built in.

The tree

TreeComponent renders ITreeNodeModel rows — a text item plus ParentId, Kind, HasChildren, Expanded, DropTarget — as a flat, keyed list in walking order, not a nested structure. TreeNode (the built-in model) and TreeNode.Flatten(roots, children, toNode) turn a nested source into that list, stamping ParentId on the way down.

  • Kind picks a node template variant added with AddNodeKind(kind, template); ConfigureDefaultNode/ SetNodeTemplate shape the one used when no variant matches.
  • HasChildren lets a node claim children before any are in the list; OnNodeUnfold/OnNodeUnfoldWithItemKey fire the first time it is unfolded, and the controller answers by adding the children or clearing the flag.
  • Renamable/RenameOnDoubleClick plus a node's own CanRename gate F2/double-click renaming; a rename writes the new title back through the node's own two-way Title binding, and OnNodeRename/ OnNodeRenameWithItemKey fire afterward for refusing or normalizing it.
  • Draggable plus a node's CanDrag gate dragging a node onto another; the drop writes DropTarget and raises OnNodeMove/OnNodeMoveWithItemKey, and the controller moves the node or leaves it — nothing moves on the client on its own.
  • Indent sets how far each level steps in, in pixels; ShowFoldChevron toggles the chevron for a node that can unfold.

Items hosts beyond lists

MenuComponent renders IMenuItemModel entries with Kind selecting header/separator/check/select/plain variants (a check shows Checked as a mark at its end, a select shows Value and opens its Items beside it), an entry's Items as a nested menu one level deep, and folds to icons alone via ICollapsibleComponent. TabsViewComponent renders ITabItemModel entries sorted by Order, with a two-way SelectedKey for the open page — its pages come from the collection, unlike TabsComponent, whose captions are fixed regions the view authors directly and which is not an items host at all. SelectComponent, SearchComponent and RadioGroupComponent extend OptionsInputComponentBase<TComponent, TItem>AddOption/AddOptions/SetOptions/BindOptions are the option-flavoured names for AddItem/AddItems/SetItems/BindItems, over IOptionModel items.

See items-view, table, tree and models for the full property and model reference.