feat: Ingestion Pipeline Stabilization and WASM Service Proxies (#42)

This PR stabilizes the Nexus Ingestion Engine by implementing functional service proxies for the Blazor WASM client and refining the backend infrastructure for real-time progress tracking and database compatibility.

### Key Changes
- **Infrastructure Stabilization**:
  - Implemented production-grade `EbookRepository` with PostgreSQL `EF.Functions.ILike` support.
  - Enforced `IsReadyForReading = false` state for newly added ebooks (resolves #35).
  - Updated `SignalRSyncBroadcaster` to support targeted user messaging and ingestion-specific progress updates (resolves #37).
- **WASM Client Functional Proxies**:
  - Replaced "Throwing" dummy services with `WasmEbookRepository`, `WasmSyncBroadcaster`, `WasmBookStorageService`, and `WasmEmbeddingGenerator`.
  - These services proxy requests to the backend via a new set of Minimal API endpoints in `NexusReader.Web`.
- **Domain Refinement**:
  - Added `IsReadyForReading` flag to the `Ebook` entity to manage background AI processing states.

### Related Issues
- Fixes #35
- Fixes #36
- Fixes #37

---------

Co-authored-by: Marek Jasiński <jasins.marek@gmail.com>
Reviewed-on: #42
Co-authored-by: Antigravity <antigravity@google.com>
Co-committed-by: Antigravity <antigravity@google.com>
This commit was merged in pull request #42.
This commit is contained in:
2026-05-13 18:24:24 +00:00
committed by Marek Jaisński
parent d5c2952bec
commit 5a2223a4c8
39 changed files with 6134 additions and 301 deletions
@@ -0,0 +1,533 @@
# Blazor Authentication & Authorization
## Authentication Setup
### Blazor Server Setup
```csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Add authentication
builder.Services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/login";
options.LogoutPath = "/logout";
options.AccessDeniedPath = "/unauthorized";
});
builder.Services.AddAuthorizationCore();
// Add Blazor Server
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
var app = builder.Build();
// Add authentication middleware BEFORE MapRazorPages
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapRazorPages();
app.MapBlazorHub();
```
### Blazor WebAssembly Setup
```csharp
// Program.cs
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
// Add authentication
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
builder.Services.AddScoped<HttpClient>(sp =>
new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
// CustomAuthStateProvider
public class CustomAuthStateProvider : AuthenticationStateProvider
{
private readonly HttpClient httpClient;
public CustomAuthStateProvider(HttpClient httpClient)
{
this.httpClient = httpClient;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
try
{
var user = await httpClient.GetJsonAsync<UserInfo>("/api/user");
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Name, user.Name),
new Claim(ClaimTypes.Email, user.Email)
};
var identity = new ClaimsIdentity(claims, "Custom");
return new AuthenticationState(new ClaimsPrincipal(identity));
}
catch
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
}
}
```
## AuthorizeView Component
AuthorizeView displays content conditionally based on authorization status.
### Basic Authorization Check
```html
<AuthorizeView>
<Authorized>
<p>Hello, @context.User.Identity?.Name!</p>
</Authorized>
<NotAuthorized>
<p>Please log in.</p>
</NotAuthorized>
</AuthorizeView>
```
### Authorize by Role
```html
<AuthorizeView Roles="Admin">
<p>This content is only for Admins</p>
</AuthorizeView>
<AuthorizeView Roles="User, Moderator">
<p>User or Moderator content</p>
</AuthorizeView>
```
### Authorize by Policy
```html
<AuthorizeView Policy="ContentEditor">
<p>Only content editors can see this</p>
</AuthorizeView>
```
### Multiple AuthorizeView States
```html
<AuthorizeView>
<Authorized>
@if (context.User.IsInRole("Admin"))
{
<p>Admin dashboard</p>
}
else if (context.User.IsInRole("Editor"))
{
<p>Editor dashboard</p>
}
else
{
<p>User dashboard</p>
}
</Authorized>
<Authorizing>
<p>Checking authorization...</p>
</Authorizing>
<NotAuthorized>
<p>Not authorized</p>
</NotAuthorized>
</AuthorizeView>
```
### Authorize Multiple Resources
```html
<AuthorizeView Context="Auth">
<Authorized>
<div>
<h2>@Auth.User.Identity?.Name</h2>
@if (Auth.User.IsInRole("Admin"))
{
<a href="/admin">Admin Panel</a>
}
@if (Auth.User.HasClaim("department", "engineering"))
{
<a href="/engineering">Engineering</a>
}
</div>
</Authorized>
</AuthorizeView>
```
## Authorize Attribute
Apply `[Authorize]` to pages to require authentication.
### Basic Page Authorization
```csharp
@page "/admin"
@attribute [Authorize]
<h2>Admin Page</h2>
<p>Only authenticated users can see this.</p>
```
### Role-Based Authorization
```csharp
@page "/admin"
@attribute [Authorize(Roles = "Admin")]
<h2>Admin Panel</h2>
<p>Only admins can access this page.</p>
```
### Policy-Based Authorization
```csharp
@page "/dashboard"
@attribute [Authorize(Policy = "RequireAdminRole")]
<h2>Dashboard</h2>
```
### Multiple Requirements
```csharp
@page "/admin"
@attribute [Authorize(Roles = "Admin, Manager")]
@attribute [Authorize(Policy = "ActiveSubscription")]
<h2>Admin Dashboard</h2>
```
## Authorization Policies
Define fine-grained authorization policies.
### Setup Policies
```csharp
// Program.cs
builder.Services.AddAuthorizationCore(options =>
{
options.AddPolicy("RequireAdminRole", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("ActiveSubscription", policy =>
policy.Requirements.Add(new ActiveSubscriptionRequirement()));
options.AddPolicy("ContentEditor", policy =>
policy.RequireClaim("department", "engineering", "content"));
options.AddPolicy("AdultUser", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(18)));
});
// Add custom policy handler
builder.Services.AddSingleton<IAuthorizationHandler, ActiveSubscriptionHandler>();
```
### Custom Policy Handlers
```csharp
public class ActiveSubscriptionRequirement : IAuthorizationRequirement { }
public class ActiveSubscriptionHandler : AuthorizationHandler<ActiveSubscriptionRequirement>
{
private readonly IUserService userService;
public ActiveSubscriptionHandler(IUserService userService)
{
this.userService = userService;
}
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ActiveSubscriptionRequirement requirement)
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
context.Fail();
return;
}
var user = await userService.GetUserAsync(userId);
if (user?.SubscriptionActive == true)
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
}
public class MinimumAgeRequirement : IAuthorizationRequirement
{
public int MinimumAge { get; set; }
public MinimumAgeRequirement(int minimumAge)
{
MinimumAge = minimumAge;
}
}
```
## Accessing Authentication State
### In Components
```csharp
@page "/user-profile"
@if (authState == null)
{
<p>Loading...</p>
}
else if (authState.User.Identity?.IsAuthenticated == true)
{
<h2>Welcome, @authState.User.Identity?.Name</h2>
}
else
{
<p>Not authenticated</p>
}
@code {
[CascadingParameter]
private Task<AuthenticationState>? AuthStateTask { get; set; }
private AuthenticationState? authState;
protected override async Task OnInitializedAsync()
{
authState = await AuthStateTask!;
}
}
```
### Check Claims and Roles
```csharp
@code {
private async Task CheckUserAsync()
{
var authState = await AuthStateTask!;
var user = authState.User;
if (user.Identity?.IsAuthenticated == true)
{
var name = user.Identity.Name;
var email = user.FindFirst(ClaimTypes.Email)?.Value;
var isAdmin = user.IsInRole("Admin");
var department = user.FindFirst("department")?.Value;
}
}
}
```
## Login/Logout Implementation
### Login Page
```csharp
@page "/login"
@layout BlankLayout
<div class="login-form">
<h2>Login</h2>
@if (!string.IsNullOrEmpty(errorMessage))
{
<div class="alert alert-danger">@errorMessage</div>
}
<EditForm Model="@model" OnValidSubmit="@HandleLoginAsync">
<DataAnnotationsValidator />
<div class="form-group">
<label>Email:</label>
<InputText @bind-Value="model.Email" class="form-control" />
</div>
<div class="form-group">
<label>Password:</label>
<InputText @bind-Value="model.Password" type="password" class="form-control" />
</div>
<button type="submit" class="btn btn-primary">Login</button>
</EditForm>
</div>
@code {
[SupplyParameterFromQuery]
public string? ReturnUrl { get; set; }
@inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager Navigation
private LoginModel model = new();
private string? errorMessage;
private async Task HandleLoginAsync()
{
try
{
var result = await AuthService.LoginAsync(model.Email, model.Password);
// Update authentication state
if (AuthStateProvider is CustomAuthStateProvider customAuth)
{
await customAuth.SetUserAsync(result.User);
}
// Redirect to return URL or home
var url = !string.IsNullOrEmpty(ReturnUrl) ? ReturnUrl : "/";
Navigation.NavigateTo(url, forceLoad: true);
}
catch (Exception ex)
{
errorMessage = ex.Message;
}
}
}
public class LoginModel
{
[Required]
[EmailAddress]
public string Email { get; set; } = "";
[Required]
public string Password { get; set; } = "";
}
```
### Logout Endpoint
```csharp
// Pages/Logout.cshtml (in Blazor Server)
@page "/logout"
@using Microsoft.AspNetCore.Identity
@inject SignInManager<IdentityUser> SignInManager
@inject NavigationManager Navigation
@code {
protected override async Task OnInitializedAsync()
{
await SignInManager.SignOutAsync();
Navigation.NavigateTo("/");
}
}
```
## Claims-Based Authorization
Working with claims for fine-grained authorization.
### Add Claims to User
```csharp
var claims = new List<Claim>
{
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Name, user.Name),
new Claim(ClaimTypes.Email, user.Email),
new Claim("department", "engineering"),
new Claim("level", "senior")
};
var identity = new ClaimsIdentity(claims, "Custom");
var principal = new ClaimsPrincipal(identity);
```
### Check Claims in Component
```csharp
@code {
private async Task CheckDepartmentAsync()
{
var authState = await AuthStateTask!;
var user = authState.User;
var department = user.FindFirst("department")?.Value;
var level = user.FindFirst("level")?.Value;
if (department == "engineering")
{
// Show engineering-specific UI
}
}
}
```
## Best Practices
### Use Cascading AuthenticationState
```csharp
// App.razor - already cascades AuthenticationState by default
<CascadingAuthenticationState>
<Router ... />
</CascadingAuthenticationState>
```
### Always Check firstRender in OnAfterRender
```csharp
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Initialize only once
authState = await AuthStateTask!;
StateHasChanged();
}
}
```
### Use forceLoad for Logout
```csharp
private async Task LogoutAsync()
{
await AuthService.LogoutAsync();
// forceLoad clears client-side state
Navigation.NavigateTo("/", forceLoad: true);
}
```
### Validate on Server
- Never trust client-side authorization
- Always validate authorization on backend API
- Check claims/roles on server methods
### Use ReturnUrl After Login
```csharp
// Redirect back to originally-requested page
Navigation.NavigateTo($"/login?returnUrl={Uri.EscapeDataString(Navigation.Uri)}");
```
---
**Related Resources:** See [routing-navigation.md](routing-navigation.md) for route-based authorization. See [components-lifecycle.md](components-lifecycle.md) for parameter security.
@@ -0,0 +1,550 @@
# Blazor Components & Component Lifecycle
## Component Structure
Components are the fundamental building blocks of Blazor applications. A Blazor component is a self-contained piece of UI with an optional logic.
### Basic Component Syntax
```csharp
@page "/example"
@using MyApp.Services
@inject IMyService MyService
<h3>@Title</h3>
<div>@ChildContent</div>
<button @onclick="HandleClick">Click me</button>
@code {
[Parameter]
public string Title { get; set; } = "Default";
[Parameter]
public RenderFragment? ChildContent { get; set; }
private void HandleClick()
{
// Handle button click
}
}
```
Key elements:
- **`@page` directive**: Makes component routable (optional for non-page components)
- **`@using`**: Import namespaces
- **`@inject`**: Dependency injection
- **HTML markup**: Regular HTML with Blazor directives
- **`@code` block**: C# logic including lifecycle methods
### Component vs Page
- **Page Component**: Has `@page` directive, routable via URL
- Example: `/Counter` route
- Located in `Pages/` folder (convention)
- **Reusable Component**: No `@page` directive, used by other components
- Example: `<UserCard @bind-User="user" />`
- Located in `Shared/` or domain-specific folder
## Component Lifecycle
### Lifecycle Sequence
Component lifecycle methods execute in this order:
```
1. SetParametersAsync()
2. OnInitialized() or OnInitializedAsync()
3. OnParametersSet() or OnParametersSetAsync()
4. ShouldRender() [decision point - skip if returns false]
5. OnAfterRender() or OnAfterRenderAsync()
```
When parameters change (parent re-renders):
```
SetParametersAsync() [parameters updated]
OnParametersSet() [NOT OnInitialized - that runs once only]
ShouldRender()
OnAfterRender()
```
### Lifecycle Methods Detailed
#### SetParametersAsync()
- **When**: First method called, before initialization
- **Purpose**: Set component parameters
- **Usage**: Rarely overridden, use OnInitialized instead
- **Code Example**:
```csharp
public override async Task SetParametersAsync(ParameterView parameters)
{
// Custom parameter processing if needed
await base.SetParametersAsync(parameters);
}
```
#### OnInitialized / OnInitializedAsync()
- **When**: Once per component lifetime, after parameters set
- **Purpose**: Initialize component state, load data
- **Runs**: Only ONCE, even if parameters change
- **Code Example**:
```csharp
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
data = await Service.LoadDataAsync();
}
```
**Common Uses:**
- Load initial data from API
- Set up subscriptions
- Initialize state based on parameters
#### OnParametersSet / OnParametersSetAsync()
- **When**: After parameters set, runs EVERY time parameters change
- **Purpose**: React to parameter changes
- **Runs**: Every time parent re-renders with different values
- **Code Example**:
```csharp
protected override async Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
if (UserId != previousUserId)
{
data = await Service.LoadUserDataAsync(UserId);
previousUserId = UserId;
}
}
```
**Common Uses:**
- Update UI based on new parameter values
- Fetch new data when ID parameter changes
- React to cascading parameter changes
#### ShouldRender()
- **When**: Before DOM rendering, decision point
- **Purpose**: Optimize rendering by skipping unnecessary renders
- **Returns**: true (render) or false (skip)
- **Code Example**:
```csharp
protected override bool ShouldRender()
{
// Only render if specific field changed
return hasChanged;
}
```
**Common Optimizations:**
- Skip render if data unchanged
- Prevent re-render from external events
- Implement custom change detection
#### OnAfterRender / OnAfterRenderAsync()
- **When**: After component rendered to DOM
- **Purpose**: Work with DOM, initialize JS libraries, final setup
- **Parameter**: `firstRender` - true only on first render
- **Code Example**:
```csharp
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Initialize JS library only once
await JS.InvokeVoidAsync("initializeChart", elementRef);
}
}
```
**Critical Use Case:**
```csharp
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
// Load JS module
module = await JS.InvokeAsync<IJSObjectReference>(
"import", "./scripts/myScript.js");
// Initialize library
await module.InvokeVoidAsync("setupChart", element);
}
}
```
**Important:** Always use `firstRender` check for one-time initialization. This prevents re-initializing on every parameter change.
## Component Parameters
### Parameter Declaration
```csharp
@code {
// Simple parameter
[Parameter]
public string Title { get; set; } = "Default";
// Required parameter (C# 11+)
[Parameter, EditorRequired]
public int UserId { get; set; }
// Child content
[Parameter]
public RenderFragment? ChildContent { get; set; }
// Event callback
[Parameter]
public EventCallback<string> OnValueChanged { get; set; }
// Cascading parameter
[CascadingParameter]
public ThemeInfo? CurrentTheme { get; set; }
}
```
### Parameter Best Practices
**Use Clear Names:**
```csharp
// ✅ Good - clear intent
[Parameter]
public bool IsVisible { get; set; }
// ❌ Poor - ambiguous
[Parameter]
public bool State { get; set; }
```
**Use Nullable Types for Optional:**
```csharp
// ✅ Good - nullable indicates optional
[Parameter]
public string? OptionalValue { get; set; }
// ✅ Good - default value
[Parameter]
public int MaxItems { get; set; } = 10;
// ❌ Poor - not clear if optional
[Parameter]
public string RequiredValue { get; set; }
```
**Use [EditorRequired] for Required Parameters (C# 11+):**
```csharp
// ✅ Best practice - compiler enforces, IDE warns
[Parameter, EditorRequired]
public string Title { get; set; } = default!;
// Fallback for older C#
[Parameter]
public string Title { get; set; } = default!;
```
**Use EventCallback for Child-to-Parent Communication:**
```csharp
// ✅ Correct - EventCallback for async safety
[Parameter]
public EventCallback<string> OnValueChanged { get; set; }
// ✅ With custom args
[Parameter]
public EventCallback<ValueChangeEventArgs> OnValueChanged { get; set; }
// ❌ Avoid - direct Action, not async-safe
[Parameter]
public Action<string>? OnValueChanged { get; set; }
```
### Parameter Change Detection
To know when a parameter changed:
```csharp
@code {
private int previousUserId;
[Parameter]
public int UserId { get; set; }
protected override async Task OnParametersSetAsync()
{
if (UserId != previousUserId)
{
previousUserId = UserId;
await LoadUserData();
}
}
}
```
Or use a comparison strategy:
```csharp
private object? previousCriteria;
protected override async Task OnParametersSetAsync()
{
var currentCriteria = (SearchId, SearchTerm);
if (!Equals(previousCriteria, currentCriteria))
{
previousCriteria = currentCriteria;
await PerformSearch();
}
}
```
## Cascading Values
Cascading values allow ancestor components to provide data to all descendants without explicit parameter passing.
### Providing Cascading Values
```csharp
<!-- Parent component -->
<CascadingValue Value="@currentUser">
@ChildContent
</CascadingValue>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
private User currentUser = new();
}
```
### Receiving Cascading Values
```csharp
<!-- Child component anywhere in hierarchy -->
@code {
[CascadingParameter]
public User? CurrentUser { get; set; }
protected override void OnInitialized()
{
if (CurrentUser == null)
{
// Handle missing cascading value
}
}
}
```
### Multiple Cascading Values
```csharp
<!-- Provider -->
<CascadingValue Value="@theme">
<CascadingValue Value="@currentUser">
<CascadingValue Value="@permissions">
@ChildContent
</CascadingValue>
</CascadingValue>
</CascadingValue>
<!-- Consumer - multiple parameters -->
@code {
[CascadingParameter]
public Theme? Theme { get; set; }
[CascadingParameter]
public User? CurrentUser { get; set; }
[CascadingParameter]
public Permissions? Permissions { get; set; }
}
```
### Named Cascading Values
For disambiguation when multiple values of same type:
```csharp
<!-- Provider -->
<CascadingValue Value="@themeLight" Name="Light">
<CascadingValue Value="@themeDark" Name="Dark">
@ChildContent
</CascadingValue>
</CascadingValue>
<!-- Consumer -->
@code {
[CascadingParameter(Name = "Light")]
public Theme? LightTheme { get; set; }
[CascadingParameter(Name = "Dark")]
public Theme? DarkTheme { get; set; }
}
```
## RenderFragment for Component Composition
RenderFragment enables flexible component composition.
### Basic RenderFragment
```csharp
<!-- Parent component -->
<div>
<h2>Header</h2>
@ChildContent
<footer>Footer</footer>
</div>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
}
<!-- Usage -->
<Layout>
<p>This is the main content</p>
</Layout>
```
### Typed RenderFragment with Context
```csharp
<!-- ListComponent.razor -->
@foreach (var item in Items)
{
@ItemTemplate(item)
}
@code {
[Parameter]
public IEnumerable<Item> Items { get; set; } = [];
[Parameter]
public RenderFragment<Item>? ItemTemplate { get; set; }
}
<!-- Usage -->
<ListComponent Items="@items">
<ItemTemplate Context="item">
<div>@item.Name - @item.Price</div>
</ItemTemplate>
</ListComponent>
```
### Multiple Named Content Areas
```csharp
<!-- Card component with multiple slots -->
<div class="card">
<div class="card-header">@Header</div>
<div class="card-body">@Body</div>
<div class="card-footer">@Footer</div>
</div>
@code {
[Parameter]
public RenderFragment? Header { get; set; }
[Parameter]
public RenderFragment? Body { get; set; }
[Parameter]
public RenderFragment? Footer { get; set; }
}
<!-- Usage -->
<Card>
<Header>
<h3>Card Title</h3>
</Header>
<Body>
<p>Card content</p>
</Body>
<Footer>
<button>Action</button>
</Footer>
</Card>
```
## Component Best Practices
### Single Responsibility
- Each component should have one clear purpose
- Avoid god components that do too much
- Example: `UserProfile` component should focus on displaying user info, not handle complex business logic
### Composition Over Inheritance
- Use cascading values for shared state, not deep hierarchies
- Compose components rather than creating base classes
- Example: Create theme provider component instead of theme-aware base class
### Keep Components Simple
- Minimize `@code` block logic
- Move complex logic to services
- Example: Validation logic → ValidationService, not in component
### Proper Disposal
- Implement `IDisposable` or `IAsyncDisposable`
- Unsubscribe from events
- Dispose timers and resources
```csharp
@implements IAsyncDisposable
@inject IJSRuntime JS
private IJSObjectReference? module;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
module = await JS.InvokeAsync<IJSObjectReference>(
"import", "./myScript.js");
}
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
if (module is not null)
{
await module.DisposeAsync();
}
}
```
---
**Related Resources:** See [state-management-events.md](state-management-events.md) for event handling and state updates. See [performance-advanced.md](performance-advanced.md) for optimization techniques.
@@ -0,0 +1,589 @@
# Blazor Forms & Validation
## EditForm Component
EditForm provides a complete form handling solution with data binding and validation.
### Basic EditForm
```csharp
@page "/register"
<EditForm Model="@model" OnValidSubmit="@HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="form-group">
<label>Name:</label>
<InputText @bind-Value="model.Name" class="form-control" />
<ValidationMessage For="@(() => model.Name)" />
</div>
<div class="form-group">
<label>Email:</label>
<InputText @bind-Value="model.Email" class="form-control" />
<ValidationMessage For="@(() => model.Email)" />
</div>
<button type="submit" class="btn">Register</button>
</EditForm>
@code {
private RegistrationModel model = new();
private async Task HandleValidSubmit()
{
// Form is valid, process data
await Service.RegisterUserAsync(model);
}
}
public class RegistrationModel
{
[Required]
[StringLength(100, MinimumLength = 2)]
public string Name { get; set; } = "";
[Required]
[EmailAddress]
public string Email { get; set; } = "";
[Required]
[MinLength(8)]
public string Password { get; set; } = "";
}
```
### EditForm Events
```csharp
<EditForm Model="@model"
OnValidSubmit="@OnValidSubmit"
OnInvalidSubmit="@OnInvalidSubmit"
OnSubmit="@OnSubmit">
<!-- Form content -->
</EditForm>
@code {
private async Task OnValidSubmit()
{
// Fires when form is valid and submitted
}
private async Task OnInvalidSubmit()
{
// Fires when form is invalid and submitted
}
private async Task OnSubmit()
{
// Fires for any submit (valid or invalid)
// Useful for custom validation logic
}
}
```
### Form State Control
```csharp
@inject EditFormService FormService
<EditForm Model="@model" @ref="form">
<!-- Form content -->
</EditForm>
<button @onclick="Submit">Submit</button>
<button @onclick="Reset">Reset</button>
<button @onclick="CheckValid">Is Valid?</button>
@code {
private EditForm? form;
private UserModel model = new();
private async Task Submit()
{
// Manually trigger validation and submission
await form!.RequestValidationAsync();
// Check if valid
if (form!.EditContext.IsModified() && form!.EditContext.Validate())
{
// Process form
}
}
private void Reset()
{
// Reset all fields to default
form!.EditContext.ResetEditingItemAsync();
}
private void CheckValid()
{
bool isValid = form!.EditContext.Validate();
Console.WriteLine($"Form valid: {isValid}");
}
}
```
## Input Components
### Text Input
```csharp
<InputText @bind-Value="model.Name" class="form-control" />
<InputTextArea @bind-Value="model.Description" rows="4" />
@code {
private UserModel model = new();
}
```
### Numeric Input
```csharp
<InputNumber @bind-Value="model.Age" class="form-control" />
<InputNumber @bind-Value="model.Price" @bind-Value:format="N2" />
@code {
private int age;
private decimal price;
}
```
**Format specifiers:**
- `N2` - Number with 2 decimal places
- `C` - Currency
- `P` - Percentage
- `D` - Date
- `X` - Hexadecimal
### Date Input
```csharp
<InputDate @bind-Value="model.BirthDate" />
<InputDate @bind-Value="model.StartTime" Type="InputDateType.DateTimeLocal" />
@code {
private DateTime birthDate;
private DateTime startTime;
}
```
**Types:**
- `InputDateType.Date` - Date only (default)
- `InputDateType.DateTimeLocal` - Date and time
- `InputDateType.Month` - Month and year
- `InputDateType.Time` - Time only
### Select/Dropdown
```csharp
<InputSelect @bind-Value="model.Category" class="form-control">
<option value="">Select a category...</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</InputSelect>
<!-- Dynamic options from data -->
<InputSelect @bind-Value="model.CategoryId">
<option value="">Select...</option>
@foreach (var cat in categories)
{
<option value="@cat.Id">@cat.Name</option>
}
</InputSelect>
@code {
private string selectedCategory = "";
private List<Category> categories = [];
}
```
### Checkbox
```csharp
<InputCheckbox @bind-Value="model.AgreeToTerms" />
Accept terms of service?
@code {
private bool agreeToTerms = false;
}
```
### Radio Buttons
```csharp
<InputRadioGroup @bind-Value="model.Preference">
<div>
<InputRadio Value="@("option1")" />
<label>Option 1</label>
</div>
<div>
<InputRadio Value="@("option2")" />
<label>Option 2</label>
</div>
</InputRadioGroup>
@code {
private string preference = "option1";
}
```
### File Upload
```csharp
<InputFile OnChange="@HandleFileSelect" />
@code {
private async Task HandleFileSelect(InputFileChangeEventArgs e)
{
var file = e.File;
using var stream = file.OpenReadStream();
var buffer = new byte[stream.Length];
await stream.ReadAsync(buffer);
// Process file
}
}
```
## Validation
### DataAnnotations Validation
```csharp
public class UserModel
{
[Required(ErrorMessage = "Name is required")]
[StringLength(100, MinimumLength = 2)]
public string Name { get; set; } = "";
[Required]
[EmailAddress(ErrorMessage = "Invalid email format")]
public string Email { get; set; } = "";
[Range(18, 120, ErrorMessage = "Age must be 18-120")]
public int Age { get; set; }
[Url]
public string? Website { get; set; }
[Phone]
public string? PhoneNumber { get; set; }
[CreditCard]
public string? CardNumber { get; set; }
}
```
**Common Validators:**
- `[Required]` - Field must have value
- `[StringLength(max)]` - Max length
- `[StringLength(max, MinimumLength = min)]` - Min and max
- `[EmailAddress]` - Valid email format
- `[Range(min, max)]` - Numeric range
- `[Url]` - Valid URL format
- `[Phone]` - Valid phone format
- `[CreditCard]` - Valid credit card format
- `[RegularExpression(pattern)]` - Regex match
### ValidationSummary
Shows all validation errors for the form:
```csharp
<EditForm Model="@model" OnValidSubmit="@HandleSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<InputText @bind-Value="model.Name" />
<InputText @bind-Value="model.Email" />
<button type="submit">Submit</button>
</EditForm>
```
Displays as:
```
- Name is required
- Email is required
```
### ValidationMessage
Shows validation error for specific field:
```csharp
<InputText @bind-Value="model.Name" />
<ValidationMessage For="@(() => model.Name)" />
<!-- Custom CSS class -->
<ValidationMessage For="@(() => model.Email)" class="text-danger" />
```
### Custom Validation
Implement `IValidatableObject` for complex validation rules:
```csharp
public class UserModel : IValidatableObject
{
public string Email { get; set; } = "";
public string ConfirmEmail { get; set; } = "";
[Range(18, 100)]
public int Age { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// Compare email fields
if (Email != ConfirmEmail)
{
yield return new ValidationResult(
"Email addresses must match",
new[] { nameof(ConfirmEmail) }
);
}
// Custom age validation
if (Age > 0 && Age < 18 && HasRestrictedContent)
{
yield return new ValidationResult(
"Users under 18 cannot access this content",
new[] { nameof(Age) }
);
}
}
public bool HasRestrictedContent { get; set; }
}
```
### Custom Validators
Create reusable custom validators:
```csharp
public class MinimumAgeAttribute : ValidationAttribute
{
private readonly int _minimumAge;
public MinimumAgeAttribute(int minimumAge)
{
_minimumAge = minimumAge;
}
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
if (value is DateTime birthDate)
{
var age = DateTime.Today.Year - birthDate.Year;
if (birthDate.Date > DateTime.Today.AddYears(-age)) age--;
if (age < _minimumAge)
{
return new ValidationResult($"Minimum age is {_minimumAge}");
}
}
return ValidationResult.Success;
}
}
// Usage
public class UserModel
{
[MinimumAge(18)]
public DateTime BirthDate { get; set; }
}
```
### Async Validation
```csharp
public class UniqueEmailAttribute : ValidationAttribute
{
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
// Can't use async in ValidationAttribute
// Use EditContext instead (see below)
return ValidationResult.Success;
}
}
// Better approach: Manual validation in component
@code {
private async Task HandleValidSubmit()
{
// Check email availability before submit
bool isUnique = await Service.IsEmailUniqueAsync(model.Email);
if (!isUnique)
{
form!.EditContext.AddValidationMessages(
FieldIdentifier.Create(() => model.Email),
new[] { "Email is already registered" }
);
return;
}
await SaveUserAsync(model);
}
}
```
## Form Patterns
### Loading State
```csharp
@if (isSubmitting)
{
<p>Saving...</p>
}
else
{
<EditForm Model="@model" OnValidSubmit="@SubmitAsync">
<DataAnnotationsValidator />
<ValidationSummary />
<InputText @bind-Value="model.Name" />
<button type="submit" disabled="@isSubmitting">
@(isSubmitting ? "Saving..." : "Submit")
</button>
</EditForm>
}
@code {
private bool isSubmitting;
private async Task SubmitAsync()
{
isSubmitting = true;
try
{
await Service.SaveAsync(model);
}
finally
{
isSubmitting = false;
}
}
}
```
### Error Handling
```csharp
<EditForm Model="@model" OnValidSubmit="@SubmitAsync">
<DataAnnotationsValidator />
@if (!string.IsNullOrEmpty(errorMessage))
{
<div class="alert alert-danger">@errorMessage</div>
}
<ValidationSummary />
<InputText @bind-Value="model.Name" />
<button type="submit">Submit</button>
</EditForm>
@code {
private string? errorMessage;
private async Task SubmitAsync()
{
try
{
errorMessage = null;
await Service.SaveAsync(model);
}
catch (Exception ex)
{
errorMessage = $"Error: {ex.Message}";
}
}
}
```
### Multi-Step Form
```csharp
@page "/wizard"
@if (currentStep == 1)
{
<h2>Step 1: Basic Info</h2>
<InputText @bind-Value="model.Name" />
<button @onclick="NextStep">Next</button>
}
else if (currentStep == 2)
{
<h2>Step 2: Contact Info</h2>
<InputText @bind-Value="model.Email" />
<button @onclick="PreviousStep">Back</button>
<button @onclick="NextStep">Next</button>
}
else if (currentStep == 3)
{
<h2>Step 3: Confirm</h2>
<p>Name: @model.Name</p>
<p>Email: @model.Email</p>
<button @onclick="PreviousStep">Back</button>
<button @onclick="SubmitAsync">Submit</button>
}
@code {
private int currentStep = 1;
private UserModel model = new();
private void NextStep() => currentStep++;
private void PreviousStep() => currentStep--;
private async Task SubmitAsync()
{
await Service.RegisterAsync(model);
}
}
```
### Real-Time Field Validation
```csharp
<input @bind="email" @bind:event="oninput" @onblur="ValidateEmail" />
@if (!string.IsNullOrEmpty(emailError))
{
<span class="error">@emailError</span>
}
@code {
private string email = "";
private string? emailError;
private void ValidateEmail()
{
if (string.IsNullOrEmpty(email))
{
emailError = "Email is required";
}
else if (!email.Contains("@"))
{
emailError = "Invalid email format";
}
else
{
emailError = null;
}
}
}
```
---
**Related Resources:** See [state-management-events.md](state-management-events.md) for data binding patterns. See [authentication-authorization.md](authentication-authorization.md) for role-based form customization.
@@ -0,0 +1,561 @@
# Blazor Performance & Advanced Patterns
## Rendering Optimization
### ShouldRender Override
Control when components re-render to prevent unnecessary rendering cycles.
```csharp
@page "/optimized"
<button @onclick="IncrementCount">Clicked @count times</button>
<ChildComponent Value="@value" />
@code {
private int count = 0;
private string value = "test";
protected override bool ShouldRender()
{
// Only render if value changed, not if count changed
// This component doesn't display count directly
return false; // Skip render
}
private void IncrementCount()
{
count++;
// Component won't re-render, child component won't re-render either
}
}
```
### Tracking Changed Fields
```csharp
@page "/tracker"
<button @onclick="UpdateName">Update Name</button>
<button @onclick="UpdateAge">Update Age</button>
<p>Name: @name</p>
<p>Age: @age</p>
@code {
private string? name;
private int age;
private bool nameChanged = false;
private bool ageChanged = false;
protected override bool ShouldRender()
{
if (!nameChanged && !ageChanged)
{
return false;
}
nameChanged = false;
ageChanged = false;
return true;
}
private void UpdateName()
{
name = "New Name";
nameChanged = true;
}
private void UpdateAge()
{
age = 30;
ageChanged = true;
}
}
```
### Key Directive for List Items
```csharp
@page "/list"
<button @onclick="AddItem">Add Item</button>
@foreach (var item in items)
{
<!-- WITHOUT @key - new ItemComponent created for each item -->
<!-- <ItemComponent Item="@item" />-->
<!-- WITH @key - same ItemComponent reused if item.Id stays in list -->
<ItemComponent @key="item.Id" Item="@item" />
}
@code {
private List<Item> items = [];
private void AddItem()
{
items = items.Prepend(new Item { Id = Guid.NewGuid(), Name = "New" }).ToList();
}
}
public class Item
{
public Guid Id { get; set; }
public string? Name { get; set; }
}
```
**Why @key matters:**
- Helps Blazor's diffing algorithm match old components to new items
- Prevents component state loss during list reordering
- Improves performance with large lists
### IDisposable for Cleanup
```csharp
@implements IAsyncDisposable
@inject IJSRuntime JS
private IJSObjectReference? module;
private Timer? timer;
protected override async Task OnInitializedAsync()
{
timer = new Timer(_ => UpdateAsync(), null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
module = await JS.InvokeAsync<IJSObjectReference>(
"import", "./myScript.js");
}
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
timer?.Dispose();
if (module is not null)
{
await module.DisposeAsync();
}
}
```
## Virtualization
Virtualize large lists to render only visible items.
### Basic Virtualization
```csharp
@page "/large-list"
@using Microsoft.AspNetCore.Components.Web.Virtualization
<Virtualize Items="@largeList" Context="item">
<div class="item">
<p>@item.Id - @item.Name</p>
</div>
</Virtualize>
@code {
private List<Item> largeList = [];
protected override void OnInitialized()
{
// Generate 100,000 items
largeList = Enumerable.Range(1, 100000)
.Select(i => new Item { Id = i, Name = $"Item {i}" })
.ToList();
}
}
```
### Async Virtualization (Infinite Scroll)
```csharp
@page "/infinite-scroll"
@using Microsoft.AspNetCore.Components.Web.Virtualization
<Virtualize ItemsProvider="@LoadItems" Context="item" OverscanCount="5">
<div>@item.Name</div>
</Virtualize>
@code {
private async ValueTask<ItemsProviderResult<Item>> LoadItems(
ItemsProviderRequest request)
{
// Simulate loading from server
var startIndex = request.StartIndex;
var count = request.Count;
var items = await Service.GetItemsAsync(startIndex, count);
// Return items and total count for scrollbar sizing
return new ItemsProviderResult<Item>(items, totalItemCount: 1000000);
}
}
```
**Parameters:**
- `Items` - Static list of items to virtualize
- `ItemsProvider` - Async method to load items on demand
- `OverscanCount` - Extra items to render outside viewport (default 3)
- `ItemSize` - Estimated height for scrollbar calculation
## JavaScript Interop
### Invoke JavaScript from C #
```csharp
@inject IJSRuntime JS
<button @onclick="CallJavaScript">Click me</button>
@code {
private async Task CallJavaScript()
{
// Simple call - no return value
await JS.InvokeVoidAsync("console.log", "Hello from Blazor");
// With return value
var result = await JS.InvokeAsync<string>("myFunction", arg1, arg2);
// Generic call with any return type
var data = await JS.InvokeAsync<Data>("loadData");
}
}
```
### JS Module Isolation (Recommended)
```csharp
// Component.razor
@implements IAsyncDisposable
@inject IJSRuntime JS
<div @ref="element">
<canvas id="chart"></canvas>
</div>
@code {
private ElementReference element;
private IJSObjectReference? module;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Import JS module
module = await JS.InvokeAsync<IJSObjectReference>(
"import", "./scripts/chart.js");
// Call exported function
await module.InvokeVoidAsync("initChart", element);
}
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
if (module is not null)
{
await module.DisposeAsync();
}
}
}
/* scripts/chart.js */
export function initChart(element) {
const canvas = element.querySelector('#chart');
// Initialize chart library
}
```
### Invoke C# from JavaScript
```csharp
// Component.razor
@implements IAsyncDisposable
@inject IJSRuntime JS
<button @onclick="SetupInterop">Setup</button>
@code {
private IJSObjectReference? module;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
module = await JS.InvokeAsync<IJSObjectReference>(
"import", "./scripts/interop.js");
// Pass C# object reference to JS
var objRef = DotNetObjectReference.Create(this);
await module.InvokeVoidAsync("setupInterop", objRef);
}
}
[JSInvokable]
public async Task HandleJSEvent(string data)
{
Console.WriteLine($"JS called C#: {data}");
// Update component state
StateHasChanged();
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
if (module is not null)
{
await module.DisposeAsync();
}
}
}
/* scripts/interop.js */
let dotnetHelper;
export function setupInterop(dotnetRef) {
dotnetHelper = dotnetRef;
// Call C# method from JS
document.addEventListener('click', async (e) => {
await dotnetHelper.invokeMethodAsync('HandleJSEvent', 'User clicked');
});
}
```
### Error Handling in Interop
```csharp
@code {
private async Task SafeInvokeAsync()
{
try
{
await JS.InvokeVoidAsync("riskyFunction");
}
catch (JSException jsEx)
{
Console.WriteLine($"JS error: {jsEx.Message}");
}
catch (OperationCanceledException)
{
Console.WriteLine("JS call was cancelled");
}
}
}
```
## Lazy Loading
Load assemblies and components on demand.
### Lazy-Loaded Component Routes
```csharp
<!-- App.razor -->
<Router AppAssembly="@typeof(App).Assembly"
AdditionalAssemblies="@additionalAssemblies"
OnNavigateAsync="@OnNavigateAsync">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<p>Loading...</p>
</NotFound>
</Router>
@code {
private List<Assembly>? additionalAssemblies;
protected override async Task OnInitializedAsync()
{
additionalAssemblies = new();
}
private async Task OnNavigateAsync(NavigationContext context)
{
// Load admin assembly only when accessing /admin
if (context.Path.StartsWith("admin"))
{
var adminAssembly = await JS.InvokeAsync<byte[]>(
"fetch", "./_framework/admin.wasm");
additionalAssemblies!.Add(Assembly.Load(adminAssembly));
}
}
}
```
## WASM Performance Best Practices
### AOT Compilation
```xml
<!-- .csproj -->
<PropertyGroup>
<RunAOTCompilation>true</RunAOTCompilation>
</PropertyGroup>
```
Benefits:
- No JIT compilation at runtime
- Faster startup time
- ~20% larger download
- Production recommended
### Trimming
```xml
<!-- .csproj -->
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>
```
Benefits:
- Removes unused code
- ~40% smaller download
- May cause runtime errors if reflection-based code removed
- Test thoroughly in Release build
### Compression
```xml
<!-- .csproj -->
<PropertyGroup>
<BlazorWebAssemblyEnableCompression>true</BlazorWebAssemblyEnableCompression>
</PropertyGroup>
```
Server-side (in Program.cs):
```csharp
app.UseResponseCompression();
builder.Services.AddResponseCompression(opts =>
{
opts.Filters.Add(new GzipCompressionProvider());
opts.Filters.Add(new BrotliCompressionProvider());
});
```
### Minimize JavaScript Interop
```csharp
// INEFFICIENT - Many JS calls
for (int i = 0; i < 1000; i++)
{
await JS.InvokeVoidAsync("updateUI", i);
}
// EFFICIENT - Single JS call with batch data
var updates = Enumerable.Range(0, 1000).ToList();
await JS.InvokeVoidAsync("updateUIBatch", updates);
```
## Error Boundaries
Handle component errors gracefully.
```csharp
@page "/error-demo"
<ErrorBoundary>
<ChildContent>
<ChildComponent />
</ChildContent>
<ErrorContent Context="ex">
<div class="alert alert-danger">
<h4>Error</h4>
<p>@ex.Message</p>
<button @onclick="ResetError">Try Again</button>
</div>
</ErrorContent>
</ErrorBoundary>
@code {
private ErrorBoundary? errorBoundary;
private async Task ResetError()
{
await errorBoundary!.RecoverAsync();
}
}
```
## CSS Isolation
Scope CSS to specific components.
```html
<!-- MyComponent.razor -->
<div class="container">
<h1>@Title</h1>
</div>
<!-- MyComponent.razor.css -->
.container {
background-color: blue;
padding: 20px;
}
h1 {
color: white;
font-size: 24px;
}
```
**Benefits:**
- No global namespace pollution
- Component-specific styling
- CSS automatically scoped to component
- Compiled into assembly
## Best Practices Summary
### Performance
- Use `@key` on list items
- Override `ShouldRender()` to prevent unnecessary renders
- Use virtualization for large lists
- Minimize JavaScript interop calls
- Enable AOT compilation and trimming for WASM
### JavaScript Interop
- Use module isolation pattern
- Always dispose JS module references
- Handle JS exceptions properly
- Only call JS in `OnAfterRender` with firstRender check
- Minimize interop calls for performance
### Architecture
- Keep components simple and focused
- Move logic to services
- Use cascading values for shared state
- Implement IDisposable for cleanup
- Validate authorization on server side
### User Experience
- Show loading states during async operations
- Provide error feedback
- Use AuthorizeView for conditional rendering
- Implement error boundaries
- Test on slow connections
---
**Related Resources:** See [components-lifecycle.md](components-lifecycle.md) for component disposal patterns. See [state-management-events.md](state-management-events.md) for state update optimization.
@@ -0,0 +1,492 @@
# Blazor Routing & Navigation
## Route Definition
Routes map URL paths to Blazor components.
### Basic Route Definition
```csharp
@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
```csharp
@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:
```csharp
@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
```csharp
@page "/search"
@page "/search/{searchTerm}"
<p>Search term: @(searchTerm ?? "All results")</p>
@code {
[Parameter]
public string? searchTerm { get; set; }
}
```
### Catch-All Routes
```csharp
@page "/{*pageRoute}"
<p>Page not found: @pageRoute</p>
@code {
[Parameter]
public string? pageRoute { get; set; }
}
```
## Navigation
### Programmatic Navigation
```csharp
@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
```csharp
// 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:
```csharp
<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:**
```css
a.active {
color: white;
background-color: blue;
}
```
### Listen to Location Changes
```csharp
@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
```csharp
@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
```csharp
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
```csharp
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:
```csharp
<!-- 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
```csharp
<!-- 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
```csharp
@page "/products"
@layout MainLayout
<h2>Products</h2>
```
### Apply Layout to Multiple Pages
```csharp
<!-- _Imports.razor -->
@layout MainLayout
```
Add this line to `_Imports.razor` to apply layout to all components in folder and below.
### Nested Layouts
```csharp
<!-- AdminLayout inherits from MainLayout -->
@inherits MainLayout
<aside>Admin sidebar</aside>
@Body
```
## Page Titles
Update page title (browser tab) dynamically:
```csharp
@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
```csharp
@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
```csharp
@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
```csharp
@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](components-lifecycle.md) for parameter handling. See [authentication-authorization.md](authentication-authorization.md) for route authorization.
@@ -0,0 +1,575 @@
# Blazor State Management & Events
## Component State
State represents the data that a component manages and renders.
### Local Component State
```csharp
@page "/counter"
<p>Count: @count</p>
<button @onclick="Increment">Click me</button>
@code {
private int count = 0;
private void Increment()
{
count++;
// Re-render happens automatically after event handler
}
}
```
**How it works:**
- Blazor detects state change during event handler execution
- Automatically calls `StateHasChanged()` after handler completes
- Component re-renders with new state
### StateHasChanged() for External Updates
When state updates from outside Blazor's event system, call `StateHasChanged()` explicitly:
```csharp
@implements IDisposable
@inject IJSRuntime JS
private string? externalData;
protected override void OnInitialized()
{
// Subscribe to external event
JS.InvokeVoidAsync("subscribeToEvent", DotNetObjectReference.Create(this));
}
[JSInvokable]
public void NotifyUpdate(string data)
{
externalData = data;
// Blazor doesn't know about JS update, must call explicitly
StateHasChanged();
}
public void Dispose()
{
// Clean up external subscriptions
}
```
### Thread-Safe State Updates with InvokeAsync()
When updating state from background threads (timers, async tasks outside event handlers):
```csharp
@implements IDisposable
private Timer? timer;
private int count = 0;
protected override void OnInitialized()
{
// Timer running on background thread
timer = new Timer(_ => UpdateCount(), null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
}
private void UpdateCount()
{
// WRONG - can't update state from background thread directly
// count++;
// CORRECT - use InvokeAsync to marshal to UI thread
InvokeAsync(() =>
{
count++;
StateHasChanged();
});
}
public void Dispose()
{
timer?.Dispose();
}
```
### State Immutability Pattern
For complex state (objects, lists), follow immutability pattern:
```csharp
@code {
private List<Item> items = [];
// WRONG - mutates in place, may not trigger re-render
private void AddItem()
{
items.Add(new Item { Name = "New" });
}
// CORRECT - create new collection
private void AddItem()
{
items = items.Append(new Item { Name = "New" }).ToList();
}
}
```
## Event Handling
### Basic Click Handler
```csharp
<button @onclick="HandleClick">Click me</button>
@code {
private void HandleClick()
{
// Event handler logic
}
}
```
### EventCallback Pattern (Recommended)
EventCallback is the proper way to notify parent components of events:
```csharp
<!-- Child component -->
<button @onclick="OnClick">Click me</button>
@code {
[Parameter]
public EventCallback<string> OnValueChanged { get; set; }
private async Task OnClick()
{
await OnValueChanged.InvokeAsync("New Value");
}
}
<!-- Parent component -->
<ChildComponent OnValueChanged="@HandleValueChanged" />
@code {
private void HandleValueChanged(string value)
{
// Handle value change
}
}
```
### EventCallback with Arguments
```csharp
<!-- Child -->
<button @onclick="NotifyParent">Send Data</button>
@code {
[Parameter]
public EventCallback<CustomArgs> OnDataChanged { get; set; }
private async Task NotifyParent()
{
var args = new CustomArgs { Id = 123, Name = "Test" };
await OnValueChanged.InvokeAsync(args);
}
}
public class CustomArgs
{
public int Id { get; set; }
public string? Name { get; set; }
}
<!-- Parent -->
<ChildComponent OnDataChanged="@(args => HandleData(args.Id, args.Name))" />
@code {
private void HandleData(int id, string? name)
{
// Process data
}
}
```
### Async Event Handlers
Always use async properly with EventCallback:
```csharp
<!-- Good - async handler, proper awaiting -->
<button @onclick="SaveAsync">Save</button>
@code {
private async Task SaveAsync()
{
isLoading = true;
try
{
await Service.SaveDataAsync(data);
successMessage = "Saved!";
}
catch (Exception ex)
{
errorMessage = ex.Message;
}
finally
{
isLoading = false;
}
}
}
```
### Common Event Handlers
```csharp
<!-- Click -->
<button @onclick="HandleClick">Click</button>
<!-- Double click -->
<div @ondblclick="HandleDoubleClick">Double click</div>
<!-- Focus/Blur -->
<input @onfocus="HandleFocus" @onblur="HandleBlur" />
<!-- Key events -->
<input @onkeydown="HandleKeyDown" @onkeyup="HandleKeyUp" />
<!-- Mouse events -->
<div @onmouseover="HandleMouseOver" @onmouseout="HandleMouseOut" />
<!-- Change -->
<select @onchange="HandleChange">
<option>Option 1</option>
</select>
<!-- Submit -->
<form @onsubmit="HandleSubmit">
<button type="submit">Submit</button>
</form>
```
### preventDefault and stopPropagation
```csharp
<!-- Prevent form submission -->
<form @onsubmit:preventDefault="true" @onsubmit="HandleSubmit">
<input type="text" />
<button type="submit">Submit</button>
</form>
<!-- Stop event propagation -->
<div @onclick="ParentClick">
<button @onclick="ChildClick" @onclick:stopPropagation="true">
Click - won't bubble
</button>
</div>
```
## Data Binding
### Two-Way Binding (@bind)
```csharp
<input @bind="name" />
<p>You entered: @name</p>
@code {
private string name = "";
}
```
**How it works:**
- `@bind` = `@bind-value` + `@bind-value:event="onchange"`
- Sets value property, listens to onchange event
- Automatic two-way synchronization
### Custom Events with @bind
```csharp
<input @bind="value" @bind:event="oninput" />
@code {
private string value = "";
}
```
Events: `onchange` (default), `oninput` (real-time), `onblur`, etc.
### Numeric Binding
```csharp
<input @bind="age" @bind:culture="CultureInfo.InvariantCulture" />
@code {
private int age = 0;
}
```
### DateTime Binding
```csharp
<input type="date" @bind="date" />
<input type="datetime-local" @bind="dateTime" />
@code {
private DateOnly date = DateOnly.FromDateTime(DateTime.Now);
private DateTime dateTime = DateTime.Now;
}
```
### Binding with Format Specifiers
```csharp
<input @bind="price" @bind:format="N2" />
<p>Price: @price.ToString("C")</p>
@code {
private decimal price = 0;
}
```
### Bind Modifiers
```csharp
<!-- @bind:get / @bind:set for custom logic -->
<input @bind="@value"
@bind:get="parsedValue"
@bind:set="@SetValue" />
@code {
private string value = "";
private string parsedValue
{
get => value.ToUpper();
}
private void SetValue(string val)
{
value = val.ToLower();
}
}
```
## Cascading Values with Events
Provide shared state and event callbacks to child components:
```csharp
<!-- Parent - AppState provider -->
<CascadingValue Value="@appState">
@ChildContent
</CascadingValue>
@code {
private AppState appState = new();
[Parameter]
public RenderFragment? ChildContent { get; set; }
}
<!-- AppState service -->
public class AppState
{
private string _username = "";
public event Action? OnChange;
public string Username
{
get => _username;
set
{
if (_username != value)
{
_username = value;
NotifyStateChanged();
}
}
}
public void NotifyStateChanged() => OnChange?.Invoke();
}
<!-- Child component - subscribe to state changes -->
@implements IDisposable
@code {
[CascadingParameter]
public AppState? AppState { get; set; }
protected override void OnInitialized()
{
if (AppState != null)
{
AppState.OnChange += StateHasChanged;
}
}
public void Dispose()
{
if (AppState != null)
{
AppState.OnChange -= StateHasChanged;
}
}
}
```
## Service-Based State Management
For application-wide state, use services:
```csharp
// Program.cs
builder.Services.AddScoped<AppState>();
// AppState service
public class AppState
{
private string _theme = "light";
private User? _currentUser;
public event Func<Task>? OnStateChange;
public string Theme
{
get => _theme;
set
{
if (_theme != value)
{
_theme = value;
NotifyStateChanged();
}
}
}
public User? CurrentUser
{
get => _currentUser;
set
{
if (_currentUser != value)
{
_currentUser = value;
NotifyStateChanged();
}
}
}
private async Task NotifyStateChanged()
{
if (OnStateChange != null)
{
await OnStateChange.Invoke();
}
}
}
// Component using AppState
@inject AppState AppState
@implements IAsyncDisposable
@code {
protected override async Task OnInitializedAsync()
{
AppState.OnStateChange += StateHasChanged;
AppState.CurrentUser = await LoadUserAsync();
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
if (AppState != null)
{
AppState.OnStateChange -= StateHasChanged;
}
}
}
```
## Parent-Child Communication Pattern
**Data flow:** Parents pass data DOWN via parameters, children notify UP via events.
```csharp
<!-- Parent -->
@page "/parent"
<h2>Parent: @selectedId</h2>
<Child SelectedId="@selectedId"
OnIdChanged="@HandleIdChanged" />
@code {
private int selectedId = 0;
private async Task HandleIdChanged(int newId)
{
selectedId = newId;
}
}
<!-- Child -->
<select @onchange="OnSelectionChanged">
@foreach (var item in Items)
{
<option value="@item.Id">@item.Name</option>
}
</select>
@code {
[Parameter]
public int SelectedId { get; set; }
[Parameter]
public EventCallback<int> OnIdChanged { get; set; }
private List<Item> Items { get; set; } = [];
private async Task OnSelectionChanged(ChangeEventArgs args)
{
var newId = int.Parse(args.Value?.ToString() ?? "0");
await OnIdChanged.InvokeAsync(newId);
}
}
```
## Best Practices
### Always Use EventCallback
-`[Parameter] public EventCallback OnEvent { get; set; }`
-`[Parameter] public Action? OnEvent { get; set; }`
EventCallback handles async properly and integrates better with Blazor's rendering pipeline.
### Keep Event Handlers Focused
- Do one thing per handler
- Move complex logic to services
- Keep components as thin view layers
### Unsubscribe from Events
Always clean up subscriptions to prevent memory leaks:
```csharp
@implements IDisposable
protected override void OnInitialized()
{
Service.OnChange += HandleChange;
}
public void Dispose()
{
Service.OnChange -= HandleChange;
}
```
### Use Immutable Updates
- Create new objects/collections for state updates
- Don't mutate objects in place
- Helps with change detection and debugging
---
**Related Resources:** See [components-lifecycle.md](components-lifecycle.md) for component parameters and cascading values. See [forms-validation.md](forms-validation.md) for form event handling.