fix(ui): EventListShell — reload on Filter swap + harden refresh-timer (HIGH+MED)
* Filter changes from the parent (page-state singleton swap) now trigger LoadAsync via OnParametersSetAsync once the component has rendered. Previously the reload happened only on first render, so navigating back to a list page with a different sport/date filter showed the prior data. * Refresh-timer Elapsed handler is hoisted to a named async-void method with try/catch around InvokeAsync. An unhandled exception on the timer thread used to crash WebView2; now it's logged and swallowed. * Overlapping ticks are skipped via a _loading short-circuit so a slow Loader doesn't stack up cancelled-superseded loads.
This commit is contained in:
@@ -317,6 +317,11 @@
|
||||
[Parameter] public IReadOnlyList<string>? AvailableCountries { get; set; }
|
||||
[Parameter] public bool LiveMode { get; set; }
|
||||
[Parameter] public int AutoRefreshSeconds { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
/// Legacy hint from parent pages that the source data is unseeded;
|
||||
/// currently advisory only — the empty-state copy is unconditional.
|
||||
/// </summary>
|
||||
[Parameter] public bool Stale { get; set; }
|
||||
|
||||
private EventBrowsingState.PageFilter _filter = default!;
|
||||
@@ -326,15 +331,24 @@
|
||||
private List<EventListItem> _rows = new();
|
||||
private DateTimeOffset? _lastLoadedAt;
|
||||
private bool _loading;
|
||||
private bool _firstRendered;
|
||||
private System.Timers.Timer? _refreshTimer;
|
||||
private readonly Dictionary<string, RatesSnap> _previousRates = new(StringComparer.Ordinal);
|
||||
|
||||
protected override void OnParametersSet()
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
// Reload when the parent swaps the Filter parameter for a different
|
||||
// instance — page state singletons hand us a new record on every
|
||||
// change, so reference inequality is the right trigger. The first
|
||||
// render still kicks off via OnAfterRenderAsync; this branch covers
|
||||
// subsequent parent-driven filter swaps (e.g. nav between pre-match
|
||||
// and live, or restoring from EventBrowsingState).
|
||||
if (!ReferenceEquals(_filter, Filter))
|
||||
{
|
||||
_filter = Filter;
|
||||
_searchInput = Filter.SearchTerm;
|
||||
if (_firstRendered)
|
||||
await LoadAsync();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,6 +356,7 @@
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
_firstRendered = true;
|
||||
await LoadAsync();
|
||||
if (LiveMode) StartTimer();
|
||||
}
|
||||
@@ -352,11 +367,27 @@
|
||||
_refreshTimer?.Dispose();
|
||||
var interval = Math.Max(5, AutoRefreshSeconds) * 1000.0;
|
||||
_refreshTimer = new System.Timers.Timer(interval) { AutoReset = true };
|
||||
_refreshTimer.Elapsed += async (_, _) =>
|
||||
_refreshTimer.Elapsed += OnRefreshTimerElapsed;
|
||||
_refreshTimer.Start();
|
||||
}
|
||||
|
||||
private async void OnRefreshTimerElapsed(object? sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
// async void here is unavoidable — System.Timers.Timer.Elapsed is an
|
||||
// event with a void-returning signature. We MUST catch every exception
|
||||
// ourselves; an unhandled throw on the threadpool would tear down the
|
||||
// WebView2 process. Skip overlapping ticks so a slow Loader doesn't
|
||||
// stack up cancelled-superseded loads on the UI thread.
|
||||
if (_loading) return;
|
||||
try
|
||||
{
|
||||
await InvokeAsync(LoadAsync);
|
||||
};
|
||||
_refreshTimer.Start();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Swallowed — LoadAsync already handles its own errors; this catch
|
||||
// is the last line of defense for InvokeAsync itself.
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadAsync()
|
||||
|
||||
Reference in New Issue
Block a user