150cbcdc29
Critical fixes (review findings #1, #2, #3): - Create IEbookRepository abstraction in Application layer - Remove illegal EF Core dependency from IngestEbookCommandHandler - Create EbookRepository implementation in Infrastructure/Persistence - Create ISyncBroadcaster in Application/Abstractions/Messaging - Create SignalRSyncBroadcaster in Infrastructure/RealTime - Move UpdateReadingProgressCommandHandler from Infrastructure → Application - Add EbookId to GetReaderPageQuery and IEpubReader signature - Rewrite EpubReaderService: DB-resolved file path, remove auto-provisioning - Split EpubService.cs into EpubReaderService.cs + EpubMetadataExtractor.cs - Add CurrentEbookId to IReaderNavigationService and ReaderNavigationService - Update WasmEpubReader and /api/epub endpoint for new signature High severity fixes (#4, #6, #7, #8, #16): - Change BookStorageService registration from Singleton → Scoped - Fix empty catch{} in ReaderCanvas JS interop init — now logs warnings - Replace all Console.WriteLine with ILogger in KnowledgeService + ReaderCanvas - Cache JsonSerializerOptions as static field in KnowledgeService - Wrap SyncService Task.Run body in comprehensive try/catch with ILogger Medium/Low fixes (#11, #13, #14, #15, #18, #20): - BookIngestionModal.DisposeAsync now nullifies _epubBytes (50MB array) - KnowledgeCoordinator.OnGraphUpdated: Action<T> → Func<T, Task> - BookStorageService: Path.Combine → forward-slash string interpolation - SignalR CancellationToken passed as named parameter (not payload arg)
10 KiB
10 KiB
Blazor Routing & Navigation
Route Definition
Routes map URL paths to Blazor components.
Basic Route Definition
@page "/product"
@page "/product/{id}"
<h3>Product: @Id</h3>
@code {
[Parameter]
public string? Id { get; set; }
}
How it works:
@pagedirective makes component routable- Parameter name in URL (
{id}) must match parameter name in@codeblock - Multiple
@pagedirectives supported (same component, multiple routes)
Route Parameters
@page "/product/{id}"
<p>Product: @id</p>
@page "/category/{categoryId}/product/{productId}"
<p>Category: @categoryId, Product: @productId</p>
@code {
[Parameter]
public string? id { get; set; }
[Parameter]
public string? categoryId { get; set; }
[Parameter]
public string? productId { get; set; }
}
Parameter Matching:
- Blazor matches route segments to parameter names (case-insensitive)
{id}in route matchesIdparameter- Extra parameters in URL are ignored
Route Constraints
Route constraints enforce parameter type and format:
@page "/product/{id:int}" <!-- Integer only -->
@page "/order/{orderId:long}" <!-- Long integer -->
@page "/user/{id:guid}" <!-- GUID format -->
@page "/article/{slug:string}" <!-- String (default) -->
@page "/event/{date:datetime}" <!-- DateTime format -->
@page "/price/{amount:decimal}" <!-- Decimal number -->
@page "/flag/{active:bool}" <!-- Boolean -->
@page "/value/{num:double}" <!-- Double/Float -->
@code {
[Parameter]
public int id { get; set; }
[Parameter]
public Guid id { get; set; }
[Parameter]
public bool active { get; set; }
}
Built-in Constraints:
:int- Integer values:long- Long integers:guid- GUID format:bool- Boolean:datetime- DateTime format:decimal- Decimal numbers:double/:float- Floating point:string- Any string (default)
Optional Route Parameters
@page "/search"
@page "/search/{searchTerm}"
<p>Search term: @(searchTerm ?? "All results")</p>
@code {
[Parameter]
public string? searchTerm { get; set; }
}
Catch-All Routes
@page "/{*pageRoute}"
<p>Page not found: @pageRoute</p>
@code {
[Parameter]
public string? pageRoute { get; set; }
}
Navigation
Programmatic Navigation
@inject NavigationManager Navigation
<button @onclick="GoHome">Go Home</button>
<button @onclick="GoToUser">Go to User</button>
@code {
private void GoHome()
{
Navigation.NavigateTo("/");
}
private void GoToUser()
{
Navigation.NavigateTo("/user/123");
}
}
Navigation with Options
// Replace browser history entry instead of adding new one
Navigation.NavigateTo("/home", replace: true);
// Force full page reload from server
Navigation.NavigateTo("/refresh", forceLoad: true);
// Combine options
Navigation.NavigateTo("/new-page", replace: true, forceLoad: true);
When to use forceLoad: true:
- After logout to clear client-side state
- Accessing completely different app
- Clearing service worker cache
- Full server-side initialization needed
NavLink Component
NavLink automatically highlights active route:
<NavLink href="/home" Match="NavLinkMatch.All">
<span class="icon">🏠</span> Home
</NavLink>
<NavLink href="/products" Match="NavLinkMatch.Prefix">
<span class="icon">📦</span> Products
</NavLink>
<NavLink href="/about" Match="NavLinkMatch.None">
About
</NavLink>
@code {
// CSS class applied to active NavLink: active
}
Match options:
NavLinkMatch.All- Exact URL match requiredNavLinkMatch.Prefix- URL starts with href (default)NavLinkMatch.None- Never highlights
CSS:
a.active {
color: white;
background-color: blue;
}
Listen to Location Changes
@implements IDisposable
@inject NavigationManager Navigation
<p>Current location: @Navigation.Uri</p>
@code {
protected override void OnInitialized()
{
Navigation.LocationChanged += LocationChanged;
}
private void LocationChanged(object? sender, LocationChangedEventArgs e)
{
Console.WriteLine($"New location: {e.Location}");
// React to navigation
StateHasChanged();
}
public void Dispose()
{
Navigation.LocationChanged -= LocationChanged;
}
}
Query Strings
Reading Query Parameters
@page "/search"
@inject NavigationManager Navigation
<p>Search results for: @searchQuery</p>
@code {
private string? searchQuery;
protected override void OnInitialized()
{
var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
var query = System.Web.HttpUtility.ParseQueryString(uri.Query);
searchQuery = query["q"];
}
}
Usage: /search?q=blazor → searchQuery = "blazor"
Building Query Strings
private void Search(string term)
{
Navigation.NavigateTo($"/search?q={Uri.EscapeDataString(term)}");
}
// Or use QueryHelpers (in .NET 6+)
var query = new Dictionary<string, string>
{
{ "q", "blazor" },
{ "page", "1" }
};
var url = NavigationManager.GetUriWithQueryParameters("/search", query);
Navigation.NavigateTo(url);
Multiple Query Parameters
var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
var query = System.Web.HttpUtility.ParseQueryString(uri.Query);
var category = query["category"];
var page = int.TryParse(query["page"], out var p) ? p : 1;
var sort = query["sort"] ?? "name";
Usage: /products?category=electronics&page=2&sort=price
Router Configuration
The Router component in App.razor configures routing:
<!-- App.razor -->
<Router AppAssembly="@typeof(App).Assembly"
AdditionalAssemblies="@additionalAssemblies"
OnNavigateAsync="@OnNavigateAsync">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<PageTitle>@pageTitle</PageTitle>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Page not found</p>
</LayoutView>
</NotFound>
</Router>
@code {
private List<Assembly>? additionalAssemblies;
private string pageTitle = "Loading...";
protected override async Task OnInitializedAsync()
{
// Load assemblies dynamically if needed
additionalAssemblies = new List<Assembly>
{
typeof(SomeOtherAssembly).Assembly
};
}
private async Task OnNavigateAsync(NavigationContext context)
{
// Can be used for lazy loading assemblies
// Not commonly needed
}
}
Layouts
Layouts are parent components that wrap pages.
Define a Layout
<!-- Layouts/MainLayout.razor -->
@inherits LayoutComponentBase
<header>@Header</header>
<nav>@Navigation</nav>
<main>@Body</main>
<footer>@Footer</footer>
@code {
[Parameter]
public RenderFragment? Header { get; set; }
[Parameter]
public RenderFragment? Navigation { get; set; }
[Parameter]
public RenderFragment? Body { get; set; }
[Parameter]
public RenderFragment? Footer { get; set; }
}
Apply Layout to Page
@page "/products"
@layout MainLayout
<h2>Products</h2>
Apply Layout to Multiple Pages
<!-- _Imports.razor -->
@layout MainLayout
Add this line to _Imports.razor to apply layout to all components in folder and below.
Nested Layouts
<!-- AdminLayout inherits from MainLayout -->
@inherits MainLayout
<aside>Admin sidebar</aside>
@Body
Page Titles
Update page title (browser tab) dynamically:
@page "/products/{id}"
@inject NavigationManager Navigation
<PageTitle>@title</PageTitle>
<h1>@title</h1>
@code {
[Parameter]
public string? id { get; set; }
private string? title;
protected override async Task OnParametersSetAsync()
{
title = await LoadProductTitleAsync(id);
}
private async Task<string> LoadProductTitleAsync(string? id)
{
// Load from service
return $"Product {id}";
}
}
Common Routing Patterns
Master-Detail Pattern
@page "/products"
@page "/products/{id}"
<div style="display: grid; grid-template-columns: 1fr 1fr;">
<ProductList OnSelectProduct="@SelectProduct" />
@if (selectedId != null)
{
<ProductDetail Id="@selectedId" />
}
</div>
@code {
[Parameter]
public string? id { get; set; }
private string? selectedId;
protected override void OnParametersSet()
{
selectedId = id;
}
private void SelectProduct(string productId)
{
Navigation.NavigateTo($"/products/{productId}");
}
}
Breadcrumb Navigation
@page "/category/{categoryId}/product/{productId}"
<div class="breadcrumb">
<a href="/">Home</a> /
<a href="/category/@categoryId">@categoryName</a> /
<span>@productName</span>
</div>
@code {
[Parameter]
public string? categoryId { get; set; }
[Parameter]
public string? productId { get; set; }
private string? categoryName;
private string? productName;
protected override async Task OnParametersSetAsync()
{
categoryName = await LoadCategoryAsync(categoryId);
productName = await LoadProductAsync(productId);
}
}
Tab-Based Navigation
@page "/settings"
<div class="tabs">
<NavLink href="/settings/profile" Match="NavLinkMatch.All">Profile</NavLink>
<NavLink href="/settings/security" Match="NavLinkMatch.All">Security</NavLink>
<NavLink href="/settings/notifications" Match="NavLinkMatch.All">Notifications</NavLink>
</div>
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(SettingsLayout)" />
</Found>
</Router>
Related Resources: See components-lifecycle.md for parameter handling. See authentication-authorization.md for route authorization.