一、MudBlazor:Material Design组件库
MudBlazor是目前Blazor生态中最受欢迎的UI框架,它将Google Material Design规范完整地实现为Blazor组件,提供了从按钮、输入框到数据表格、对话框的完整组件集,且无需JavaScript依赖------绝大多数功能都是纯C#/Razor实现,与Blazor的编程模型完美融合。
首先安装包并配置:
bash
dotnet add package MudBlazor
在Program.cs中注册MudBlazor服务:
csharp
// Program.cs
builder.Services.AddMudServices(config =>
{
// Snackbar(轻提示)全局配置
config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomRight;
config.SnackbarConfiguration.PreventDuplicates = false;
config.SnackbarConfiguration.VisibleStateDuration = 3000;
});
在App.razor(或Blazor Server的_Host.cshtml对应文件)中添加必要的提供者组件和样式引用:
csharp
@* App.razor *@
<!DOCTYPE html>
<html>
<head>
@* MudBlazor 字体和图标 *@
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
</head>
<body>
@* MudBlazor 需要这三个Provider:主题、对话框、消息提示 *@
<MudThemeProvider @ref="@_mudThemeProvider" @bind-IsDarkMode="@_isDarkMode" Theme="_customTheme" />
<MudDialogProvider />
<MudSnackbarProvider />
<Routes />
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
</body>
</html>
@code {
private MudThemeProvider _mudThemeProvider = null!;
private bool _isDarkMode = false;
// 自定义主题:覆盖MudBlazor的默认调色板
private readonly MudTheme _customTheme = new()
{
PaletteLight = new PaletteLight
{
Primary = "#6200EA", // 主色调:深紫色
Secondary = "#03DAC6", // 辅助色:青色
AppbarBackground = "#6200EA", // 顶部导航栏颜色
},
PaletteDark = new PaletteDark
{
Primary = "#BB86FC",
}
};
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// 跟随浏览器/系统的深色模式偏好
_isDarkMode = await _mudThemeProvider.GetSystemPreference();
StateHasChanged();
}
}
}
1.1 MudDataGrid 数据表格
MudBlazor的MudDataGrid是功能最完整的组件之一,内置排序、筛选、分页和行选择:
csharp
@* Pages/Products.razor *@
@page "/products"
@using MudBlazor
@inject ProductApiClient ApiClient
@inject IDialogService DialogService
@inject ISnackbar Snackbar
<MudText Typo="Typo.h4" Class="mb-4">产品管理</MudText>
<MudDataGrid T="ProductDto"
@ref="_dataGrid"
ServerData="LoadServerData"
Filterable="true"
SortMode="SortMode.Multiple"
Hover="true"
Striped="true"
FixedHeader="true"
Height="600px">
<ToolBarContent>
<MudText Typo="Typo.h6">产品列表</MudText>
<MudSpacer />
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.Add"
OnClick="OpenCreateDialog">
新增产品
</MudButton>
</ToolBarContent>
<Columns>
@* 文字列:启用排序和筛选 *@
<PropertyColumn Property="x => x.Name" Title="产品名称"
Sortable="true" Filterable="true" />
<PropertyColumn Property="x => x.Price" Title="价格(元)"
Format="C2" CellStyle="text-align:right" />
<PropertyColumn Property="x => x.Stock" Title="库存" />
@* 自定义列:操作按钮 *@
<TemplateColumn Title="操作" CellStyle="width:150px">
<CellTemplate>
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Color="Color.Primary"
Size="Size.Small"
OnClick="@(() => OpenEditDialog(context.Item))" />
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
Size="Size.Small"
OnClick="@(() => DeleteProduct(context.Item))" />
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="ProductDto" PageSizeOptions="new[] { 10, 25, 50 }" />
</PagerContent>
</MudDataGrid>
@code {
private MudDataGrid<ProductDto> _dataGrid = null!;
// ServerData回调由DataGrid在排序、翻页、筛选时自动调用
private async Task<GridData<ProductDto>> LoadServerData(GridState<ProductDto> state)
{
// state.Page:当前页码(0起始)
// state.PageSize:每页条数
// state.SortDefinitions:排序字段和方向列表
// state.FilterDefinitions:筛选条件列表
var result = await ApiClient.GetProductsAsync(
page: state.Page + 1,
pageSize: state.PageSize,
sortField: state.SortDefinitions.FirstOrDefault()?.SortBy,
ascending: state.SortDefinitions.FirstOrDefault()?.Descending == false);
return new GridData<ProductDto>
{
Items = result.Items,
TotalItems = result.TotalCount
};
}
private async Task OpenCreateDialog()
{
var options = new DialogOptions
{
CloseOnEscapeKey = true,
MaxWidth = MaxWidth.Medium,
FullWidth = true
};
var dialog = await DialogService.ShowAsync<ProductEditDialog>(
"新增产品", options);
var result = await dialog.Result;
// 如果对话框不是取消关闭(用户点了保存),刷新表格
if (!result!.Canceled)
{
await _dataGrid.ReloadServerData();
Snackbar.Add("产品创建成功", Severity.Success);
}
}
private async Task DeleteProduct(ProductDto product)
{
bool confirmed = await DialogService.ShowMessageBox(
"确认删除",
$"确定要删除产品 [{product.Name}] 吗?此操作不可撤销。",
yesText: "删除", cancelText: "取消") ?? false;
if (confirmed)
{
await ApiClient.DeleteProductAsync(product.Id);
await _dataGrid.ReloadServerData();
Snackbar.Add($"产品 {product.Name} 已删除", Severity.Info);
}
}
private Task OpenEditDialog(ProductDto product) =>
OpenCreateDialog(); // 实际应传入product参数,此处简化
}
二、Ant Design Blazor:企业级组件库
Ant Design Blazor将蚂蚁集团的Ant Design设计语言引入Blazor,适合构建中后台管理系统,在国内企业项目中使用广泛。
bash
dotnet add package AntDesign
csharp
// Program.cs
builder.Services.AddAntDesign();
csharp
@* _Imports.razor *@
@using AntDesign
在App.razor添加必要的组件:
razor
<AntContainer /> @* 全局消息、通知等的挂载点 *@
Ant Design Blazor与MudBlazor最大的区别在于组件命名风格(前缀Ant)和设计语言(更扁平、更商务),但核心概念相似------服务注入(IMessageService、INotificationService、IModalService)替代提供者组件:
csharp
@* 使用 Ant Design 表单示例 *@
@page "/ant-demo"
@inject IMessageService Message
<Form Model="@formModel"
OnFinish="OnSubmit"
OnFinishFailed="OnError"
LabelCol="new ColLayoutParam { Span = 6 }"
WrapperCol="new ColLayoutParam { Span = 18 }">
<FormItem Label="产品名称" Required>
<Input @bind-Value="@formModel.Name" Placeholder="请输入产品名称" />
</FormItem>
<FormItem Label="价格">
<AntDesign.InputNumber @bind-Value="@formModel.Price"
Min="0.01m" Step="0.01m"
Formatter="value => $"¥{value:N2}"" />
</FormItem>
<FormItem WrapperCol="new ColLayoutParam { Offset = 6, Span = 18 }">
<Button Type="ButtonType.Primary" HtmlType="submit">提交</Button>
</FormItem>
</Form>
@code {
private ProductForm formModel = new();
private async Task OnSubmit(EditContext context)
{
await Message.Success("保存成功!");
}
private async Task OnError(EditContext context)
{
await Message.Error("请检查表单填写是否完整。");
}
record ProductForm
{
public string Name { get; set; } = "";
public decimal Price { get; set; } = 1.0m;
}
}
三、构建可复用自定义组件库
当项目达到一定规模,或多个项目共用同一套UI规范时,将通用组件提取到**Razor类库(Razor Class Library,RCL)**是最佳实践。
bash
# 创建RCL项目
dotnet new razorclasslib -o MyCompany.UI.Components
# 引用到主项目
dotnet add MyBlazorApp/MyBlazorApp.csproj reference MyCompany.UI.Components/MyCompany.UI.Components.csproj
RCL的_Imports.razor中添加全局引用,使消费方项目无需在每个组件中手动using:
csharp
@* MyCompany.UI.Components/_Imports.razor *@
@using Microsoft.AspNetCore.Components.Web
@using MyCompany.UI.Components
@using MyCompany.UI.Components.Models
以一个支持HeaderTemplate、BodyContent和FooterTemplate三个插槽的通用Card组件为例:
csharp
@* MyCompany.UI.Components/Components/Card/AppCard.razor *@
<div class="app-card @CssClass">
@* 可选标题插槽:如果没有传入HeaderTemplate则不渲染标题区域 *@
@if (HeaderTemplate is not null)
{
<div class="app-card__header">
@HeaderTemplate
</div>
}
<div class="app-card__body">
@* ChildContent是Blazor的约定名称,对应<AppCard>标签体的内容 *@
@ChildContent
</div>
@* 可选脚注插槽 *@
@if (FooterTemplate is not null)
{
<div class="app-card__footer">
@FooterTemplate
</div>
}
</div>
@code {
/// <summary>附加CSS类,允许消费方扩展样式</summary>
[Parameter] public string? CssClass { get; set; }
/// <summary>标题区插槽(可选)</summary>
[Parameter] public RenderFragment? HeaderTemplate { get; set; }
/// <summary>主体内容插槽(必需)</summary>
[Parameter] public RenderFragment? ChildContent { get; set; }
/// <summary>脚注插槽(可选)</summary>
[Parameter] public RenderFragment? FooterTemplate { get; set; }
}
使用泛型参数的组件(RenderFragment<T>)可以实现更灵活的模板:
csharp
@* 泛型列表组件:消费方决定每行如何渲染 *@
@typeparam TItem
<div class="app-list">
@if (Items is null || !Items.Any())
{
<div class="app-list__empty">
@(EmptyContent ?? (RenderFragment)(@<p>暂无数据</p>))
</div>
}
else
{
@foreach (var item in Items)
{
@* ItemTemplate是 RenderFragment<TItem>,调用时传入 item 作为 context *@
<div class="app-list__item">
@ItemTemplate(item)
</div>
}
}
</div>
@code {
[Parameter, EditorRequired] public IEnumerable<TItem>? Items { get; set; }
/// <summary>每条数据的渲染模板,context 是 TItem 实例</summary>
[Parameter, EditorRequired] public RenderFragment<TItem> ItemTemplate { get; set; } = null!;
/// <summary>空状态渲染内容(可选)</summary>
[Parameter] public RenderFragment? EmptyContent { get; set; }
}
消费方使用泛型列表组件时这样写:
csharp
<AppList Items="@products" TItem="ProductDto">
<ItemTemplate Context="product">
<strong>@product.Name</strong>
<span>¥@product.Price</span>
</ItemTemplate>
<EmptyContent>
<p>还没有添加任何产品,点击右上角按钮开始创建。</p>
</EmptyContent>
</AppList>
将RCL打包为NuGet包非常简单,在.csproj中添加必要的元数据:
xml
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<!-- NuGet包信息 -->
<PackageId>MyCompany.UI.Components</PackageId>
<Version>1.0.0</Version>
<Authors>MyCompany</Authors>
<Description>MyCompany内部Blazor UI组件库</Description>
<!-- 将 wwwroot 静态资源打包进NuGet *-->
<StaticWebAssetBasePath>/_content/MyCompany.UI.Components</StaticWebAssetBasePath>
</PropertyGroup>
...
</Project>
四、总结
本章从MudBlazor的Material Design美学出发,展示了如何通过AddMudServices、三个Provider组件和自定义主题来快速搭建风格统一的应用界面,MudDataGrid的ServerData机制让服务端分页、排序、筛选只需一个回调即可完成;Ant Design Blazor则以国内企业更熟悉的扁平设计风格提供了配套服务(IMessageService、IModalService);而基于RCL的自定义组件库------利用RenderFragment、RenderFragment<T>和[Parameter]构建带多个插槽的可组合组件------是大型项目实现UI一致性、提高跨项目复用率的关键路径。
Blazor不仅是Web框架,它的代码和组件还可以运行在.NET MAUI中,通过BlazorWebView控件让Razor组件直接渲染在原生iOS、Android和Windows桌面应用里。下一章,我们将探索.NET MAUI与Blazor的混合应用开发模式,看看如何通过Razor类库在Web和移动端之间最大化代码复用。