@* Data grid with sorting *@
Edit
@* Accordion *@
Content for section 1
Content for section 2
```
---
### Feedback Components
```razor
@* Dialog *@
Confirmation
Are you sure you want to delete this item?
Yes, Delete
Cancel
@* Message bar (notification) *@
Item saved successfully!
@* Progress ring (loading spinner) *@
@* Toast notification *@
@inject IToastService ToastService
@code {
private void ShowToast()
{
ToastService.ShowSuccess("Operation completed successfully!");
}
}
```
---
## 4. State Management
### Component State
```csharp
@code {
// Private field for component-local state
private int count = 0;
private string message = "";
private List items = new();
// State change triggers re-render automatically
private void UpdateState()
{
count++;
// Component re-renders automatically
}
}
```
---
### Service-Based State
```csharp
// AppState.cs
public class AppState
{
private string currentUser = "";
public string CurrentUser
{
get => currentUser;
set
{
if (currentUser != value)
{
currentUser = value;
NotifyStateChanged();
}
}
}
public event Action? OnChange;
private void NotifyStateChanged() => OnChange?.Invoke();
}
// Program.cs
builder.Services.AddScoped();
// Component
@inject AppState AppState
@implements IDisposable
@code {
protected override void OnInitialized()
{
AppState.OnChange += StateHasChanged;
}
public void Dispose()
{
AppState.OnChange -= StateHasChanged;
}
private void UpdateUser()
{
AppState.CurrentUser = "John Doe";
// All subscribed components re-render
}
}
```
---
### Cascading Values
```razor
@* App.razor or parent component *@
@* Child components *@
@code {
private string currentTheme = "light";
}
@* Child component *@
@code {
[CascadingParameter]
public string Theme { get; set; } = "";
// Access Theme without prop drilling
}
```
---
## 5. Forms & Validation
### Basic EditForm
```razor
Submit
@code {
private Person person = new();
private async Task HandleValidSubmit()
{
// Form is valid
await SavePersonAsync(person);
}
public class Person
{
[Required(ErrorMessage = "Name is required")]
[MaxLength(100)]
public string Name { get; set; } = "";
[Range(0, 150, ErrorMessage = "Age must be between 0 and 150")]
public int Age { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; } = "";
}
}
```
---
### FluentValidation
```csharp
// Install: FluentValidation.Blazor
using FluentValidation;
public class PersonValidator : AbstractValidator
{
public PersonValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Name is required")
.MaximumLength(100);
RuleFor(x => x.Age)
.InclusiveBetween(0, 150);
RuleFor(x => x.Email)
.NotEmpty()
.EmailAddress();
}
}
// Component
@* Form fields *@
```
---
### Custom Validation
```csharp
// Custom attribute
public class FutureDateAttribute : ValidationAttribute
{
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
if (value is DateTime date && date > DateTime.Now)
{
return ValidationResult.Success;
}
return new ValidationResult("Date must be in the future");
}
}
// Usage
public class Event
{
[FutureDate]
public DateTime EventDate { get; set; }
}
```
---
## 6. Routing & Navigation
### Route Declaration
```razor
@* Basic route *@
@page "/products"
@* Multiple routes *@
@page "/products"
@page "/items"
@* Route parameter *@
@page "/product/{id}"
@code {
[Parameter]
public string Id { get; set; } = "";
}
@* Optional parameter *@
@page "/product/{id?}"
@* Route constraint *@
@page "/product/{id:int}"
@page "/user/{userId:guid}"
@* Catch-all parameter *@
@page "/docs/{*path}"
```
---
### Navigation
```csharp
@inject NavigationManager Navigation
@code {
private void NavigateToProduct(int id)
{
Navigation.NavigateTo($"/product/{id}");
}
private void NavigateExternal()
{
Navigation.NavigateTo("https://example.com", forceLoad: true);
}
private void Refresh()
{
Navigation.NavigateTo(Navigation.Uri, forceLoad: true);
}
// Get current URL
var currentUrl = Navigation.Uri;
var baseUrl = Navigation.BaseUri;
// Query string
var uri = new Uri(Navigation.Uri);
var query = System.Web.HttpUtility.ParseQueryString(uri.Query);
var searchTerm = query["search"];
}
```
---
### NavLink Component
```razor
@* Exact match (active only for exact path) *@
Home
@* Prefix match (active for path and descendants) *@
Products
```
---
## 7. JavaScript Interop
### Calling JavaScript from Blazor
```csharp
@inject IJSRuntime JS
@code {
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Call void function
await JS.InvokeVoidAsync("alert", "Hello from Blazor!");
// Call function with return value
var result = await JS.InvokeAsync("localStorage.getItem", "key");
// Call custom function
await JS.InvokeVoidAsync("myApp.initialize", elementRef);
}
}
}
```
**JavaScript file (wwwroot/js/app.js)**:
```javascript
window.myApp = {
initialize: function(element) {
console.log("Initializing", element);
},
getData: function() {
return { value: 42 };
}
};
```
---
### Calling Blazor from JavaScript
```csharp
// Component
public class InteropComponent : ComponentBase
{
private DotNetObjectReference? dotNetHelper;
protected override void OnInitialized()
{
dotNetHelper = DotNetObjectReference.Create(this);
}
[JSInvokable]
public void CallMeFromJS(string message)
{
Console.WriteLine($"Called from JS: {message}");
StateHasChanged(); // Re-render if needed
}
[JSInvokable]
public Task GetDataFromBlazor()
{
return Task.FromResult("Data from Blazor");
}
public void Dispose()
{
dotNetHelper?.Dispose();
}
}
```
**JavaScript**:
```javascript
// Call static method
DotNet.invokeMethodAsync('MyApp', 'StaticMethod', 'arg1');
// Call instance method
dotNetHelper.invokeMethodAsync('CallMeFromJS', 'Hello');
```
---
## 8. Accessibility (WCAG 2.1 AA)
### Semantic HTML
```razor
Page Title
Article Heading
Content...
```
---
### ARIA Attributes
```razor
@* Button with accessible label *@
@* Form with descriptive text *@
We'll never share your email.
@* Hidden decorative icon *@
@* Live region for dynamic updates *@
@statusMessage
```
---
### Keyboard Navigation
```razor
@* All Fluent UI components support keyboard navigation *@
@* Custom component with keyboard support *@
Click or press Enter
@code {
private void HandleKeyDown(KeyboardEventArgs e)
{
if (e.Key == "Enter" || e.Key == " ")
{
// Activate on Enter or Space
Activate();
}
else if (e.Key == "Escape")
{
// Close on Escape
Close();
}
}
}
```
---
## 9. SignalR & Real-Time (Blazor Server)
### Hub Definition
```csharp
// NotificationHub.cs
using Microsoft.AspNetCore.SignalR;
public class NotificationHub : Hub
{
public async Task SendNotification(string user, string message)
{
await Clients.All.SendAsync("ReceiveNotification", user, message);
}
public async Task JoinGroup(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
}
public async Task SendToGroup(string groupName, string message)
{
await Clients.Group(groupName).SendAsync("ReceiveMessage", message);
}
}
// Program.cs
builder.Services.AddSignalR();
app.MapHub("/notificationhub");
```
---
### Component with SignalR
```razor
@inject NavigationManager Navigation
@implements IAsyncDisposable
Real-Time Notifications
@foreach (var notification in notifications)
{
@notification
}
@code {
private HubConnection? hubConnection;
private List notifications = new();
protected override async Task OnInitializedAsync()
{
hubConnection = new HubConnectionBuilder()
.WithUrl(Navigation.ToAbsoluteUri("/notificationhub"))
.WithAutomaticReconnect()
.Build();
hubConnection.On("ReceiveNotification", (user, message) =>
{
notifications.Add($"{user}: {message}");
StateHasChanged();
});
await hubConnection.StartAsync();
}
private async Task SendNotification()
{
if (hubConnection is not null)
{
await hubConnection.SendAsync("SendNotification", "User", "Hello!");
}
}
public async ValueTask DisposeAsync()
{
if (hubConnection is not null)
{
await hubConnection.DisposeAsync();
}
}
}
```
---
## 10. Testing with bUnit
### Basic Component Test
```csharp
using Bunit;
using FluentAssertions;
using Xunit;
public class CounterTests : TestContext
{
[Fact]
public void Counter_StartsAtZero()
{
// Arrange & Act
var cut = RenderComponent();
// Assert
cut.Find("p").TextContent.Should().Contain("count: 0");
}
[Fact]
public void Counter_Increments_OnButtonClick()
{
// Arrange
var cut = RenderComponent();
var button = cut.Find("button");
// Act
button.Click();
// Assert
cut.Find("p").TextContent.Should().Contain("count: 1");
}
}
```
---
### Testing with Parameters
```csharp
[Fact]
public void Component_Renders_WithParameters()
{
// Arrange & Act
var cut = RenderComponent(parameters => parameters
.Add(p => p.Title, "Test Title")
.Add(p => p.Count, 5));
// Assert
cut.Find("h3").TextContent.Should().Be("Test Title");
cut.Find("p").TextContent.Should().Contain("5");
}
```
---
### Testing EventCallbacks
```csharp
[Fact]
public void Component_Invokes_Callback()
{
// Arrange
var callbackInvoked = false;
var cut = RenderComponent(parameters => parameters
.Add(p => p.OnClick, () => callbackInvoked = true));
// Act
cut.Find("button").Click();
// Assert
callbackInvoked.Should().BeTrue();
}
```
---
### Testing with Services
```csharp
[Fact]
public void Component_Uses_InjectedService()
{
// Arrange
var mockService = new Mock();
mockService.Setup(s => s.GetData()).Returns(new List { new() { Name = "Test" } });
Services.AddSingleton(mockService.Object);
// Act
var cut = RenderComponent();
// Assert
cut.FindAll("li").Should().HaveCount(1);
cut.Find("li").TextContent.Should().Be("Test");
}
```
---
## Common Patterns Summary
| Pattern | Key Concept | Example |
|---------|-------------|---------|
| **Component Parameters** | `[Parameter]` property | Parent-to-child data flow |
| **EventCallback** | `EventCallback` parameter | Child-to-parent communication |
| **Cascading Values** | `` + `[CascadingParameter]` | Share data down tree |
| **State Service** | Scoped service with events | Global state management |
| **EditForm** | `` | Form with validation |
| **JavaScript Interop** | `IJSRuntime.InvokeAsync` | Call JS from Blazor |
| **SignalR** | `HubConnection` | Real-time communication |
| **bUnit Testing** | `RenderComponent()` | Component unit tests |
---
## Best Practices
1. **Use Fluent UI components** for consistent, accessible UI
2. **Dispose resources** in `Dispose()`/`DisposeAsync()`
3. **Async all the way** - Prefer `OnInitializedAsync` over synchronous methods
4. **EventCallback for events** - Not regular events
5. **Services for shared state** - With change notification pattern
6. **Semantic HTML** - Use proper elements for accessibility
7. **ARIA labels** - For icon buttons and dynamic content
8. **Test components** - Use bUnit for component logic tests
9. **SignalR reconnection** - Use `WithAutomaticReconnect()`
10. **Route constraints** - Validate route parameters at routing level
---
**Next**: See `REFERENCE.md` for comprehensive guides, advanced patterns, and production deployment strategies.