Improve filter panel restore logic and doc clarity
Major overhaul of MgGridFilterPanelTagBox restore/healing logic: - Level-triggered healing prevents lost filter criteria after async loads. - Healing now only triggers when needed; delayed assert ensures stability. - ValuesChanged event guarded to avoid accidental filter clearing. - Diagnostics dormant but retained for future debugging. - Comments and summaries updated for clarity. Docs (MGGRID_PARAMETERS.md, 0004-signalr-datasource-server-side-filtering.md) updated to explain new logic, rationale, and future plans. Minor refactor in GridShippingDocument.razor for reload clarity. No functional change in ShippingsAdmin.razor (comment only).
This commit is contained in:
parent
efd4c67cc0
commit
11d1ae1d17
|
|
@ -46,14 +46,15 @@ public class MgGridFilterPanelTagBox<TData, TValue> : MgTagBox<TData, TValue>, I
|
|||
InheritSizeModeFromGrid();
|
||||
|
||||
TrackEditorSnapshot();
|
||||
Diag("OnParametersSet");
|
||||
//Diag("OnParametersSet");
|
||||
SyncSelectionToCriteria();
|
||||
}
|
||||
|
||||
// ======================= TEMP DIAGNOSTIC — REMOVE after the restore-race investigation =======================
|
||||
// Purpose: prove/disprove that DevExpress raises ValuesChanged with an EMPTY list around Reload() (dropping
|
||||
// not-yet-resolvable tags), which OnSelectionChanged would misread as a user edit and clear the criteria.
|
||||
// Blazor WASM: Console.WriteLine lands in the browser console. Filter the console by "[MgFP".
|
||||
// ---- Dormant diagnostics -------------------------------------------------------------------------------
|
||||
// Console tracing for the restore/heal flow (Blazor WASM: Console.WriteLine lands in the browser console;
|
||||
// filter by "[MgFP"). The call sites are commented out — uncomment them when tracing is needed again
|
||||
// (e.g. during the v2 envelope/pull-model work, ADR 0004). This tooling proved the ValuesChanged-drop and
|
||||
// the fill-timing root causes; keep it.
|
||||
|
||||
private static string Fmt(IEnumerable<TValue>? values) => values is null ? "null" : $"[{string.Join(",", values)}]";
|
||||
|
||||
|
|
@ -77,7 +78,6 @@ public class MgGridFilterPanelTagBox<TData, TValue> : MgTagBox<TData, TValue>, I
|
|||
Console.WriteLine($"[MgFP:{FieldName}] {message} | dataCount={dataCount} snapWasEmpty={_snapshotTakenWhileDataEmpty} " +
|
||||
$"values={Fmt(Values)} criteria={criteria}");
|
||||
}
|
||||
// ===================== /TEMP DIAGNOSTIC =====================================================================
|
||||
|
||||
// ---- Editor-snapshot state -----------------------------------------------------------------------------
|
||||
// A restore has TWO inputs that arrive in arbitrary order: the criteria (async layout restore via JS interop)
|
||||
|
|
@ -91,10 +91,14 @@ public class MgGridFilterPanelTagBox<TData, TValue> : MgTagBox<TData, TValue>, I
|
|||
|
||||
private IEnumerable<TData>? _snapshotData;
|
||||
private bool _snapshotTakenWhileDataEmpty;
|
||||
private bool _reassertAfterReloadPending;
|
||||
|
||||
private bool IsDataEmpty => Data is null || !Data.Any();
|
||||
|
||||
// The measured editor-internal reload (a JS hop) settles in ~300 ms; the delay is a generous cover, not a
|
||||
// contract — the v2 envelope/pull model removes the need for it entirely (see ADR 0004 → planned v2).
|
||||
private const int HealAssertDelayMs = 750;
|
||||
private bool _healAssertScheduled;
|
||||
|
||||
/// <summary>
|
||||
/// Records whether the editor's current item snapshot was taken against an EMPTY <c>Data</c>. Runs on every
|
||||
/// parameter pass REGARDLESS of criteria presence — arming must not depend on the criteria arriving first;
|
||||
|
|
@ -119,25 +123,64 @@ public class MgGridFilterPanelTagBox<TData, TValue> : MgTagBox<TData, TValue>, I
|
|||
|
||||
/// <summary>
|
||||
/// Forces the editor to re-read its <c>Data</c> source when its snapshot is known-stale (taken while empty)
|
||||
/// and <c>Data</c> has since been filled in place. Returns true when a reload was issued, and queues ONE
|
||||
/// deferred re-assert: DevExpress documents no settle guarantee for <c>Reload()</c>
|
||||
/// (<c>DxListEditorBase.Reload</c> is a bare <c>void</c>), so a same-pass <c>Values</c> assignment could still
|
||||
/// race the item list.
|
||||
/// and <c>Data</c> has since been filled in place. SAFE ORDER (console-proven, 2026-07-17): the reload runs
|
||||
/// against an EMPTY editor — no value sits in it (see the data-empty gates in <see cref="SyncSelectionToCriteria"/>
|
||||
/// and <see cref="SyncFromGrid"/>) — so the editor's async revalidation has nothing to drop and raises no
|
||||
/// spurious <c>ValuesChanged</c>. The selection is then asserted ONCE, delayed, into the settled editor
|
||||
/// (<see cref="ScheduleHealAssert"/>); its echo rewrites the same criteria, which is harmless.
|
||||
/// </summary>
|
||||
private bool HealEditorSnapshotIfStale()
|
||||
private void HealEditorSnapshotIfStale()
|
||||
{
|
||||
if (!_snapshotTakenWhileDataEmpty || IsDataEmpty) return false;
|
||||
if (!_snapshotTakenWhileDataEmpty || IsDataEmpty) return;
|
||||
|
||||
_snapshotTakenWhileDataEmpty = false;
|
||||
//Diag(">>> HEAL: calling DevExpress Reload() on an EMPTY editor");
|
||||
Reload();
|
||||
_reassertAfterReloadPending = true;
|
||||
//Diag("<<< HEAL: Reload() returned");
|
||||
|
||||
return true;
|
||||
ScheduleHealAssert();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Brings the displayed selection in line with our field's active criteria: heals a stale editor snapshot
|
||||
/// first, then asserts the values when the snapshot was just healed or the selection drifted.
|
||||
/// Schedules the ONE selection assert that follows a heal, delayed until the editor's internal reload has
|
||||
/// settled. Re-reads the criteria AT FIRE TIME — if it is gone by then (user cleared it, panel hidden), no-op.
|
||||
/// </summary>
|
||||
private void ScheduleHealAssert()
|
||||
{
|
||||
if (_healAssertScheduled) return;
|
||||
_healAssertScheduled = true;
|
||||
|
||||
_ = InvokeAsync(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(HealAssertDelayMs);
|
||||
_healAssertScheduled = false;
|
||||
|
||||
if (Grid is null || string.IsNullOrWhiteSpace(FieldName) || IsDataEmpty) return;
|
||||
if (Grid.GetFilterPanelCriteria(FieldName) is not { } criteria) return;
|
||||
if (!TryExtractInValues(criteria, out var values)) return;
|
||||
|
||||
//Diag($"HEAL-assert (delayed {HealAssertDelayMs} ms) -> {Fmt(values)}");
|
||||
SetValuesCore(values);
|
||||
StateHasChanged();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Best-effort: a failed delayed assert leaves the box empty — the criteria is untouched and the
|
||||
// Clear button still signals it; a later reconcile/parameter pass re-asserts.
|
||||
//Diag("HEAL-assert FAILED");
|
||||
_healAssertScheduled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Brings the displayed selection in line with our field's active criteria. GATE: while <c>Data</c> is empty
|
||||
/// NOTHING is asserted — an unresolvable value sitting in the editor is exactly the ammunition the editor's
|
||||
/// async revalidation needs to raise a spurious empty <c>ValuesChanged</c> (console-proven to destroy the
|
||||
/// criteria). The criteria itself stays untouched meanwhile (rows filter, the Clear button signals it); the
|
||||
/// assert happens once the data is there — immediately when no heal was needed, delayed after a heal.
|
||||
/// </summary>
|
||||
private void SyncSelectionToCriteria()
|
||||
{
|
||||
|
|
@ -145,10 +188,14 @@ public class MgGridFilterPanelTagBox<TData, TValue> : MgTagBox<TData, TValue>, I
|
|||
if (Grid.GetFilterPanelCriteria(FieldName) is not { } criteria) return;
|
||||
if (!TryExtractInValues(criteria, out var criteriaValues)) return;
|
||||
|
||||
var healed = HealEditorSnapshotIfStale();
|
||||
var drifted = Values is null || !Values.SequenceEqual(criteriaValues);
|
||||
if (IsDataEmpty) return; // no value may enter an empty editor
|
||||
|
||||
if (healed || drifted) SetValuesCore(criteriaValues);
|
||||
HealEditorSnapshotIfStale(); // reloads the still-empty editor + schedules the delayed assert
|
||||
|
||||
if (_healAssertScheduled) return; // the delayed assert owns the write — don't race the editor's reload
|
||||
|
||||
var drifted = Values is null || !Values.SequenceEqual(criteriaValues);
|
||||
if (drifted) SetValuesCore(criteriaValues);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -181,27 +228,17 @@ public class MgGridFilterPanelTagBox<TData, TValue> : MgTagBox<TData, TValue>, I
|
|||
// Register once, after the first render (grid API calls are unsafe mid-render). The grid holds the
|
||||
// registration weakly and reconciles this component immediately — restoring a persisted panel filter
|
||||
// into the UI, or clearing it while the panel is hidden. See IMgGridFilterPanelComponent / ADR 0004.
|
||||
if (firstRender) Grid?.RegisterFilterPanelComponent(this);
|
||||
|
||||
// Deferred re-assert after a Reload(): the reload-triggered render has completed by now, so the editor's
|
||||
// item list is settled even though Reload() itself gives no such guarantee. Flag-gated and idempotent —
|
||||
// the re-assert's own render finds the flag cleared, so this cannot loop.
|
||||
if (_reassertAfterReloadPending)
|
||||
if (firstRender)
|
||||
{
|
||||
_reassertAfterReloadPending = false;
|
||||
|
||||
if (Grid is not null && !string.IsNullOrWhiteSpace(FieldName)
|
||||
&& Grid.GetFilterPanelCriteria(FieldName) is { } criteria
|
||||
&& TryExtractInValues(criteria, out var values))
|
||||
{
|
||||
SetValuesCore(values); // fresh instance => the editor re-resolves its tags
|
||||
StateHasChanged();
|
||||
}
|
||||
//Diag("OnAfterRender(firstRender) -> RegisterFilterPanelComponent");
|
||||
Grid?.RegisterFilterPanelComponent(this);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSelectionChanged(IEnumerable<TValue>? values)
|
||||
{
|
||||
//Diag($"*** ValuesChanged CALLBACK — incoming={Fmt(values)}");
|
||||
|
||||
SetValuesCore(values);
|
||||
ApplyFilterPanelCriteria(values);
|
||||
}
|
||||
|
|
@ -215,6 +252,21 @@ public class MgGridFilterPanelTagBox<TData, TValue> : MgTagBox<TData, TValue>, I
|
|||
// Empty selection => null => clears this field's criteria. Otherwise IN(FieldName, selected...).
|
||||
CriteriaOperator? criteria = selected.Length > 0 ? new InOperator(FieldName, selected) : null;
|
||||
|
||||
// OWNERSHIP: an empty selection may only clear OUR criteria. The per-field slot is shared with the
|
||||
// grid's filter row — a not-owned criteria (e.g. Contains("EUR") typed there) must survive any empty
|
||||
// ValuesChanged, be it a genuine user clear (nothing of ours is active anyway) or the echo of a
|
||||
// programmatic clear.
|
||||
if (criteria is null
|
||||
&& Grid.GetFilterPanelCriteria(FieldName) is { } current && !OwnsCriteria(current))
|
||||
{
|
||||
//Diag("WRITE skipped — empty selection over a NOT-OWNED criteria (filter-row value protected)");
|
||||
return;
|
||||
}
|
||||
|
||||
//Diag(criteria is null
|
||||
// ? "!!! WRITE criteria -> NULL (CLEARING the field's criteria)"
|
||||
// : $"WRITE criteria -> {criteria}");
|
||||
|
||||
Grid.SetFilterPanelCriteria(FieldName, criteria);
|
||||
}
|
||||
|
||||
|
|
@ -229,21 +281,26 @@ public class MgGridFilterPanelTagBox<TData, TValue> : MgTagBox<TData, TValue>, I
|
|||
// NOTE: CriteriaOperator overloads == / != (they BUILD criteria) — null checks must be pattern-based.
|
||||
var criteria = Grid.GetFilterPanelCriteria(FieldName);
|
||||
|
||||
//Diag($"SyncFromGrid (from grid reconcile) — criteria={(criteria is null ? "null" : criteria.ToString())}");
|
||||
|
||||
if (criteria is not null && TryExtractInValues(criteria, out var values))
|
||||
{
|
||||
// The grid may be telling us about a criteria whose lookup data has only just arrived — e.g. the
|
||||
// consumer calling RefreshFilterPanel() right after an in-place fill. Without healing the editor's
|
||||
// (empty) snapshot first, the assert below could never resolve into tags: this is what made
|
||||
// RefreshFilterPanel fall short of its documented contract.
|
||||
// Same gate as SyncSelectionToCriteria: no value may enter an empty editor — the criteria stays
|
||||
// untouched and the assert happens once the lookup data exists (this is also what made the consumer's
|
||||
// RefreshFilterPanel call after an in-place fill actually work: the heal below reloads the editor).
|
||||
if (IsDataEmpty) return;
|
||||
|
||||
HealEditorSnapshotIfStale();
|
||||
SetValuesCore(values);
|
||||
|
||||
if (!_healAssertScheduled) SetValuesCore(values);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Null criteria, or a criteria that is not ours: the per-field slot is SHARED with the grid's filter
|
||||
// row (e.g. Contains("EUR") typed into the column filter) — leave it untouched (it is visible in the
|
||||
// grid's own filter UI, so it is not invisible filtering) and just show no selection.
|
||||
SetValuesCore(null);
|
||||
// grid's own filter UI, so it is not invisible filtering) and just show no selection. Only write when
|
||||
// there is something to clear: a null-over-null write can still echo a spurious ValuesChanged.
|
||||
if (Values is not null && Values.Any()) SetValuesCore(null);
|
||||
}
|
||||
|
||||
// May be called from the grid (outside this component's render path) — request our own re-render.
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue