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
+288
View File
@@ -0,0 +1,288 @@
---
name: blazor-expert
description: Comprehensive Blazor development expertise covering Blazor Server, WebAssembly, and Hybrid apps. Use when building Blazor components, implementing state management, handling routing, JavaScript interop, forms and validation, authentication, or optimizing Blazor applications. Includes best practices, architecture patterns, and troubleshooting guidance.
version: 2.0
---
# Blazor Expert - Orchestration Hub
Expert-level guidance for developing applications with Blazor, Microsoft's framework for building interactive web UIs using C# instead of JavaScript.
## Quick Reference: When to Load Which Resource
| Task | Load Resource | Key Topics |
|------|---------------|-----------|
| **Build components, handle lifecycle events** | [components-lifecycle.md](resources/components-lifecycle.md) | Component structure, lifecycle methods, parameters, cascading values, RenderFragment composition |
| **Manage component state, handle events** | [state-management-events.md](resources/state-management-events.md) | Local state, EventCallback, data binding, cascading state, service-based state |
| **Configure routes, navigate between pages** | [routing-navigation.md](resources/routing-navigation.md) | Route parameters, constraints, navigation, NavLink, query strings, layouts |
| **Build forms, validate user input** | [forms-validation.md](resources/forms-validation.md) | EditForm, input components, DataAnnotations validation, custom validators |
| **Setup authentication & authorization** | [authentication-authorization.md](resources/authentication-authorization.md) | Auth setup, AuthorizeView, Authorize attribute, policies, claims |
| **Optimize performance, use JavaScript interop** | [performance-advanced.md](resources/performance-advanced.md) | Rendering optimization, virtualization, JS interop, lazy loading, WASM best practices |
## Orchestration Protocol
### Phase 1: Task Analysis
Identify your primary objective:
- **UI Building** → Load components-lifecycle.md
- **State Handling** → Load state-management-events.md
- **Navigation** → Load routing-navigation.md
- **Data Input** → Load forms-validation.md
- **User Access** → Load authentication-authorization.md
- **Speed/Efficiency** → Load performance-advanced.md
### Phase 2: Resource Loading
Open the recommended resource file(s) and search for your specific need using Ctrl+F. Each resource is organized by topic with working code examples.
### Phase 3: Implementation & Validation
- Follow code patterns from the resource
- Adapt to your specific requirements
- Test in appropriate hosting model (Server/WASM/Hybrid)
- Review troubleshooting section if issues arise
## Blazor Hosting Models Overview
### Blazor Server
- **How**: Runs on server via SignalR
- **Best For**: Line-of-business apps, need full .NET runtime, small download size
- **Trade-offs**: High latency, requires connection, server resource intensive
### Blazor WebAssembly
- **How**: Runs in browser via WebAssembly
- **Best For**: PWAs, offline apps, no server dependency, client-heavy applications
- **Trade-offs**: Large initial download, limited .NET APIs, slower cold start
### Blazor Hybrid
- **How**: Runs in MAUI/WPF/WinForms with Blazor UI
- **Best For**: Cross-platform desktop/mobile apps
- **Trade-offs**: Platform-specific considerations, additional dependencies
**Decision**: Choose based on deployment environment, offline requirements, and server constraints.
## Common Implementation Workflows
### Scenario 1: Build a Data-Entry Component
1. Read [components-lifecycle.md](resources/components-lifecycle.md) - Component structure section
2. Read [state-management-events.md](resources/state-management-events.md) - EventCallback pattern
3. Read [forms-validation.md](resources/forms-validation.md) - EditForm component
4. Combine: Create component with parameters → capture user input → validate → notify parent
### Scenario 2: Implement User Authentication & Protected Pages
1. Read [authentication-authorization.md](resources/authentication-authorization.md) - Setup section
2. Read [routing-navigation.md](resources/routing-navigation.md) - Layouts section
3. Read [authentication-authorization.md](resources/authentication-authorization.md) - AuthorizeView section
4. Combine: Configure auth → create login page → protect routes → check auth in components
### Scenario 3: Build Interactive List with Search/Filter
1. Read [routing-navigation.md](resources/routing-navigation.md) - Query strings section
2. Read [state-management-events.md](resources/state-management-events.md) - Data binding section
3. Read [performance-advanced.md](resources/performance-advanced.md) - Virtualization section
4. Combine: Capture search input → update URL query → fetch filtered data → virtualize if large
### Scenario 4: Optimize Performance of Existing App
1. Read [performance-advanced.md](resources/performance-advanced.md) - All sections
2. Identify bottlenecks:
- Unnecessary renders? → ShouldRender override, @key directive
- Large lists? → Virtualization
- JS latency? → Module isolation pattern
3. Apply targeted optimizations from resource
## Key Blazor Concepts
### Component Architecture
- **Components**: Self-contained UI units with optional logic
- **Parameters**: Inputs to components, enable reusability
- **Cascading Values**: Share state with descendants without explicit parameters
- **Events**: Child-to-parent communication via EventCallback
- **Layouts**: Parent wrapper for consistent page structure
### State Management
- **Local State**: Component-specific fields and properties
- **Cascading Values**: Share state to descendants
- **Services**: Application-wide state via dependency injection
- **Event Binding**: React to user interactions
- **Data Binding**: Two-way synchronization with UI
### Routing & Navigation
- **@page Directive**: Make component routable
- **Route Parameters**: Pass data via URL (`{id:int}`)
- **Navigation**: Programmatic navigation via NavigationManager
- **NavLink**: UI component that highlights active route
- **Layouts**: Wrap pages with common structure
### Forms & Validation
- **EditForm**: Form component with validation support
- **Input Components**: Typed controls (InputText, InputNumber, etc.)
- **Validators**: DataAnnotations attributes or custom logic
- **EventCallback**: Notify parent of form changes
- **Messages**: Display validation errors to user
### Authentication & Authorization
- **Claims & Roles**: Identify users and define access levels
- **Policies**: Fine-grained authorization rules
- **Authorize Attribute**: Protect pages from unauthorized access
- **AuthorizeView**: Conditional rendering based on permissions
- **AuthenticationStateProvider**: Get current user information
### Performance Optimization
- **ShouldRender()**: Prevent unnecessary re-renders
- **@key Directive**: Help diffing algorithm match list items
- **Virtualization**: Render only visible items in large lists
- **JS Interop**: Call JavaScript from C# and vice versa
- **AOT/Trimming**: Reduce WASM download size (production)
## Best Practices Highlights
### Component Design
**Single Responsibility** - Each component has one clear purpose
**Composition** - Use RenderFragments for flexible layouts
**Parameter Clarity** - Use descriptive names, mark required with `[EditorRequired]`
**Proper Disposal** - Implement `IDisposable` to clean up resources
**Event-Based Communication** - Use `EventCallback` for child-to-parent updates
### State Management
**EventCallback Over Action** - Proper async handling
**Immutable Updates** - Create new objects/collections, don't mutate
**Service-Based State** - Use scoped services for shared state
**Unsubscribe from Events** - Prevent memory leaks in Dispose
**InvokeAsync for Background Threads** - Thread-safe state updates
### Routing & Navigation
**Route Constraints** - Use `:int`, `:guid`, etc. to validate formats
**NavLink Component** - Automatic active state highlighting
**forceLoad After Logout** - Clear client-side state
**ReturnUrl Pattern** - Redirect back after login
**Query Strings** - Preserve filters/pagination across navigation
### Forms & Validation
**EditForm + DataAnnotationsValidator** - Built-in validation
**ValidationMessage** - Show field-level errors
**Custom Validators** - Extend for complex rules
**Async Validation** - Check server availability before submit
**Loading State** - Disable submit button while processing
### Authentication & Authorization
**Server Validation** - Never trust client-side checks alone
**Policies Over Roles** - More flexible authorization rules
**Claims for Details** - Store user attributes in claims
**Cascading AuthenticationState** - Available in all components
**Error Boundaries** - Graceful error handling
### Performance
**@key on Lists** - Optimize item matching
**ShouldRender Override** - Prevent unnecessary renders
**Virtualization for Large Lists** - Only render visible items
**JS Module Isolation** - Load and cache JS modules efficiently
**AOT for WASM** - Production deployments
## Common Troubleshooting
### Component Not Re-rendering
- **Cause**: Mutation instead of reassignment
- **Fix**: Create new object/collection: `items = items.Append(item).ToList()`
- **Or**: Call `StateHasChanged()` manually
### Parameter Not Updating
- **Cause**: Parent not re-rendering or same object reference
- **Fix**: Parent must re-render, ensure new reference for objects
- **Debug**: Check OnParametersSet is firing
### JS Interop Errors
- **Cause**: Called before script loaded or wrong function name
- **Fix**: Use `firstRender` check, verify JS file path
- **Pattern**: Use module isolation: `await JS.InvokeAsync("import", "./script.js")`
### Authentication State Not Available
- **Cause**: Cascading parameter not provided or timing issue
- **Fix**: Ensure AuthenticationStateProvider configured
- **Pattern**: Always null-check and use `await AuthStateTask!` in code block
### Large List Performance Issues
- **Cause**: Rendering all items in DOM
- **Fix**: Use Virtualize component for 1000+ items
- **Alternative**: Paginate with buttons/infinite scroll
### Blazor Server Connection Issues
- **Cause**: SignalR connection dropped or configuration issue
- **Fix**: Implement reconnection UI, increase timeout
- **Config**: Adjust `CircuitOptions.DisconnectedCircuitRetentionPeriod`
## Resource Files Summary
### components-lifecycle.md
Complete guide to component structure, lifecycle methods, parameters, cascading values, and composition patterns. Essential for understanding Blazor component fundamentals.
### state-management-events.md
Comprehensive coverage of local and service-based state, event handling with EventCallback, data binding patterns, and component communication. Core for interactive UI building.
### routing-navigation.md
Complete routing reference including route parameters, constraints, programmatic navigation, query strings, and layout management. Essential for multi-page apps.
### forms-validation.md
Full forms API with EditForm component, input controls, DataAnnotations validation, custom validators, and form patterns. Required for data entry scenarios.
### authentication-authorization.md
Complete auth setup for Server and WASM, AuthorizeView, policies, claims-based access control, and login/logout patterns. Necessary for secured applications.
### performance-advanced.md
Performance optimization techniques including ShouldRender, virtualization, JavaScript interop patterns, lazy loading, and WASM best practices. Vital for production apps.
---
## Implementation Approach
When implementing Blazor features:
1. **Identify Your Task** - Match against the decision table above
2. **Load Relevant Resource** - Read the appropriate .md file
3. **Find Code Example** - Search resource for similar implementation
4. **Adapt to Your Context** - Modify for your specific requirements
5. **Test Thoroughly** - Verify in your hosting model
6. **Reference Troubleshooting** - Consult resource if issues arise
## Next Steps
- **New to Blazor?** Start with [components-lifecycle.md](resources/components-lifecycle.md)
- **Building Data App?** Move through: components → state → forms → validation
- **Scaling Existing App?** Focus on [performance-advanced.md](resources/performance-advanced.md)
- **Adding Security?** Follow [authentication-authorization.md](resources/authentication-authorization.md)
---
**Version**: 2.0 - Modular Orchestration Pattern
**Last Updated**: December 4, 2025
**Status**: Production Ready ✅
@@ -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.