# Tree View `TreeView` is a generic, reusable Blazor Server component that renders any tree-shaped data as an expandable/collapsible hierarchy with ARIA roles, optional guide lines, single or checkbox selection, and session-persistent expansion state. ## Overview The component lives at `src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/TreeView.razor` alongside its scoped stylesheet `TreeView.razor.css`. It is data-agnostic: the caller supplies the data source, a children accessor, a key function, and a `RenderFragment` for node content — the component owns only the structural chrome (indentation, toggle, guide lines, ARIA attributes, selection highlight, context menu positioning). Active uses within the Central UI: - **Data Connections page** (`DataConnections.razor`) — two-level Site → Connection tree with a kebab action menu per node and search-based dimming. - **Topology page** (`Topology.razor`) — three-level Site → Area → Instance tree with inline rename, context menus, and `StorageKey = "topology-tree"` expansion persistence. - **`TemplateFolderTree`** (`TemplateFolderTree.razor`) — a domain-specific wrapper that projects `TemplateFolder` / `Template` entities into `TemplateTreeNode` items and delegates to `TreeView`. Consumed by the Templates browser page (Single mode, click-to-navigate) and the Transport Export wizard (Checkbox mode, bulk template selection). ## Key Concepts ### Generic node typing `@typeparam TItem` means the component imposes no node base class. The consumer brings its own model — a record, a class, an interface — and wires four `[EditorRequired]` delegates that teach the component how to navigate it. The `TemplateTreeNode` class in `Components/Shared/TemplateTreeNode.cs` is the shared adapter type for template/folder hierarchies; the Data Connections and Topology pages use local `record` types. ### Expansion state Expanded-node keys are stored in `_expandedKeys` (`HashSet`). Keys are stringified via `KeyStr(object) => key.ToString()!` for consistency with `sessionStorage`, which stores them as a JSON array when `StorageKey` is set. On first render the component reads `sessionStorage` via `treeviewStorage.load`; on every toggle it writes back via `treeviewStorage.save`. `InitiallyExpanded` applies only when no persisted state exists (or `StorageKey` is null). The two sources are always unioned — a `RevealNode` call before the async storage read completes is not clobbered. ### Selection modes `TreeViewSelectionMode` (defined in `TreeViewSelectionMode.cs`) controls how nodes are selected: ```csharp public enum TreeViewSelectionMode { Single, // Default. Clicking node content fires SelectedKeyChanged with the node key. Checkbox, // Renders a tri-state checkbox per node. Folder check state is aggregated // from descendant leaves; only leaf keys enter SelectedKeys. } ``` In `Single` mode the component uses `SelectedKey` / `SelectedKeyChanged` (two-way binding on a single `object?`). In `Checkbox` mode it uses `SelectedKeys` / `SelectedKeysChanged` (`HashSet`). Checking a folder selects or deselects all of its descendant leaf keys. The `indeterminate` checkbox property is set via JS interop (`treeviewStorage.setIndeterminate`) after every render because Blazor does not bind `input.indeterminate` natively. ### Context menu When `ContextMenu` is non-null, right-clicking any row suppresses the browser default and positions a Bootstrap `dropdown-menu show` div at the cursor coordinates using `position: fixed`. An invisible overlay behind the menu dismisses it on click-outside; Escape also dismisses it. The menu receives the `TItem` of the right-clicked node, so the consumer's fragment can branch on node type. ## Architecture The component is a single `@typeparam` `.razor` file with a private `void RenderNode(TItem item, int depth)` local function that recurses the tree at render time — no intermediate view model is built inside the component. Every `
  • ` carries `@key="key"` so Blazor can diff the list efficiently. `IJSRuntime` is injected for two purposes: reading/writing `sessionStorage` for expansion persistence, and setting `input.indeterminate` for tri-state checkboxes. Both call sites guard `JSDisconnectedException` so a disconnected circuit never throws out of the lifecycle methods. The public surface the caller can invoke via `@ref`: ```csharp public bool IsExpanded(object key); // Whether the given key is currently expanded. public void ExpandAll(); // Expand every branch node; persists if StorageKey set. public void CollapseAll(); // Collapse every node; clears persisted state. public async Task RevealNode(object key, bool select = false); // Expands all ancestors of the given key. Optionally selects the node. // No-op if the key does not exist in the current tree. ``` ## Usage The Data Connections page binds a two-level Site → Connection tree with `StorageKey`, single selection, and a context menu for Edit and Delete actions on connection nodes: ```razor @if (node.Kind == DcNodeKind.Site) { @node.Label @node.Children.Count } else { @node.Label @node.Connection!.Protocol } @if (node.Kind == DcNodeKind.Site) { } else { } No sites configured. Add sites under Admin → Sites. @code { record DcTreeNode(string Key, string Label, DcNodeKind Kind, List Children, int? SiteId = null, DataConnection? Connection = null); enum DcNodeKind { Site, DataConnection } private TreeView? _tree; private object? _selectedKey; private void OnTreeNodeSelected(object? key) => _selectedKey = key; } ``` The `TemplateFolderTree` wrapper demonstrates `Checkbox` mode, where `SelectedKeys` / `SelectedKeysChanged` drive bulk template selection in the Transport Export wizard: ```razor @node.Name ``` ## Configuration All `[Parameter]` properties on `TreeView`. Parameters marked **required** carry `[EditorRequired]` and must be supplied; omitting them produces a build warning. | Parameter | Type | Default | Description | |---|---|---|---| | `Items` | `IReadOnlyList` | — | **Required.** Root-level nodes to render. | | `ChildrenSelector` | `Func>` | — | **Required.** Returns the ordered children of a node. | | `HasChildrenSelector` | `Func` | — | **Required.** Returns `true` for branch nodes. Determines whether the expand toggle is rendered. | | `KeySelector` | `Func` | — | **Required.** Unique stable key per node. Used for expansion tracking, selection, and `@key` diffing. | | `NodeContent` | `RenderFragment` | — | **Required.** Render fragment for node label content. Receives the `TItem`; responsible for all domain-specific markup (glyphs, labels, badges). | | `EmptyContent` | `RenderFragment?` | `null` | Shown when `Items` is empty or null. | | `ContextMenu` | `RenderFragment?` | `null` | Right-click menu content. Receives the right-clicked node. If null, right-click is not intercepted and the browser default is preserved. If non-null, `@oncontextmenu:preventDefault` is always active — the browser default is suppressed for every node regardless of whether the fragment renders any items for that node type. | | `IndentPx` | `int` | `24` | Pixels of left padding added per depth level via inline `style`. | | `ShowGuideLines` | `bool` | `true` | Adds `tv-guides` CSS class to the root `
      `, enabling the depth guide lines drawn by a `linear-gradient` pseudo-element in `TreeView.razor.css`. | | `InitiallyExpanded` | `Func?` | `null` | Predicate used to expand matching nodes on first load. When `StorageKey` is null it is applied immediately (synchronously in `OnParametersSet`). When `StorageKey` is set, the predicate is applied only after the async storage read completes and returns empty — persisted state takes precedence and `InitiallyExpanded` is a fallback for first-ever loads. | | `StorageKey` | `string?` | `null` | Browser `sessionStorage` key for expansion persistence (`treeview:{StorageKey}`). When null, expansion is in-memory only. | | `Selectable` | `bool` | `false` | Enables click-to-select on node content. Clicking the expand toggle never changes selection. | | `SelectedKey` | `object?` | `null` | Currently selected node key for `Single` mode. Supports two-way binding (`@bind-SelectedKey`). | | `SelectedKeyChanged` | `EventCallback` | — | Fires when selection changes in `Single` mode. Also fires on `RevealNode(..., select: true)`. Fires with `null` when the selected key disappears from the tree. | | `SelectedCssClass` | `string` | `"bg-primary bg-opacity-10"` | CSS class(es) applied to the selected node's row div in addition to `tv-selected`. | | `SelectionMode` | `TreeViewSelectionMode` | `Single` | Switches between single-key selection and tri-state checkbox selection. | | `SelectedKeys` | `HashSet?` | `null` | Set of currently selected leaf keys for `Checkbox` mode. | | `SelectedKeysChanged` | `EventCallback>` | — | Fires with the updated set when any checkbox is toggled in `Checkbox` mode. Always fires with a fresh `HashSet` reference. | ### CSS utility classes in `NodeContent` The scoped stylesheet defines layout slots that `NodeContent` fragments should use for consistent alignment: | Class | Purpose | |---|---| | `tv-glyph` | 20 px flex slot for a Bootstrap Icon (``). | | `tv-label` | `flex: 1 1 auto; min-width: 0` — primary text with ellipsis overflow. | | `tv-meta` | `margin-left: auto` — right-aligned badges or trailing controls. | | `tv-kebab` | Opt-in hidden-by-default "more actions" slot; revealed on row hover. | ## Dependencies & Interactions - **Bootstrap 5** — all state visuals use Bootstrap utility classes and CSS variables (`--bs-tertiary-bg`, `--bs-border-color`, `--bs-primary-rgb`). No third-party Blazor component frameworks. - **Bootstrap Icons** — static files served from `wwwroot/lib/bootstrap-icons/`; referenced once in `MainLayout.razor`. `NodeContent` fragments use `` inside the `tv-glyph` slot. - **`IJSRuntime`** — injected for `treeviewStorage.load` / `treeviewStorage.save` (expansion persistence) and `treeviewStorage.setIndeterminate` (checkbox tri-state). The JS helpers live in the CentralUI's shared JS bundle. - **`TemplateFolderTree`** (`Components/Shared/TemplateFolderTree.razor`) — a domain-specific wrapper around `TreeView` that handles folder/template tree construction, text filtering, and `ExtraTemplateChildren` injection. Consumers that need the template hierarchy use `TemplateFolderTree`; they do not wire `TreeView` directly. - **`TemplateTreeNode` / `TemplateTreeNodeKind`** (`Components/Shared/TemplateTreeNode.cs`) — the shared node model used by `TemplateFolderTree` and its callers. Folder keys are prefixed `f:`, template keys `t:`, composition keys `c:`. - **Data Connections page** (`Components/Pages/Design/DataConnections.razor`) — binds `TreeView` directly with a local two-level record type. - **Topology page** (`Components/Pages/Deployment/Topology.razor`) — binds `TreeView` for the Site → Area → Instance hierarchy; calls `ExpandAll` and `CollapseAll` via `@ref`. - **Central UI component** — see [./CentralUI.md](./CentralUI.md) for the broader Blazor Server application context. ## Related Documentation - [Tree View design specification](../requirements/Component-TreeView.md) - [Central UI](./CentralUI.md) - [Template Engine](./TemplateEngine.md)