using collAnon.Client.Services; using decePubClient.Services; using Microsoft.AspNetCore.Components; namespace decePubClient.LayerComponents { /// /// Base of every page and app component (b2b.next's WidmannComponentBase). It gives them the cascading state, the /// localizer, navigation and toasts, the loading flag and the disable rule, and the offline helpers: a page that /// finds the app offline registers its fetch with WhenOnline, starts StartOnlinePolling and stops loading; the /// job runs once when the app is back online. /// public class LocalizableComponentBase : ComponentBase, IAsyncDisposable { [CascadingParameter] protected CascadingState CascadingState { get; set; } [Inject] public CoalescingStringLocalizer Localizer { get; set; } [Inject] protected NavigationManager Navigation { get; set; } [Inject] protected ToastService Toast { get; set; } /// Work to run once after the next render, e.g. JS interop that needs the rendered DOM. protected List> AfterRenderAsyncJobs = []; /// True until the component's first load completes; set it to false at the end of OnInitializedAsync. protected bool IsLoading { get; set; } = true; /// What every Disabled= binding starts from: loading, or no connection. protected bool IsDefaultDisabled => IsLoading || CascadingState.IsOffline; readonly List> onOnlineJobs = []; PeriodicTimer pollingTimer; CancellationTokenSource pollingCancellation; bool disposed; protected override async Task OnAfterRenderAsync(bool firstRender) { if (AfterRenderAsyncJobs.Count == 0) return; var jobs = AfterRenderAsyncJobs.ToList(); AfterRenderAsyncJobs.Clear(); foreach (var job in jobs) await job(); } /// Runs the job once, the next time the app is online (needs StartOnlinePolling). protected void WhenOnline(Func job) => onOnlineJobs.Add(job); /// Checks every interval (10 seconds by default) whether the app is back online to run the WhenOnline jobs. protected void StartOnlinePolling(TimeSpan? interval = null) { if (pollingTimer is not null) return; pollingCancellation = new(); pollingTimer = new(interval ?? TimeSpan.FromSeconds(10)); _ = PollOnline(pollingCancellation.Token); } async Task PollOnline(CancellationToken cancellationToken) { try { while (await pollingTimer.WaitForNextTickAsync(cancellationToken)) { if (CascadingState.IsOffline || onOnlineJobs.Count == 0) continue; var jobs = onOnlineJobs.ToList(); onOnlineJobs.Clear(); foreach (var job in jobs) { try { await InvokeAsync(job); } catch (Exception ex) { await CascadingState.ProcessError(ex, $"{GetType().Name}.{nameof(PollOnline)}", cancellationToken); } } await InvokeAsync(StateHasChanged); } } catch (OperationCanceledException) { } } public virtual async ValueTask DisposeAsync() { if (disposed) return; disposed = true; if (pollingCancellation is not null) { await pollingCancellation.CancelAsync(); pollingCancellation.Dispose(); } pollingTimer?.Dispose(); onOnlineJobs.Clear(); GC.SuppressFinalize(this); } } }