17. 【Blazor全栈开发实战指南】--Blazor UI框架集成

一、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)和设计语言(更扁平、更商务),但核心概念相似------服务注入(IMessageServiceINotificationServiceIModalService)替代提供者组件:

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

以一个支持HeaderTemplateBodyContentFooterTemplate三个插槽的通用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组件和自定义主题来快速搭建风格统一的应用界面,MudDataGridServerData机制让服务端分页、排序、筛选只需一个回调即可完成;Ant Design Blazor则以国内企业更熟悉的扁平设计风格提供了配套服务(IMessageServiceIModalService);而基于RCL的自定义组件库------利用RenderFragmentRenderFragment<T>[Parameter]构建带多个插槽的可组合组件------是大型项目实现UI一致性、提高跨项目复用率的关键路径。

Blazor不仅是Web框架,它的代码和组件还可以运行在.NET MAUI中,通过BlazorWebView控件让Razor组件直接渲染在原生iOS、Android和Windows桌面应用里。下一章,我们将探索.NET MAUI与Blazor的混合应用开发模式,看看如何通过Razor类库在Web和移动端之间最大化代码复用。

相关推荐
没有bug.的程序员2 小时前
500个微服务上云全线假死:Spring Boot 3.2 自动配置底层的生死狙击
java·spring boot·微服务·kubernetes·自动配置
renhongxia13 小时前
人工智能代理能生成微服务吗?我们离多远了?
人工智能·深度学习·学习·微服务·云原生·架构·机器人
2501_941149503 小时前
2026 级微服务演进:深度解析 Cosvice 架构下的服务编排与性能调优
微服务·云原生·架构
JamesYoung79714 小时前
第八部分 — UI 表面 sidePanel (如使用) + UX约束
前端·javascript·ui·ux
我是唐青枫4 小时前
C#.NET 源生成器 深入解析:编译时代码生成与增量生成器实战
c#·.net
Hody915 小时前
【XR开发系列】UI 入门 - 创建一个简单的分数显示
ui·xr
唐青枫5 小时前
C#.NET Pipelines 深入解析:高性能 IO 管道与零拷贝协议处理实战
c#·.net
毕设源码-郭学长5 小时前
【开题答辩全过程】以 基于.NET MVC的乡村综合信息化 管理系统设计与实现为例,包含答辩的问题和答案
mvc·.net
江沉晚呤时5 小时前
C# 接口默认实现与依赖注入实战指南:.NET 9 企业级开发高级技巧
c#·log4j·.net·.netcore