Files
Nexus.Reader/.agent/skills/blazor-expert/resources/routing-navigation.md
T
mjasin 150cbcdc29 refactor(arch): introduce IEbookRepository, ISyncBroadcaster, fix EpubReader path resolution
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)
2026-05-12 21:21:30 +02:00

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:

  • @page directive makes component routable
  • Parameter name in URL ({id}) must match parameter name in @code block
  • Multiple @page directives 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 matches Id parameter
  • 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 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 required
  • NavLinkMatch.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=blazorsearchQuery = "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.