perf(adminui): RawTree renders a flat Virtualize list, pages 500 per level, caches menus (R04 P5)
The recursive RenderNode rendered every row of an expanded device, and built a fresh context-menu list + closures per node per render. RawTreeRows flattens the expanded tree (explicit stack) into rows rendered by one Virtualize; each level shows 500 children, then a 'Show more' row. RawNodeMenuCache keys menus weakly on the node instance and rebuilds on a picker-mode or Enabled change. Refs #745 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
@@ -17,15 +17,19 @@
|
||||
surface for delete-blockers and rename warnings. This
|
||||
component is a single instance that recurses via the RenderNode RenderFragment, so it
|
||||
owns one set of modals + one injected service for the whole tree. *@
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers
|
||||
@inject IRawTreeService Svc
|
||||
|
||||
@foreach (var root in Roots)
|
||||
{
|
||||
@RenderNode(root, 0)
|
||||
}
|
||||
@* One flat, virtualized row list (R04 P5): only the rows in view render, and each expanded level
|
||||
shows RawTreeRows.DefaultPageSize children before a "Show more" row. Flatten runs per render; it is
|
||||
O(visible rows) with no allocation per node beyond one row struct, so every mutation path below
|
||||
(reload, toggle, load) is picked up without separate invalidation. *@
|
||||
<Virtualize Items="@RawTreeRows.Flatten(Roots)" ItemSize="30" OverscanCount="20" Context="row">
|
||||
@RenderRow(row)
|
||||
</Virtualize>
|
||||
|
||||
@* --- Real modals + dialogs (one instance each for the whole recursive tree) --- *@
|
||||
<DriverConfigModal @bind-Visible="_driverCfgVisible" DriverInstanceId="@_driverCfgId" OnSaved="DriverCfgSaved" />
|
||||
@@ -163,12 +167,45 @@
|
||||
// Row rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Per-node menus are built once and reused across renders (R04 P5); see RawNodeMenuCache.
|
||||
private readonly RawNodeMenuCache _menus = new();
|
||||
|
||||
private RenderFragment RenderRow(RawTreeRow row) => __builder =>
|
||||
{
|
||||
switch (row.Kind)
|
||||
{
|
||||
case RawTreeRowKind.Node:
|
||||
@RenderNode(row.Node, row.Depth)
|
||||
break;
|
||||
case RawTreeRowKind.Loading:
|
||||
<div @key="row.Key" class="small text-muted py-1" style="@($"padding-left:{row.Depth * 18}px")">
|
||||
<span class="spinner-border spinner-border-sm me-1"></span>Loading…
|
||||
</div>
|
||||
break;
|
||||
case RawTreeRowKind.Error:
|
||||
<div @key="row.Key" class="small text-danger py-1" style="@($"padding-left:{row.Depth * 18}px")">
|
||||
@row.Node.Error
|
||||
</div>
|
||||
break;
|
||||
default:
|
||||
<div @key="row.Key" class="small py-1" style="@($"padding-left:{row.Depth * 18}px")">
|
||||
<button type="button" class="btn btn-sm btn-link p-0" @onclick="@(() => ShowMore(row.Node))">
|
||||
Show @Math.Min(row.Remaining, RawTreeRows.DefaultPageSize) more
|
||||
(@row.Remaining not shown)
|
||||
</button>
|
||||
</div>
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private void ShowMore(RawNode node) => RawTreeRows.ShowMore(node);
|
||||
|
||||
private RenderFragment RenderNode(RawNode node, int depth) => __builder =>
|
||||
{
|
||||
var indent = $"padding-left:{depth * 18}px";
|
||||
var hasExpander = node.HasLazyChildren || node.Children.Count > 0;
|
||||
var muted = node.Enabled == false && node.Kind is RawNodeKind.Driver or RawNodeKind.Device;
|
||||
var items = MenuFor(node);
|
||||
var items = _menus.GetOrBuild(node, PickerMode, MenuFor);
|
||||
|
||||
<div @key="node.Key" class="d-flex align-items-center gap-1 py-1" style="@indent">
|
||||
@if (hasExpander)
|
||||
@@ -194,26 +231,6 @@
|
||||
@RenderRowBody(node, muted)
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (node.Expanded && node.Loading)
|
||||
{
|
||||
<div class="small text-muted" style="@($"padding-left:{(depth + 1) * 18}px")">
|
||||
<span class="spinner-border spinner-border-sm me-1"></span>Loading…
|
||||
</div>
|
||||
}
|
||||
else if (node.Expanded && node.Error is not null)
|
||||
{
|
||||
<div class="small text-danger" style="@($"padding-left:{(depth + 1) * 18}px")">
|
||||
@node.Error
|
||||
</div>
|
||||
}
|
||||
else if (node.Expanded)
|
||||
{
|
||||
@foreach (var child in node.Children)
|
||||
{
|
||||
@RenderNode(child, depth + 1)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// The icon + label + badge shown for a node — the ContextMenu right-click surface.
|
||||
|
||||
Reference in New Issue
Block a user