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 空模板相同,只是增加了一些关键内容:

  1. 使用您首选的代码编辑器打开 ModernWeb 解决方案,然后添加一个新项目,如下列表所定义:项目模板:Blazor Web 应用 / blazor --interactivity Auto解决方案文件和文件夹: ModernWeb项目文件和文件夹: Northwind.Blazor认证类型:无配置为 HTTPS:已选择交互式渲染模式:自动(服务器和 WebAssembly)交互位置:每页/组件包含样本页面:已选择请勿使用顶级语句:已清除

如果您正在使用 VS Code 或 Rider,请在命令提示符或终端中输入以下命令: dotnet new blazor --interactivity Auto -o Northwind.Blazor

良好实践:默认的交互式渲染模式是服务器。我们明确选择了自动,以便在此项目中查看两种渲染模式。我们还选择了包含示例页面,这在实际项目中您可能不希望包含。

  1. 注意创建了两个项目:Northwind.Blazor : 这是主要的 ASP.NET Core 项目,定义并运行静态 SSR、流式传输和服务器端 Blazor 组件。它还引用并托管您的客户端 WebAssembly Blazor 组件。Northwind.Blazor.Client : 这是一个 Blazor WebAssembly 项目,用于您定义的任何客户端组件。在未来,它可能不需要在单独的项目中,但对于 .NET 8 和 .NET 9,它仍然需要。
  2. 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>
  1. Northwind.Blazor.csproj 中,请注意它与使用 Web SDK 并针对.NET 9 的 ASP.NET Core 项目是相同的。还请注意,它引用了客户端项目。
  2. 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>
  1. Northwind.Blazor.Client.csproj 中,请注意它类似于 ASP.NET Core 项目,但使用 Blazor WebAssembly SDK。
  2. 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>
  1. 构建 Northwind.BlazorNorthwind.Blazor.Client 项目。
  2. 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();
  1. Northwind.Blazor 中,请注意 Components 文件夹及其子文件夹,如 LayoutPages ,使用了您在启用 Blazor 组件时在 Northwind.Web 项目中使用的相同命名约定。
  2. Northwind.Blazor.Client 中,在 Program.cs 中,请注意它创建了一个 WebAssemblyHostBuilder 而不是正常的 WebApplication 构建器,如以下代码中突出显示的内容所示:
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
await builder.Build().RunAsync();
  1. Northwind.Blazor.Client 中,在 Pages 文件夹中,请注意有一个名为 Counter.razor 的 Blazor 组件。

审查 Blazor 路由、布局和导航

让我们回顾一下这个 Blazor 项目的路由配置、布局和导航菜单:

  1. 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 组件,并随后从服务器端切换到客户端组件执行。
  1. 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>
  1. Components\Layout 文件夹中的 MainLayout.razor.css ,请注意它包含该组件的独立 CSS 样式。由于命名约定,在此文件中定义的样式优先于其他可能影响该组件的样式。

Blazor 组件通常需要提供自己的 CSS 以应用样式或 JavaScript 以执行无法仅通过 C# 完成的活动,例如访问浏览器 API。为了确保这不会与站点级别的 CSS 和 JavaScript 冲突,Blazor 支持 CSS 和 JavaScript 隔离。如果您有一个名为 Home.razor 的组件,只需创建一个名为 Home.razor.css 的 CSS 文件。此文件中定义的样式将覆盖项目中的任何其他样式。

  1. 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>
  1. 请注意, NavMenu.razor 有一个名为 NavMenu.razor.css 的独立样式表。
  2. 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 文件夹中。