PR U-6a: MainLayout wraps @Body in an ErrorBoundary

Add a Blazor built-in ErrorBoundary around @Body with a friendly
FluentMessageBar + retry / home buttons. Also hook
NavigationManager.LocationChanged so the boundary resets on any nav
(otherwise the error state persists across NavigateTo and "Zur
Startseite" still shows the failure banner).

U-6 split into U-6a (this) and U-6b (per-handler try/catch to route
expected exceptions to IMessageService — deferred).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Claudio Schaad 2026-07-04 09:47:14 +02:00
parent 58ca05f83e
commit 562338d0c4
2 changed files with 42 additions and 3 deletions

View file

@ -1,4 +1,6 @@
@inherits LayoutComponentBase
@inject NavigationManager Navigation
@implements IDisposable
<FluentLayout>
<FluentHeader Class="hidePrint">
@ -9,7 +11,24 @@
<FluentBodyContent Class="body-content">
<FluentMessageBarProvider Section="MESSAGES_TOP" @rendermode="InteractiveServer" />
<div class="content">
<ErrorBoundary @ref="errorBoundary">
<ChildContent>
@Body
</ChildContent>
<ErrorContent>
<FluentStack Orientation="Orientation.Vertical" Style="padding: 2rem; max-width: 640px;">
<FluentMessageBar Intent="MessageIntent.Error"
Title="Ein Fehler ist aufgetreten"
AllowDismiss="false">
Diese Seite konnte nicht angezeigt werden. Details stehen im Server-Log.
</FluentMessageBar>
<FluentStack Orientation="Orientation.Horizontal">
<FluentButton Appearance="Appearance.Neutral" OnClick="Recover">Neu versuchen</FluentButton>
<FluentAnchor Href="/" Appearance="Appearance.Neutral">Zur Startseite</FluentAnchor>
</FluentStack>
</FluentStack>
</ErrorContent>
</ErrorBoundary>
</div>
</FluentBodyContent>
</FluentStack>
@ -29,3 +48,22 @@
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
@code {
private ErrorBoundary? errorBoundary;
protected override void OnInitialized()
{
// ErrorBoundary state persists across NavigationManager.NavigateTo, so the
// error page would linger even after the user clicked "Zur Startseite".
// Reset on any location change.
Navigation.LocationChanged += OnLocationChanged;
}
private void OnLocationChanged(object? sender, Microsoft.AspNetCore.Components.Routing.LocationChangedEventArgs e)
=> errorBoundary?.Recover();
private void Recover() => errorBoundary?.Recover();
public void Dispose() => Navigation.LocationChanged -= OnLocationChanged;
}

View file

@ -165,7 +165,8 @@ Split into two PRs on delivery because the header path touches ~15 pages.
### Phase 3 — Loading and error UX consistency
- **U-5** Shared `<LoadingWhen Data="@x">` component with a consistent skeleton visual. Adopt across every page currently using ad-hoc `@if (x is null) { <p>Lädt…</p> return; }`.
- **U-6** Wrap each page (or the layout's `@Body`) in `<ErrorBoundary>` with a friendly retry UI. Route `SaveTransactionAsync` / `DeleteAccountAsync` etc. exceptions to `IMessageService` instead of bubbling.
- **U-6a** Wrap `MainLayout`'s `@Body` in `<ErrorBoundary>` with a friendly retry UI. Reset on any `NavigationManager.LocationChanged` so the error state doesn't stick after the user clicks "Zur Startseite".
- **U-6b** Route expected `SaveTransactionAsync` / `DeleteAccountAsync` / etc. exceptions to `IMessageService` — try/catch in each of the ~15 event handlers with a shared error-formatting helper.
### Phase 4 — Feedback + polish for mutations