C# 13 和 .NET 9 全知道 :14 使用 Blazor 构建交互式 Web 组件 (1)
本章介绍如何使用 Blazor 构建交互式网页用户界面组件。您将学习如何构建可以在网页服务器或网页浏览器中执行其 C# 和 .NET 代码的 Blazor 组件。
当组件在服务器上执行时,Blazor 使用 SignalR 将必要的更新传达给浏览器中的用户界面。
当组件在浏览器中使用 WebAssembly 执行时,它们必须进行 HTTP 调用以与服务器上的数据进行交互。您将在第 15 章《构建和使用 Web 服务》中了解更多信息。
在本章中,我们将涵盖以下主题:
- 审查 Blazor Web 应用程序项目模板
- 使用 Blazor 构建组件
- 使用 EditForm 组件定义表单
审查 Blazor Web 应用程序项目模板
在 .NET 8 之前,不同的托管模型有各自的项目模板,例如 Blazor Server 应用、Blazor WebAssembly 应用和 Blazor WebAssembly 空应用。.NET 8 引入了一个统一的项目模板,名为 Blazor Web 应用,以及一个仅客户端的项目模板,重命名为 Blazor WebAssembly 独立应用。除非必须使用旧版 .NET SDK,否则请避免使用其他遗留项目模板。
创建 Blazor Web 应用程序项目
让我们看看 Blazor Web 应用项目的默认模板。大多数情况下,您会发现它与 ASP.NET Core 空模板相同,只是增加了一些关键内容:
- 使用您首选的代码编辑器打开 ModernWeb 解决方案,然后添加一个新项目,如下列表所定义:项目模板:Blazor Web 应用 / blazor --interactivity Auto解决方案文件和文件夹: ModernWeb项目文件和文件夹: Northwind.Blazor认证类型:无配置为 HTTPS:已选择交互式渲染模式:自动(服务器和 WebAssembly)交互位置:每页/组件包含样本页面:已选择请勿使用顶级语句:已清除
如果您正在使用 VS Code 或 Rider,请在命令提示符或终端中输入以下命令: dotnet new blazor --interactivity Auto -o Northwind.Blazor
良好实践:默认的交互式渲染模式是服务器。我们明确选择了自动,以便在此项目中查看两种渲染模式。我们还选择了包含示例页面,这在实际项目中您可能不希望包含。
- 注意创建了两个项目:Northwind.Blazor : 这是主要的 ASP.NET Core 项目,定义并运行静态 SSR、流式传输和服务器端 Blazor 组件。它还引用并托管您的客户端 WebAssembly Blazor 组件。Northwind.Blazor.Client : 这是一个 Blazor WebAssembly 项目,用于您定义的任何客户端组件。在未来,它可能不需要在单独的项目中,但对于 .NET 8 和 .NET 9,它仍然需要。
- 在 ModernWeb 文件夹中,在 Directory.Packages.props ,添加一个 <ItemGroup> 以设置服务器端托管的版本号和定义 Blazor WebAssembly 包,如下所示的标记:
<ItemGroup Label="For Blazor.">
<PackageVersion Include=
"Microsoft.AspNetCore.Components.WebAssembly.Server"
Version="9.0.0" />
<PackageVersion Include=
"Microsoft.AspNetCore.Components.WebAssembly"
Version="9.0.0" />
</ItemGroup>
- 在 Northwind.Blazor.csproj 中,请注意它与使用 Web SDK 并针对.NET 9 的 ASP.NET Core 项目是相同的。还请注意,它引用了客户端项目。
- 在 Northwind.Blazor.csproj 中,删除允许该项目托管 WebAssembly 组件的 Microsoft.AspNetCore.Components.WebAssembly.Server 包的版本号,如下所示:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Northwind.Blazor.Client\Northwind.Blazor.Client.csproj" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" />
</ItemGroup>
</Project>
- 在 Northwind.Blazor.Client.csproj 中,请注意它类似于 ASP.NET Core 项目,但使用 Blazor WebAssembly SDK。
- 在 Northwind.Blazor.Client.csproj 中,删除允许该项目定义 WebAssembly 组件的 Microsoft.AspNetCore.Components.WebAssembly 包的版本号,如下标记所示:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" />
</ItemGroup>
</Project>
- 构建 Northwind.Blazor 和 Northwind.Blazor.Client 项目。
- 在 Northwind.Blazor 中,请注意 Program.cs 几乎与 ASP.NET Core 项目相同。一个不同之处在于配置服务的部分,它调用了 AddRazorComponents 方法,这在我们的 Northwind.Web 项目中也有。该部分还调用以启用服务器和客户端的交互性,如以下代码中所示的高亮部分:
using Northwind.Blazor.Client.Pages;
using Northwind.Blazor.Components;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddInteractiveWebAssemblyComponents();
var app = builder.Build();
请注意配置 HTTP 管道的部分,该部分调用了 MapRazorComponents<App> 方法。这配置了一个根应用程序组件,名称为 App.razor ,如下代码所示:
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseWebAssemblyDebugging();
}
else
{
app.UseExceptionHandler(
"/Error", createScopeForErrors: true);
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseAntiforgery();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode()
.AddInteractiveWebAssemblyRenderMode()
.AddAdditionalAssemblies(
typeof(Northwind.Blazor.Client._Imports).Assembly);
app.Run();
- 在 Northwind.Blazor 中,请注意 Components 文件夹及其子文件夹,如 Layout 和 Pages ,使用了您在启用 Blazor 组件时在 Northwind.Web 项目中使用的相同命名约定。
- 在 Northwind.Blazor.Client 中,在 Program.cs 中,请注意它创建了一个 WebAssemblyHostBuilder 而不是正常的 WebApplication 构建器,如以下代码中突出显示的内容所示:
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
await builder.Build().RunAsync();
- 在 Northwind.Blazor.Client 中,在 Pages 文件夹中,请注意有一个名为 Counter.razor 的 Blazor 组件。
审查 Blazor 路由、布局和导航
让我们回顾一下这个 Blazor 项目的路由配置、布局和导航菜单:
- 在 Northwind.Blazor 项目文件夹中,在 Components 文件夹内,在 App.razor 中,请注意它定义了基本的 HTML 页面标记,该标记引用了本地的 Bootstrap 副本用于样式,以及一些 Blazor 特定的元素,如下标记中突出显示的内容和标记后面列表中提到的内容:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport"
content="width=device-width, initial-scale=1.0 " />
<base href="/" />
<link rel="stylesheet"
href="@Assets["bootstrap/bootstrap.min.css"]" />
<link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet"
href="@Assets["Northwind.Blazor.styles.css"]" />
<ImportMap />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes />
<script src="_framework/blazor.web.js"></script>
</body>
</html>
在审查前面的标记时,请注意以下事项:
- 资产通过 ComponentBase.Assets 属性进行引用,该属性解析给定资产的指纹 URL。当您在 Program.cs 中使用 MapStaticAssets 中间件时,应使用此属性。
- 一个 <ImportMap /> Blazor 组件,用于表示一个导入映射元素 ( <script type="importmap"></script> ),该元素定义了模块脚本的导入映射。您可以通过以下链接了解导入映射: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap。
- 一个 <HeadOutlet /> Blazor 组件,用于将额外内容注入到 <head> 部分。这是所有 Blazor 项目中可用的内置组件之一。例如,在 Blazor 页面组件中,使用 <PageTitle> 组件来设置网页的 <title> 。
- 一个 <Routes /> Blazor 组件,用于在此项目中定义自定义路由。由于该组件是当前项目的一部分,因此开发人员可以完全自定义它,文件名为 Routes.razor 。
- 一个用于 blazor.web.js 的脚本块,管理与服务器的通信,以支持 Blazor 的动态功能,例如在后台下载 WebAssembly 组件,并随后从服务器端切换到客户端组件执行。
- 在 Components 文件夹中,在 Routes.razor ,请注意 <Router> 为当前项目程序集或 Northwind.Blazor.Client 项目程序集(针对 WebAssembly 组件)中的所有 Blazor 组件启用路由,如果找到匹配的路由,则执行 RouteView ,这将把组件的默认布局设置为 MainLayout 并将任何路由数据参数传递给组件。对于该组件,将聚焦于其中的第一个 <h1> 元素,如以下代码所示:
<Router AppAssembly="@typeof(Program).Assembly"
AdditionalAssemblies="new[] {
typeof(Client._Imports).Assembly }">
<Found Context="routeData">
<RouteView RouteData="@routeData"
DefaultLayout="@typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
</Router>
在 Components 文件夹中,在 _Imports.razor ,请注意此文件导入了一些在您所有自定义 Blazor 组件中使用的有用命名空间,如下代码所示:
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using Northwind.Blazor
@using Northwind.Blazor.Client
@using Northwind.Blazor.Components
在 Components\Layout 文件夹中,在 MainLayout.razor ,请注意它为侧边栏定义了 <div> ,该侧边栏包含一个由本项目中的 NavMenu.razor 组件文件实现的导航菜单,以及用于内容的 HTML5 元素,如 <main> 和 <article> ,并有一个 <div> 来显示未处理的错误,如以下代码所示:
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/"
target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss"></span>
</div>
- 在 Components\Layout 文件夹中的 MainLayout.razor.css ,请注意它包含该组件的独立 CSS 样式。由于命名约定,在此文件中定义的样式优先于其他可能影响该组件的样式。
Blazor 组件通常需要提供自己的 CSS 以应用样式或 JavaScript 以执行无法仅通过 C# 完成的活动,例如访问浏览器 API。为了确保这不会与站点级别的 CSS 和 JavaScript 冲突,Blazor 支持 CSS 和 JavaScript 隔离。如果您有一个名为 Home.razor 的组件,只需创建一个名为 Home.razor.css 的 CSS 文件。此文件中定义的样式将覆盖项目中的任何其他样式。
- 在 Components\Layout 文件夹中,在 NavMenu.razor ,请注意它有三个菜单项,主页、计数器和天气。这些菜单链接是通过使用名为 NavLink 的组件创建的,如下标记所示:
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">Northwind.Blazor</a>
</div>
</div>
<input type="checkbox" title="Navigation menu"
class="navbar-toggler" />
<div class="nav-scrollable" onclick=
"document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href=""
Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu"
aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="bi bi-plus-square-fill-nav-menu"
aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu"
aria-hidden="true"></span> Weather
</NavLink>
</div>
</nav>
</div>
- 请注意, NavMenu.razor 有一个名为 NavMenu.razor.css 的独立样式表。
- 在 Components\Pages 文件夹中,在 Home.razor ,请注意它定义了一个设置页面标题的组件,然后渲染一个标题和欢迎消息,如下代码所示:
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
在 Components\Pages 文件夹中,在 Weather.razor ,请注意它定义了一个组件,该组件从注入的依赖天气服务中获取天气预报,然后将其渲染在一个表格中,如以下代码所示:
@page "/weather"
@attribute [StreamRendering]
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool",
"Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}
在 Northwind.Blazor.Client 项目中,在 Pages 文件夹内,在 Counter.razor 中,请注意定义了一个 Blazor 页面组件,其路由为 /counter ,渲染模式将在服务器和 WebAssembly 之间自动切换,具有一个名为 currentCount 的私有字段,该字段在每次点击按钮时递增,如以下标记所示:
@page "/counter"
@rendermode InteractiveAuto
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary"
@onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
如何定义可路由页面组件
要创建一个可路由的页面组件,请在组件的 .razor 文件顶部添加 @page 指令,如下所示的标记:
@page "/customers"
前面的代码相当于 Program.cs 中的一个映射端点,如下代码所示:
app.MapGet("/customers", () => ...);
一个页面组件可以有多个 @page 指令来注册多个路由,如以下代码所示:
@page "/weather"
@page "/forecast"
Router 组件专门在其 AppAssembly 参数中扫描程序集,以查找具有 @page 指令的 Blazor 组件,将它们的 URL 路径注册为端点。
在运行时,页面组件与您在 Routes.razor 文件 <RouteView> 组件中指定的任何特定布局合并。默认情况下,Blazor Web App 项目模板将 MainLayout.razor 定义为页面组件的布局。
良好实践:按照惯例,将可路由页面的 Blazor 组件放在 Components\Pages 文件夹中。