Quick reference for Blazor hosting models (Server, WebAssembly, .NET 8+ render modes), component structure, parameters, and lifecycle methods.
npx claudepluginhub fortiumpartners/ai-meshThis skill uses the workspace's default tool permissions.
**Version**: 1.0.0
PATTERNS-EXTRACTED.mdREADME.mdREFERENCE.mdVALIDATION.mdexamples/README.mdexamples/realtime-dashboard.example.razorexamples/todo-app.example.razortemplates/component.template.razortemplates/form.template.razortemplates/layout.template.razortemplates/page.template.razortemplates/service.template.cstemplates/test.template.csGuides setup and usage of Fluent UI Blazor components in .NET Blazor apps, including providers, services, icons, list bindings, and troubleshooting JS interop or theming.
Write UI tests for Blazor Server/WebAssembly apps using Playwright. Covers navigation, interactions, authentication, selectors, SignalR updates, screenshots, and responsive testing.
Creates new Angular apps using Angular CLI with flags for routing, SSR, SCSS, prefixes, and AI config. Follows best practices for modern TypeScript/Angular development. Use when starting Angular projects.
Share bugs, ideas, or general feedback.
Version: 1.0.0 Target: .NET 8.0+ with Blazor Server/WebAssembly UI Library: Microsoft Fluent UI Blazor Components Purpose: Fast lookup for common Blazor patterns and best practices
Characteristics:
Setup:
// Program.cs
builder.Services.AddServerSideBlazor();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
Use Cases: Internal apps, intranet, enterprise applications
Characteristics:
Setup:
// Program.cs (Client)
builder.Services.AddBlazorWebAssembly();
await builder.Build().RunAsync();
Use Cases: Public-facing apps, PWAs, offline-first applications
@* Server-side with SignalR *@
@rendermode InteractiveServer
@* Client-side WebAssembly *@
@rendermode InteractiveWebAssembly
@* Auto: Server initially, then WebAssembly after download *@
@rendermode InteractiveAuto
@* Static: Server-side rendering without interactivity *@
@rendermode @(new Microsoft.AspNetCore.Components.Web.RenderMode.Static)
@page "/counter"
@using MyApp.Services
<h3>@Title</h3>
<p>Current count: @currentCount</p>
<button @onclick="IncrementCount">Click me</button>
@code {
[Parameter]
public string Title { get; set; } = "Counter";
[Parameter]
public EventCallback<int> OnCountChanged { get; set; }
private int currentCount = 0;
private async Task IncrementCount()
{
currentCount++;
await OnCountChanged.InvokeAsync(currentCount);
}
}
// Required parameter
[Parameter, EditorRequired]
public string Name { get; set; } = "";
// Optional parameter with default
[Parameter]
public int MaxCount { get; set; } = 10;
// EventCallback (parent-child communication)
[Parameter]
public EventCallback<string> OnValueChanged { get; set; }
// Cascading parameter (from ancestor)
[CascadingParameter]
public AppState AppState { get; set; } = default!;
// Capture unmatched attributes
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary<string, object>? AdditionalAttributes { get; set; }
// 1. Parameters set (before initialization)
public override void SetParametersAsync(ParameterView parameters)
{
base.SetParametersAsync(parameters);
}
// 2. Component initialization (once)
protected override void OnInitialized()
{
// Synchronous initialization
}
protected override async Task OnInitializedAsync()
{
// Async initialization (preferred for data loading)
data = await DataService.GetDataAsync();
}
// 3. Parameters set (every time parameters change)
protected override void OnParametersSet()
{
// React to parameter changes
}
protected override async Task OnParametersSetAsync()
{
// Async parameter processing
}
// 4. After rendering
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
// One-time setup after first render
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// JS interop, focus management
await JS.InvokeVoidAsync("initializeComponent");
}
}
// 5. Determine if re-render needed
protected override bool ShouldRender()
{
// Return false to skip re-render
return true;
}
// 6. Disposal (cleanup)
public void Dispose()
{
// Unsubscribe from events, dispose resources
AppState.OnChange -= StateHasChanged;
}
public async ValueTask DisposeAsync()
{
// Async disposal
if (hubConnection is not null)
{
await hubConnection.DisposeAsync();
}
}
@using Microsoft.FluentUI.AspNetCore.Components
@* Vertical stack with gap *@
<FluentStack Orientation="Orientation.Vertical" VerticalGap="16">
<h1>Title</h1>
<p>Content</p>
</FluentStack>
@* Horizontal stack *@
<FluentStack Orientation="Orientation.Horizontal" HorizontalGap="8">
<FluentButton>Cancel</FluentButton>
<FluentButton Appearance="Appearance.Accent">Save</FluentButton>
</FluentStack>
@* Grid layout *@
<FluentGrid Spacing="16">
<FluentGridItem xs="12" sm="6" md="4">
<FluentCard>Column 1</FluentCard>
</FluentGridItem>
<FluentGridItem xs="12" sm="6" md="4">
<FluentCard>Column 2</FluentCard>
</FluentGridItem>
</FluentGrid>
@* Text input *@
<FluentTextField
@bind-Value="name"
Label="Name"
Placeholder="Enter your name"
Required="true" />
@* Text area *@
<FluentTextArea
@bind-Value="description"
Label="Description"
Rows="4" />
@* Number input *@
<FluentNumberField
@bind-Value="age"
Label="Age"
Min="0"
Max="150" />
@* Select dropdown *@
<FluentSelect
@bind-Value="selectedOption"
Label="Choose option"
Items="@options"
OptionText="@(x => x.Name)"
OptionValue="@(x => x.Id)" />
@* Checkbox *@
<FluentCheckbox
@bind-Value="accepted"
Label="I accept the terms" />
@* Switch toggle *@
<FluentSwitch
@bind-Value="isEnabled"
Label="Enable feature" />
@* Date picker *@
<FluentDatePicker
@bind-Value="selectedDate"
Label="Select date" />
@* Primary button *@
<FluentButton Appearance="Appearance.Accent" OnClick="Submit">
Submit
</FluentButton>
@* Secondary button *@
<FluentButton Appearance="Appearance.Neutral">
Cancel
</FluentButton>
@* Icon button *@
<FluentIconButton
Icon="@(new Icons.Regular.Size20.Add())"
aria-label="Add item"
OnClick="AddItem" />
@* Split button *@
<FluentSplitButton Text="Actions">
<FluentMenuItem OnClick="Edit">Edit</FluentMenuItem>
<FluentMenuItem OnClick="Delete">Delete</FluentMenuItem>
</FluentSplitButton>
@* Card *@
<FluentCard>
<h3>Card Title</h3>
<p>Card content goes here.</p>
</FluentCard>
@* Data grid with sorting *@
<FluentDataGrid Items="@users" Virtualize="true">
<PropertyColumn Property="@(u => u.Name)" Sortable="true" />
<PropertyColumn Property="@(u => u.Email)" />
<PropertyColumn Property="@(u => u.CreatedAt)" Format="yyyy-MM-dd" Sortable="true" />
<TemplateColumn Title="Actions">
<FluentButton OnClick="@(() => EditUser(context))">Edit</FluentButton>
</TemplateColumn>
</FluentDataGrid>
@* Accordion *@
<FluentAccordion>
<FluentAccordionItem Heading="Section 1">
Content for section 1
</FluentAccordionItem>
<FluentAccordionItem Heading="Section 2">
Content for section 2
</FluentAccordionItem>
</FluentAccordion>
@* Dialog *@
<FluentDialog @bind-Hidden="hideDialog" Modal="true">
<h2>Confirmation</h2>
<p>Are you sure you want to delete this item?</p>
<FluentButton Appearance="Appearance.Accent" OnClick="ConfirmDelete">
Yes, Delete
</FluentButton>
<FluentButton OnClick="@(() => hideDialog = true)">
Cancel
</FluentButton>
</FluentDialog>
@* Message bar (notification) *@
<FluentMessageBar Intent="MessageIntent.Success" Visible="@showSuccess">
Item saved successfully!
</FluentMessageBar>
@* Progress ring (loading spinner) *@
<FluentProgressRing Visible="@isLoading" />
@* Toast notification *@
@inject IToastService ToastService
@code {
private void ShowToast()
{
ToastService.ShowSuccess("Operation completed successfully!");
}
}
@code {
// Private field for component-local state
private int count = 0;
private string message = "";
private List<Item> items = new();
// State change triggers re-render automatically
private void UpdateState()
{
count++;
// Component re-renders automatically
}
}
// 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<AppState>();
// 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
}
}
@* App.razor or parent component *@
<CascadingValue Value="@currentTheme">
<Router AppAssembly="@typeof(Program).Assembly">
@* Child components *@
</Router>
</CascadingValue>
@code {
private string currentTheme = "light";
}
@* Child component *@
@code {
[CascadingParameter]
public string Theme { get; set; } = "";
// Access Theme without prop drilling
}
<EditForm Model="@person" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<FluentTextField @bind-Value="person.Name" Label="Name" />
<ValidationMessage For="@(() => person.Name)" />
<FluentNumberField @bind-Value="person.Age" Label="Age" />
<ValidationMessage For="@(() => person.Age)" />
<FluentButton Type="ButtonType.Submit" Appearance="Appearance.Accent">
Submit
</FluentButton>
</EditForm>
@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; } = "";
}
}
// Install: FluentValidation.Blazor
using FluentValidation;
public class PersonValidator : AbstractValidator<Person>
{
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
<EditForm Model="@person" OnValidSubmit="HandleValidSubmit">
<FluentValidationValidator />
<ValidationSummary />
@* Form fields *@
</EditForm>
// 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; }
}
@* 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}"
@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"];
}
<FluentNavMenu>
@* Exact match (active only for exact path) *@
<FluentNavLink Href="/" Match="NavLinkMatch.All">
Home
</FluentNavLink>
@* Prefix match (active for path and descendants) *@
<FluentNavLink Href="/products" Match="NavLinkMatch.Prefix">
Products
</FluentNavLink>
</FluentNavMenu>
@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<string>("localStorage.getItem", "key");
// Call custom function
await JS.InvokeVoidAsync("myApp.initialize", elementRef);
}
}
}
JavaScript file (wwwroot/js/app.js):
window.myApp = {
initialize: function(element) {
console.log("Initializing", element);
},
getData: function() {
return { value: 42 };
}
};
// Component
public class InteropComponent : ComponentBase
{
private DotNetObjectReference<InteropComponent>? 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<string> GetDataFromBlazor()
{
return Task.FromResult("Data from Blazor");
}
public void Dispose()
{
dotNetHelper?.Dispose();
}
}
JavaScript:
// Call static method
DotNet.invokeMethodAsync('MyApp', 'StaticMethod', 'arg1');
// Call instance method
dotNetHelper.invokeMethodAsync('CallMeFromJS', 'Hello');
<nav aria-label="Main navigation">
<FluentNavMenu>
<FluentNavLink Href="/">Home</FluentNavLink>
<FluentNavLink Href="/about">About</FluentNavLink>
</FluentNavMenu>
</nav>
<main>
<h1>Page Title</h1>
<article>
<h2>Article Heading</h2>
<p>Content...</p>
</article>
</main>
<aside aria-label="Sidebar">
<h3>Related Links</h3>
<ul>
<li><a href="/link1">Link 1</a></li>
</ul>
</aside>
@* Button with accessible label *@
<FluentIconButton
Icon="@(new Icons.Regular.Size20.Delete())"
aria-label="Delete item"
OnClick="DeleteItem" />
@* Form with descriptive text *@
<FluentTextField
@bind-Value="email"
Label="Email"
aria-describedby="email-help" />
<span id="email-help">We'll never share your email.</span>
@* Hidden decorative icon *@
<FluentIcon Icon="Icons.Regular.Size20.Info" aria-hidden="true" />
@* Live region for dynamic updates *@
<div aria-live="polite" aria-atomic="true">
@statusMessage
</div>
@* All Fluent UI components support keyboard navigation *@
@* Custom component with keyboard support *@
<div
tabindex="0"
@onkeydown="HandleKeyDown"
role="button"
aria-label="Custom button">
Click or press Enter
</div>
@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();
}
}
}
// 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>("/notificationhub");
@inject NavigationManager Navigation
@implements IAsyncDisposable
<h3>Real-Time Notifications</h3>
@foreach (var notification in notifications)
{
<FluentCard>@notification</FluentCard>
}
@code {
private HubConnection? hubConnection;
private List<string> notifications = new();
protected override async Task OnInitializedAsync()
{
hubConnection = new HubConnectionBuilder()
.WithUrl(Navigation.ToAbsoluteUri("/notificationhub"))
.WithAutomaticReconnect()
.Build();
hubConnection.On<string, string>("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();
}
}
}
using Bunit;
using FluentAssertions;
using Xunit;
public class CounterTests : TestContext
{
[Fact]
public void Counter_StartsAtZero()
{
// Arrange & Act
var cut = RenderComponent<Counter>();
// Assert
cut.Find("p").TextContent.Should().Contain("count: 0");
}
[Fact]
public void Counter_Increments_OnButtonClick()
{
// Arrange
var cut = RenderComponent<Counter>();
var button = cut.Find("button");
// Act
button.Click();
// Assert
cut.Find("p").TextContent.Should().Contain("count: 1");
}
}
[Fact]
public void Component_Renders_WithParameters()
{
// Arrange & Act
var cut = RenderComponent<MyComponent>(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");
}
[Fact]
public void Component_Invokes_Callback()
{
// Arrange
var callbackInvoked = false;
var cut = RenderComponent<MyComponent>(parameters => parameters
.Add(p => p.OnClick, () => callbackInvoked = true));
// Act
cut.Find("button").Click();
// Assert
callbackInvoked.Should().BeTrue();
}
[Fact]
public void Component_Uses_InjectedService()
{
// Arrange
var mockService = new Mock<IDataService>();
mockService.Setup(s => s.GetData()).Returns(new List<Item> { new() { Name = "Test" } });
Services.AddSingleton(mockService.Object);
// Act
var cut = RenderComponent<DataComponent>();
// Assert
cut.FindAll("li").Should().HaveCount(1);
cut.Find("li").TextContent.Should().Be("Test");
}
| Pattern | Key Concept | Example |
|---|---|---|
| Component Parameters | [Parameter] property | Parent-to-child data flow |
| EventCallback | EventCallback<T> parameter | Child-to-parent communication |
| Cascading Values | <CascadingValue> + [CascadingParameter] | Share data down tree |
| State Service | Scoped service with events | Global state management |
| EditForm | <EditForm Model> | Form with validation |
| JavaScript Interop | IJSRuntime.InvokeAsync | Call JS from Blazor |
| SignalR | HubConnection | Real-time communication |
| bUnit Testing | RenderComponent<T>() | Component unit tests |
Dispose()/DisposeAsync()OnInitializedAsync over synchronous methodsWithAutomaticReconnect()Next: See REFERENCE.md for comprehensive guides, advanced patterns, and production deployment strategies.