fix(centralui): safe-invoke fire-and-forget calls in AlarmSummary and TreeView (review 2026-10-21 07/M-4)
This commit is contained in:
@@ -449,7 +449,7 @@
|
|||||||
private void OnLiveAlarmsChanged(int siteId)
|
private void OnLiveAlarmsChanged(int siteId)
|
||||||
{
|
{
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
_ = InvokeAsync(() =>
|
_ = SafeInvokeAsync(() =>
|
||||||
{
|
{
|
||||||
// Drop stale callbacks for a site we've since navigated away from, and
|
// Drop stale callbacks for a site we've since navigated away from, and
|
||||||
// let the poll drive until the aggregator has actually seeded (so we never
|
// let the poll drive until the aggregator has actually seeded (so we never
|
||||||
@@ -464,6 +464,16 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Runs the action on the render thread; a callback racing circuit teardown makes
|
||||||
|
// InvokeAsync throw ObjectDisposedException into an otherwise-unobserved task
|
||||||
|
// (pattern from DebugView.SafeInvokeAsync).
|
||||||
|
private async Task SafeInvokeAsync(Action action)
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
try { await InvokeAsync(action); }
|
||||||
|
catch (ObjectDisposedException) { /* disposed between the guard and the dispatch */ }
|
||||||
|
}
|
||||||
|
|
||||||
// Rebuilds rows/rollup/visible-rows from the live snapshot. Leaves _notReporting
|
// Rebuilds rows/rollup/visible-rows from the live snapshot. Leaves _notReporting
|
||||||
// as the last poll computed it (the alarm-only cache can't know it). Idempotent.
|
// as the last poll computed it (the alarm-only cache can't know it). Idempotent.
|
||||||
private void ApplyLiveSnapshot(int siteId)
|
private void ApplyLiveSnapshot(int siteId)
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ else
|
|||||||
// Clear selection if the selected key no longer exists in the current items tree
|
// Clear selection if the selected key no longer exists in the current items tree
|
||||||
if (Selectable && SelectedKey != null && _items is not null && !KeyExistsInTree(_items, SelectedKey))
|
if (Selectable && SelectedKey != null && _items is not null && !KeyExistsInTree(_items, SelectedKey))
|
||||||
{
|
{
|
||||||
_ = SelectedKeyChanged.InvokeAsync(null);
|
_ = SafeFireAndForgetAsync(() => SelectedKeyChanged.InvokeAsync(null));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,10 +553,22 @@ else
|
|||||||
if (StorageKey != null)
|
if (StorageKey != null)
|
||||||
{
|
{
|
||||||
var json = System.Text.Json.JsonSerializer.Serialize(_expandedKeys.ToList());
|
var json = System.Text.Json.JsonSerializer.Serialize(_expandedKeys.ToList());
|
||||||
_ = JSRuntime.InvokeVoidAsync("treeviewStorage.save", StorageKey, json);
|
_ = SafeFireAndForgetAsync(() =>
|
||||||
|
JSRuntime.InvokeVoidAsync("treeviewStorage.save", StorageKey, json).AsTask());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fire-and-forget calls (an EventCallback, a JS persist) can race circuit teardown;
|
||||||
|
// swallow the expected disposal/disconnect failures so no unobserved-task exception
|
||||||
|
// reaches the log (review 2026-10-21 07/M-4).
|
||||||
|
private static async Task SafeFireAndForgetAsync(Func<Task> work)
|
||||||
|
{
|
||||||
|
try { await work(); }
|
||||||
|
catch (ObjectDisposedException) { }
|
||||||
|
catch (JSDisconnectedException) { }
|
||||||
|
catch (TaskCanceledException) { }
|
||||||
|
}
|
||||||
|
|
||||||
private async Task OnContentClick(object key)
|
private async Task OnContentClick(object key)
|
||||||
{
|
{
|
||||||
// Clicking a node makes it the tab entry point (roving tabindex follows the
|
// Clicking a node makes it the tab entry point (roving tabindex follows the
|
||||||
|
|||||||
Reference in New Issue
Block a user