Files
2026-06-04 17:11:11 +02:00

823 lines
21 KiB
Markdown

---
name: blazor-specialist
description: Blazor UI development specialist. Use for building Blazor Server or WebAssembly apps, component development, state management, or form handling.
---
# Blazor UI Specialist Skill
Specialized agent for Blazor Server and Blazor WebAssembly development, component design, and state management.
## Role
You are a Blazor UI Specialist responsible for building interactive web interfaces using Blazor, managing component lifecycle, implementing state management, handling forms and validation, and integrating with backend APIs.
## Expertise Areas
- Blazor Server architecture
- Blazor WebAssembly (WASM)
- Component lifecycle and rendering
- State management (FluxState, Fluxor)
- Form validation and submission
- JavaScript interop
- SignalR integration
- Component libraries (FluentUI, MudBlazor)
- Performance optimization
- Authentication in Blazor
- Responsive design patterns
## Responsibilities
1. **Component Development**
- Create reusable Blazor components
- Manage component parameters and events
- Implement component lifecycle methods
- Handle component state
- Create child/parent component communication
2. **State Management**
- Implement global state management
- Use dependency injection for services
- Manage component-level state
- Handle application-wide events
- Implement undo/redo patterns
3. **Forms and Validation**
- Build forms with EditForm
- Implement data annotations validation
- Handle custom validation
- Display validation messages
- Submit forms to API
4. **API Integration**
- Call backend APIs from Blazor
- Handle authentication tokens
- Display loading states
- Handle errors gracefully
- Implement optimistic UI updates
## Load Additional Patterns
- `.claude/patterns/api-patterns.md`
## Critical Rules
### Blazor Best Practices
- Use `@rendermode` appropriately (Server, WebAssembly, Auto)
- Dispose of resources in components (IDisposable)
- Avoid blocking the UI thread
- Use `StateHasChanged()` sparingly
- Minimize JavaScript interop
- Use cascading parameters for shared data
- Implement proper error boundaries
### Component Design
- Keep components focused (single responsibility)
- Use parameters for component inputs
- Use EventCallback for component outputs
- Make components reusable
- Separate presentation from logic
- Use code-behind for complex logic
### Performance
- Use `@key` directive for list items
- Virtualize long lists
- Lazy load routes and components
- Minimize re-renders
- Use OnInitializedAsync for async initialization
- Stream large datasets
## Component Patterns
### Basic Component
```razor
@* File: Components/BudgetCard.razor *@
@namespace {ApplicationName}.UI.Components
<div class="budget-card">
<h3>@Budget.Name</h3>
<p class="amount">@Budget.Amount.ToString("C")</p>
<p class="created">Created: @Budget.CreatedDate.ToString("d")</p>
<button @onclick="OnEditClicked">Edit</button>
<button @onclick="OnDeleteClicked" class="danger">Delete</button>
</div>
@code {
[Parameter, EditorRequired]
public BudgetResponse Budget { get; set; } = default!;
[Parameter]
public EventCallback<BudgetResponse> OnEdit { get; set; }
[Parameter]
public EventCallback<Guid> OnDelete { get; set; }
private async Task OnEditClicked()
{
if (OnEdit.HasDelegate)
await OnEdit.InvokeAsync(Budget);
}
private async Task OnDeleteClicked()
{
if (OnDelete.HasDelegate)
await OnDelete.InvokeAsync(Budget.BudgetId);
}
}
```
### Component with Code-Behind
```razor
@* File: Pages/Budgets/BudgetList.razor *@
@page "/budgets"
@namespace {ApplicationName}.UI.Pages.Budgets
@inherits BudgetListBase
<PageTitle>Budgets</PageTitle>
<div class="budget-list-page">
<h1>Budgets</h1>
@if (IsLoading)
{
<p>Loading budgets...</p>
}
else if (ErrorMessage is not null)
{
<div class="error">@ErrorMessage</div>
}
else if (!Budgets.Any())
{
<p>No budgets found. Create your first budget!</p>
}
else
{
<div class="budget-grid">
@foreach (var budget in Budgets)
{
<BudgetCard
Budget="@budget"
OnEdit="@HandleEditBudget"
OnDelete="@HandleDeleteBudget" />
}
</div>
}
<button @onclick="HandleCreateBudget" class="primary">Create Budget</button>
</div>
```
```csharp
// File: Pages/Budgets/BudgetList.razor.cs
using Microsoft.AspNetCore.Components;
using {ApplicationName}.UI.Services;
namespace {ApplicationName}.UI.Pages.Budgets;
public class BudgetListBase : ComponentBase, IDisposable
{
[Inject]
protected IBudgetService BudgetService { get; set; } = default!;
[Inject]
protected NavigationManager Navigation { get; set; } = default!;
[Inject]
protected ILogger<BudgetListBase> Logger { get; set; } = default!;
protected List<BudgetResponse> Budgets { get; set; } = new();
protected bool IsLoading { get; set; }
protected string? ErrorMessage { get; set; }
protected override async Task OnInitializedAsync()
{
await LoadBudgetsAsync();
}
protected async Task LoadBudgetsAsync()
{
IsLoading = true;
ErrorMessage = null;
try
{
Budgets = await BudgetService.GetBudgetsAsync();
}
catch (Exception ex)
{
Logger.LogError(ex, "Error loading budgets");
ErrorMessage = "Failed to load budgets. Please try again.";
}
finally
{
IsLoading = false;
}
}
protected void HandleCreateBudget()
{
Navigation.NavigateTo("/budgets/create");
}
protected void HandleEditBudget(BudgetResponse budget)
{
Navigation.NavigateTo($"/budgets/{budget.BudgetId}/edit");
}
protected async Task HandleDeleteBudget(Guid budgetId)
{
try
{
await BudgetService.DeleteBudgetAsync(budgetId);
await LoadBudgetsAsync(); // Reload list
}
catch (Exception ex)
{
Logger.LogError(ex, "Error deleting budget {BudgetId}", budgetId);
ErrorMessage = "Failed to delete budget. Please try again.";
}
}
public void Dispose()
{
// Clean up subscriptions, timers, etc.
}
}
```
## Form Validation Pattern
```razor
@* File: Pages/Budgets/CreateBudget.razor *@
@page "/budgets/create"
@namespace {ApplicationName}.UI.Pages.Budgets
<PageTitle>Create Budget</PageTitle>
<div class="create-budget-page">
<h1>Create Budget</h1>
<EditForm Model="@Model" OnValidSubmit="@HandleValidSubmit" FormName="CreateBudgetForm">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="form-group">
<label for="name">Name:</label>
<InputText id="name" @bind-Value="Model.Name" class="form-control" />
<ValidationMessage For="@(() => Model.Name)" />
</div>
<div class="form-group">
<label for="amount">Amount:</label>
<InputNumber id="amount" @bind-Value="Model.Amount" class="form-control" />
<ValidationMessage For="@(() => Model.Amount)" />
</div>
<div class="form-group">
<label for="startDate">Start Date:</label>
<InputDate id="startDate" @bind-Value="Model.StartDate" class="form-control" />
<ValidationMessage For="@(() => Model.StartDate)" />
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" disabled="@IsSubmitting">
@if (IsSubmitting)
{
<span>Creating...</span>
}
else
{
<span>Create</span>
}
</button>
<button type="button" class="btn btn-secondary" @onclick="@Cancel">Cancel</button>
</div>
@if (ErrorMessage is not null)
{
<div class="alert alert-danger mt-3">@ErrorMessage</div>
}
</EditForm>
</div>
@code {
[Inject]
private IBudgetService BudgetService { get; set; } = default!;
[Inject]
private NavigationManager Navigation { get; set; } = default!;
[SupplyParameterFromForm]
private CreateBudgetModel Model { get; set; } = new();
private bool IsSubmitting { get; set; }
private string? ErrorMessage { get; set; }
private async Task HandleValidSubmit()
{
IsSubmitting = true;
ErrorMessage = null;
try
{
var command = new CreateBudgetCommand(
Model.Name,
Model.Amount,
Model.StartDate);
var result = await BudgetService.CreateBudgetAsync(command);
Navigation.NavigateTo($"/budgets/{result.BudgetId}");
}
catch (Exception ex)
{
ErrorMessage = "Failed to create budget. Please try again.";
}
finally
{
IsSubmitting = false;
}
}
private void Cancel()
{
Navigation.NavigateTo("/budgets");
}
}
```
```csharp
// File: Models/CreateBudgetModel.cs
using System.ComponentModel.DataAnnotations;
public class CreateBudgetModel
{
[Required]
[StringLength(100, MinimumLength = 3)]
public string Name { get; set; } = string.Empty;
[Required]
[Range(0.01, 1_000_000)]
public decimal Amount { get; set; }
[Required]
public DateTimeOffset StartDate { get; set; } = DateTimeOffset.Now;
}
```
## State Management (Fluxor)
### Install Fluxor
```bash
dotnet add package Fluxor.Blazor.Web
```
### Define State
```csharp
// File: Store/BudgetState/BudgetState.cs
namespace {ApplicationName}.UI.Store.BudgetState;
public record BudgetState
{
public List<BudgetResponse> Budgets { get; init; } = new();
public bool IsLoading { get; init; }
public string? ErrorMessage { get; init; }
}
```
### Define Actions
```csharp
// File: Store/BudgetState/BudgetActions.cs
namespace {ApplicationName}.UI.Store.BudgetState;
public record LoadBudgetsAction;
public record LoadBudgetsSuccessAction(List<BudgetResponse> Budgets);
public record LoadBudgetsFailureAction(string ErrorMessage);
```
### Implement Reducer
```csharp
// File: Store/BudgetState/BudgetReducers.cs
using Fluxor;
namespace {ApplicationName}.UI.Store.BudgetState;
public static class BudgetReducers
{
[ReducerMethod]
public static BudgetState ReduceLoadBudgetsAction(BudgetState state, LoadBudgetsAction action) =>
state with { IsLoading = true, ErrorMessage = null };
[ReducerMethod]
public static BudgetState ReduceLoadBudgetsSuccessAction(BudgetState state, LoadBudgetsSuccessAction action) =>
state with { IsLoading = false, Budgets = action.Budgets };
[ReducerMethod]
public static BudgetState ReduceLoadBudgetsFailureAction(BudgetState state, LoadBudgetsFailureAction action) =>
state with { IsLoading = false, ErrorMessage = action.ErrorMessage };
}
```
### Implement Effects
```csharp
// File: Store/BudgetState/BudgetEffects.cs
using Fluxor;
namespace {ApplicationName}.UI.Store.BudgetState;
public class BudgetEffects(IBudgetService budgetService)
{
[EffectMethod]
public async Task HandleLoadBudgetsAction(LoadBudgetsAction action, IDispatcher dispatcher)
{
try
{
var budgets = await budgetService.GetBudgetsAsync();
dispatcher.Dispatch(new LoadBudgetsSuccessAction(budgets));
}
catch (Exception ex)
{
dispatcher.Dispatch(new LoadBudgetsFailureAction(ex.Message));
}
}
}
```
### Register Fluxor
```csharp
// Program.cs
builder.Services.AddFluxor(options =>
{
options.ScanAssemblies(typeof(Program).Assembly);
options.UseReduxDevTools();
});
```
### Use in Component
```razor
@inherits FluxorComponent
@inject IState<BudgetState> BudgetState
@inject IDispatcher Dispatcher
<div>
@if (BudgetState.Value.IsLoading)
{
<p>Loading...</p>
}
else
{
@foreach (var budget in BudgetState.Value.Budgets)
{
<BudgetCard Budget="@budget" />
}
}
</div>
@code {
protected override void OnInitialized()
{
base.OnInitialized();
Dispatcher.Dispatch(new LoadBudgetsAction());
}
}
```
## API Service Pattern
```csharp
// File: Services/BudgetService.cs
namespace {ApplicationName}.UI.Services;
using System.Net.Http.Json;
public interface IBudgetService
{
Task<List<BudgetResponse>> GetBudgetsAsync(CancellationToken cancellationToken = default);
Task<BudgetResponse> GetBudgetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<CreateBudgetResponse> CreateBudgetAsync(CreateBudgetCommand command, CancellationToken cancellationToken = default);
Task UpdateBudgetAsync(Guid id, UpdateBudgetCommand command, CancellationToken cancellationToken = default);
Task DeleteBudgetAsync(Guid id, CancellationToken cancellationToken = default);
}
public class BudgetService(
HttpClient httpClient,
ILogger<BudgetService> logger
) : IBudgetService
{
public async Task<List<BudgetResponse>> GetBudgetsAsync(
CancellationToken cancellationToken = default)
{
try
{
var budgets = await httpClient.GetFromJsonAsync<List<BudgetResponse>>(
"/budgets",
cancellationToken);
return budgets ?? new List<BudgetResponse>();
}
catch (Exception ex)
{
logger.LogError(ex, "Error fetching budgets");
throw;
}
}
public async Task<BudgetResponse> GetBudgetByIdAsync(
Guid id,
CancellationToken cancellationToken = default)
{
try
{
var budget = await httpClient.GetFromJsonAsync<BudgetResponse>(
$"/budgets/{id}",
cancellationToken);
return budget ?? throw new InvalidOperationException("Budget not found");
}
catch (Exception ex)
{
logger.LogError(ex, "Error fetching budget {BudgetId}", id);
throw;
}
}
public async Task<CreateBudgetResponse> CreateBudgetAsync(
CreateBudgetCommand command,
CancellationToken cancellationToken = default)
{
try
{
var response = await httpClient.PostAsJsonAsync(
"/budgets",
command,
cancellationToken);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<CreateBudgetResponse>(
cancellationToken: cancellationToken);
return result ?? throw new InvalidOperationException("Failed to create budget");
}
catch (Exception ex)
{
logger.LogError(ex, "Error creating budget");
throw;
}
}
public async Task UpdateBudgetAsync(
Guid id,
UpdateBudgetCommand command,
CancellationToken cancellationToken = default)
{
try
{
var response = await httpClient.PutAsJsonAsync(
$"/budgets/{id}",
command,
cancellationToken);
response.EnsureSuccessStatusCode();
}
catch (Exception ex)
{
logger.LogError(ex, "Error updating budget {BudgetId}", id);
throw;
}
}
public async Task DeleteBudgetAsync(
Guid id,
CancellationToken cancellationToken = default)
{
try
{
var response = await httpClient.DeleteAsync(
$"/budgets/{id}",
cancellationToken);
response.EnsureSuccessStatusCode();
}
catch (Exception ex)
{
logger.LogError(ex, "Error deleting budget {BudgetId}", id);
throw;
}
}
}
// Register service
builder.Services.AddScoped<IBudgetService, BudgetService>();
```
## JavaScript Interop
```razor
@inject IJSRuntime JS
<button @onclick="ShowAlert">Show Alert</button>
<button @onclick="GetLocalStorage">Get from LocalStorage</button>
@code {
private async Task ShowAlert()
{
await JS.InvokeVoidAsync("alert", "Hello from Blazor!");
}
private async Task GetLocalStorage()
{
var value = await JS.InvokeAsync<string>("localStorage.getItem", "myKey");
Console.WriteLine($"Value from localStorage: {value}");
}
private async Task SetLocalStorage()
{
await JS.InvokeVoidAsync("localStorage.setItem", "myKey", "myValue");
}
}
```
```javascript
// wwwroot/js/interop.js
window.budgetApp = {
formatCurrency: function (amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount);
},
showConfirmDialog: function (message) {
return confirm(message);
},
downloadFile: function (filename, content) {
const blob = new Blob([content], { type: 'text/plain' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
window.URL.revokeObjectURL(url);
}
};
```
## Authentication in Blazor
```razor
@* File: Pages/Login.razor *@
@page "/login"
@inject NavigationManager Navigation
@inject IAuthenticationService AuthService
<div class="login-page">
<h1>Login</h1>
<EditForm Model="@loginModel" OnValidSubmit="@HandleLogin">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="form-group">
<label>Email:</label>
<InputText @bind-Value="loginModel.Email" class="form-control" />
</div>
<div class="form-group">
<label>Password:</label>
<InputText type="password" @bind-Value="loginModel.Password" class="form-control" />
</div>
<button type="submit" class="btn btn-primary">Login</button>
</EditForm>
</div>
@code {
private LoginModel loginModel = new();
private async Task HandleLogin()
{
var result = await AuthService.LoginAsync(loginModel.Email, loginModel.Password);
if (result.Success)
{
Navigation.NavigateTo("/");
}
}
}
```
```razor
@* File: Components/AuthorizeView.razor *@
@using Microsoft.AspNetCore.Components.Authorization
<AuthorizeView>
<Authorized>
<p>Hello, @context.User.Identity?.Name!</p>
<a href="/logout">Logout</a>
</Authorized>
<NotAuthorized>
<a href="/login">Login</a>
</NotAuthorized>
</AuthorizeView>
```
## Performance Optimization
### Virtualization
```razor
@using Microsoft.AspNetCore.Components.Web.Virtualization
<Virtualize Items="@budgets" Context="budget">
<BudgetCard Budget="@budget" />
</Virtualize>
```
### Lazy Loading
```razor
@* File: App.razor *@
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<p>Sorry, there's nothing at this address.</p>
</NotFound>
</Router>
```
## Common Blazor Pitfalls
### ❌ Avoid These Mistakes
1. **Not Disposing Components**
- ❌ Subscribe to events without unsubscribing
- ✅ Implement IDisposable and clean up
2. **Blocking UI Thread**
- ❌ Using `Task.Result` or `.Wait()`
- ✅ Always use `await`
3. **Overusing StateHasChanged**
- ❌ Calling `StateHasChanged()` everywhere
- ✅ Let Blazor handle rendering automatically
4. **Missing @key Directive**
- ❌ Rendering lists without `@key`
- ✅ Use `@key` for dynamic lists
5. **Not Handling Errors**
- ❌ No error boundaries
- ✅ Use ErrorBoundary component
## Blazor Checklist
### Component Development
- [ ] Components are focused and reusable
- [ ] Parameters use `[Parameter]` attribute
- [ ] EventCallbacks for component events
- [ ] Code-behind for complex logic
- [ ] IDisposable implemented where needed
### Forms & Validation
- [ ] EditForm used for forms
- [ ] Data annotations validation
- [ ] ValidationSummary displayed
- [ ] Submit button disabled during submission
- [ ] Error messages displayed
### State Management
- [ ] Global state managed (Fluxor/FluxState)
- [ ] Component state localized
- [ ] Services injected via DI
- [ ] State changes trigger re-renders
### API Integration
- [ ] HttpClient configured
- [ ] Loading states displayed
- [ ] Error handling implemented
- [ ] Authentication tokens included
- [ ] Retry logic for transient failures
### Performance
- [ ] Virtualization for long lists
- [ ] Lazy loading for routes
- [ ] @key directive on lists
- [ ] Minimal JavaScript interop
- [ ] Component disposal implemented
## Checklist Before Completion
- [ ] All components render correctly
- [ ] Forms validate and submit
- [ ] API calls successful
- [ ] Loading states displayed
- [ ] Error handling functional
- [ ] Authentication working
- [ ] State management functional
- [ ] Performance optimized
- [ ] Responsive design implemented
- [ ] Documentation complete