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 TRow — DefaultRowTemplate 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 isUIComparisonOperator.LikeIgnoreCase.SortBy(source, itemProperty, direction, activeOperator, activeValue, priority)— several sorts combine by ascendingpriority.- 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:
SelectionMode—None(default),One,Many.SelectedKey— two-way, the chosen key underOne.SelectedKeys— two-way, the chosen keys in choice order underMany.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
IItemAbilitiesModel — CanSelect, 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, onItemsViewComponent,TableComponentandTreeComponentalike.
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 inWindowedmode; calling it on an already-virtualized host throws, and vice versa.WindowSizesets 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
}
}
UIItemWindowRequestcarriesAnchor(UIItemAnchor.Start/.End/.At(offset)/.Before(key)/.After(key)),Count,Mode(ReplaceorExtend, joining the read to the existing window and trimming the far side), andQuery.UIItemsQuery(Filters/Sorts, asUIItemFilterTerm/UIItemSortTerm) is what the host's filter/sort rules resolved to, evaluated against the controller value behind each rule'sSource— 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, plusOffset,TotalCount,HasMoreBefore,HasMoreAfter.UIComparisonEvaluator(src/Core/NE.Standard.UI/Items/UIComparisonEvaluator.cs) applies aUIItemsQueryin 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.
Kindpicks a node template variant added withAddNodeKind(kind, template);ConfigureDefaultNode/SetNodeTemplateshape the one used when no variant matches.HasChildrenlets a node claim children before any are in the list;OnNodeUnfold/OnNodeUnfoldWithItemKeyfire the first time it is unfolded, and the controller answers by adding the children or clearing the flag.Renamable/RenameOnDoubleClickplus a node's ownCanRenamegate F2/double-click renaming; a rename writes the new title back through the node's own two-wayTitlebinding, andOnNodeRename/OnNodeRenameWithItemKeyfire afterward for refusing or normalizing it.Draggableplus a node'sCanDraggate dragging a node onto another; the drop writesDropTargetand raisesOnNodeMove/OnNodeMoveWithItemKey, and the controller moves the node or leaves it — nothing moves on the client on its own.Indentsets how far each level steps in, in pixels;ShowFoldChevrontoggles 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.