Blazor 是 ASP.NET Core 新添加的一个功能,它向 Web 应用程序添加客户端交互性。Blazor 有两个变种,本章将重点介绍 Blazor Server,另一种是 Blazor WebAssembly。解释它解决的问题和它是如何工作的。展示如何配置 ASP.NET Core应用程序来使用 Blazor Server,并描述使用 Razor 组件时可用的基本特性,这是 Blazor Server项目的构建块。
Blazor Server使用 JavaScript 来接收浏览器事件,这些事件转发给 ASP.NET Core,使用 C#代码计算。事件对应用程序状态的影响发送回浏览器,并显示给用户。
本章介绍 Blazor Server,解释它解决的问题,并描述了它的优缺点。展示如何配置ASP.NET Core 应用程序来启用 Blazor Server,并展示使用 Razor 组件时可用的基本特性,这些组件是 Blazor 的构建块。
1 准备工作
本章继续使用上章项目。
2 理解 Blazor Server
2.1 Blazor Server 的优势
Blazor 最大的吸引力在于它基于 C# 编写的 Razor Pages。这意味着不需要学习新框架(如Angular 或 React)和新语言,就可提高效率和响应能力。Blazor 很好地集成到 ASP.NET Core 的其余部分中,构建在前面章节中描述的特性之上,这使得它易于使用。
2.2 Blazor Server 的缺点
Blazor 需要一个现代浏览器来建立和维护它的持久 HTTP 连接。而且,如果这种连接丢失,使用 Blazor 的应用程序就会停止工作,这使得它们不适合离线使用,也就是不能依赖连接或者连接速度很慢的地方。Blazor WebAssembly 解决了这些问题,但是,它自己的局限性。
3 从 Blazor 开始
3.1 为 Blazor Server 配置 ASP.NET Core
Startup.cs 文件中添加服务和中间件。
csharp
services.AddServerSideBlazor();
csharp
endpoints.MapBlazorHub();
1.给布局添加 Blazor JavaScript 文件
Blazor 依赖 JavaSaipt 代码与 ASP.NET Core 服务器进行通信,在 Views/Shared 文件夹的 _Layout.cshtml 中将JavaScript文件添加到控制器视图使用的布局中。
csharp
<!DOCTYPE html>
<html>
<head>
<title>@ViewBag.Title</title>
<link href="/lib/twitter-bootstrap/css/bootstrap.min.css" rel="stylesheet" />
<base href="~/" />
</head>
<body>
<div class="m-2">
@RenderBody()
</div>
<script src="_framework/blazor.server.js"></script>
</body>
</html>
script 元素指定 JavaScript 文件的名称,对它的请求被添加到请求管道中的中间件拦截,因此不需要额外的包将 JavaScript 代码添加到项目中。还必须添加基本元素以指定应用程序的根 URL。同样的元素必须添加到 Razor Pages 使用的布局中。2.创建 Blazor 导入文件
添加一个名为 _Imports.razor 的文件。如果使用的是 Visual Sudio,可以使用 Razor View Imports 模板来创建这个文件,但是要确保使用 .Razor 文件扩展名。
csharp
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@using Microsoft.EntityFrameworkCore
@using MyAdvanced.Models
3.2 创建 Blazor 组件
这里有一个术语上的冲突:技术是 Blazor,但关键的构建块称为 Razor 组件。Razor 组件是在扩展名为 .Razor 的文件中定义的,并且必须以大写字母开头。组件可以在任何地方定义,但它们通常组合在一起,以帮助保持项目的组织性。
创建一个 Blazor 文件夹,并添加一个名为 PeopleList. Razor 的 Razor 组件。
csharp
<table class="table table-sm table-bordered table-striped">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Dept</th>
<th>Location</th>
</tr>
</thead>
<tbody>
@foreach (Person p in People)
{
<tr class="@GetClass(p.Location.City)">
<td>@p.PersonId</td>
<td>@p.Surname, @p.Firstname</td>
<td>@p.Department.Name</td>
<td>@p.Location.City, @p.Location.State</td>
</tr>
}
</tbody>
</table>
<div class="form-group">
<label for="city">City</label>
<select name="city" class="form-control" @bind="SelectedCity">
<option disabled selected>Select City</option>
@foreach (string city in Cities)
{
<option value="@city" selected="@(city == SelectedCity)">
@city
</option>
}
</select>
</div>
@code
{
[Inject]
public DataContext Context { get; set; }
public IEnumerable<Person> People =>
Context.People.Include(p => p.Department).Include(p => p.Location);
public IEnumerable<string> Cities => Context.Locations.Select(l => l.City);
public string SelectedCity { get; set; }
public string GetClass(string city) =>
SelectedCity == city ? "bg-info text-white" : "";
}
Razor 组件类似于 Razor Pages。视图部分依赖于在前面章节中看到的 Razor 特性,使用@表达式将数据值插入组件的 HTML中,或者以序列化方式为对象生成元素。这个@foreach 表达式为 Cities 序列中的每个值生成 option 元素。
尽管 Razor 组件看起来很熟悉,但有一些重要的区别。第一个是没有页面模型类和 @mode 表达式。支持组件 HTML 的属性和方法直接在 @code 表达式中定义,该表达式与 Razor Pages 的 @functions 表达式对应。例如,要定义向视图部分提供 Person 对象的属性,只需要在@code 部分中定义一个 People 属性。
而且,因为没有页面模型类,所以没有用于声明服务依赖关系的构造函数。相反,依赖注入会设置已被 [Inject] 特性修饰的属性值。最显著的区别是在 select 元素上使用了特殊属性 @bind,在 select 元素的值和 @code 部分中定义的 SelectedCity 属性之间创建一个数据绑定。使用 Razor 组件
Razor 组件作为 Razor Pages 或控制器视图的一部分交付给浏览器。在 Views/Home 文件夹的 Index.cshtml 文件中使用 Razor 组件。
csharp
@model PeopleListViewModel
<h4 class="bg-primary text-white text-center p-2">People</h4>
<component type="typeof(MyAdvanced.Blazor.PeopleList)" render-mode="Server" />
Razor 组件是使用组件元素应用的,对于组件元素有一个标签助手。组件元素使用 type 和 render-mode 属性配置。type 属性用于指定 Razor 组件。Razor 组件编译成类,就像控制器视图和 Razor Pages 一样。render-mode 属性用于选择组件如何使用 RenderMode 枚举中的值生成内容,如下表:
名称 | 描述 |
---|---|
Static | Razor 组件将其视图部分呈现为不支持客户端的静态 HTML |
Server | HTML 文档连同组件的占位符一起发送到浏览器。组件显示的 HTML 通过持久 HTTP 连接发送 到浏览器并显示给用户 |
ServerPrerendered | 组件的视图部分包含在 HTML 中,并立即显示给用户。HTML 内容将通过持久 HTTP 连再次发送 |
对于大多数应用程序,Server 选项是一个不错的选择。ServerPrerendered 在发送到浏览器的 HTML 文档中包含了 Razor 组件视图部分的静态再现。它充当占位符内容,这样在加载和执行 JavaScript 代码时,用户不会看到空的浏览器窗口。一旦建立了持久 HTTP 连接,占位符内容将被删除,替换为 Blazor 发送的动态版本。向用户显示静态内容的想法是好的,但它可以被混淆,因为 HTML元素没有连接到应用程序的服务器端部分,与任何用户交互失效或一旦动态内容到来HTML 元素就会被丢弃。
要查看 Blazor 的运行情况,使用浏览器请求 http://localhost:5000/controllers
。在使用 Blazor 时不需要提交表单,因为一旦更改了 select 元素的值,数据绑定就会响应。
Razor 组件也可在 Razor Pages 中使用。将一个名为 Blazor.cshtml 的 Blazor 页面添加到 Pages 文件夹中。请求 http://localhost:5000/pages/blazor
。
csharp
@page "/pages/blazor"
<h4 class="bg-primary text-white text-center p-2">Blazor People</h4>
<component type="typeof(MyAdvanced.Blazor.PeopleList)" render-mode="Server" />
4 理解 Razor 组件的基本特性
4.1 理解 Blazor 事件和数据绑定
事件允许 Razor 组件响应用户的交互,Blazor 使用持久的 HTTP 连接将事件的详细信息发送到可以处理它的服务器。要査看 Blazor 事件的运行情况,为 Blazor文件夹添加 Events.razor 的 Razor 组件。
csharp
<div class="m-2 p-2 border">
<button class="btn btn-primary" @onclick="IncrementCounter">
Increment
</button>
<span class="p-2">Counter Value:@Counter</span>
</div>
@code
{
public int Counter { get; set; } = 1;
public void IncrementCounter(MouseEventArgs e)
{
Counter++;
}
}
向 HTML元素添加属性,可注册事件处理程序,其中属性名称为@onclick,后面跟着事件名称。在这个例子中,为 button 元素生成的 click 事件设置了一个处理方法。
赋给属性的值是在触发事件时调用的方法的名称。该方法可以定义一个可选参数,该参数要么是 EventArgs 类的实例,要么是 EventArgs 派生的类,提供关于事件的附加信息。
对于 onclick 事件,处理程序方法接收一个 MouseEventArgs 对象,该对象提供额外的细节,比如单击的屏幕坐标。
Blazor JavaScript 代码在事件触发时接收事件,并通过持久 HTTP 连接将其转发给服务器。调用处理程序方法,并更新组件的状态。对组件的视图部分生成的内容的任何更改都将发送回 JavaScript 代码,JavaScript 代码将更新浏览器显示的内容。
在本例中,单击事件将由 IncrementCounter方法处理,该方法会更改 Counter 属性的值。Counter 属性的值包含在组件呈现的 HTML 中,因此 Blazor 将更改发送到浏览器,以便 JavaScript 代码可以更新显示给用户的 HTML 元素。
要显示 Events 组件,请替换 Pages 文件夹中 Blazor.cshtml 文件的内容。
csharp
@page "/pages/blazor"
<h4 class="bg-primary text-white text-center p-2">Events</h4>
<component type="typeof(MyAdvanced.Blazor.Events)" render-mode="Server" />
1.处理来自多个元素的事件
为避免代码重复,一个处理程序方法可以接收来自多个元素的操作。以下在 Blazor 文件夹的 Events.razor 文件中处理事件:
csharp
</div>
<div class="m-2 p-2 border">
<button class="btn btn-primary" @onclick="@(e=>IncrementCounter(e,1))">
Increment Counter #2
</button>
<span class="p-2">Counter Value:@Counter[1]</span>
</div>
@code
{
public int[] Counter { get; set; } = new int[] { 1, 1 };
public void IncrementCounter(MouseEventArgs e, int index)
{
Counter[index]++;
}
}
Blazor 事件属性可与 lambda 函数一起使用,后者接收 EventArgs 对象并调用带有附加参数的处理程序方法。本例向 IncrementCounter 方法添加了一个索引参数,该参数用于确定应该更新哪个计数器值。
当以编程方式生成元素时,也可以使用这种技术,如下:
csharp
@for (int i = 0; i < ElementCount; i++)
{
int local = i;
<div class="m-2 p-2 border">
<button class="btn btn-primary" @onclick="@(() => IncrementCounter(local))">
Increment Counter #@(i + 1)
</button>
<span class="p-2">Counter Value: @GetCounter(i)</span>
</div>
}
@code
{
public int ElementCount { get; set; } = 4;
public Dictionary<int, int> Counters { get; } = new Dictionary<int, int>();
public int GetCounter(int index) =>
Counters.ContainsKey(index) ? Counters[index] : 0;
public void IncrementCounter(int index) =>
Counters[index] = GetCounter(index) + 1;
}
本例使用 @for 表法式来生成元素,并使用循环变量作为处理程序方法的参数。还从处理程序方法中删除了 EventArgs 参数。2.避免处理程序方法名的陷阱
在指定事件处理程序方法时,最常见的错误是包含括号@onclick="Incrementcounter()"
,当指定一个处理程序方法时,必须只指定事件名@onclick="IncrementCounter"
,可以将方法名指定为 Razor 表达式@onclick="@IncrementCounter"
。
在使用 lambda 函数时,有一组不同的规则,在 Razor 表达式中,lambda 函数的定义就像它在 C#类中的定义一样,这意味着定义参数,后跟箭头,再跟函数体,@onclick="@((e)=> HandleEvent(e, local))
,如果不需要使用 EventArgs 对象,那么可以忽略 lambda 函数的参数,@onclick="@(()=> IncrementCounter (local))"
。
请求http://localhost:5000/pages/blazor
,所有 button 元素生成的 click 事件由相同的方法处理,但是 lambda 函数提供的参数确保更新了正确的计数器。3.不使用处理程序方法处理事件
简单的事件处理可直接在 lambda 函数中完成,而不需要使用处理程序方法:
csharp
<button class="btn btn-info" @onclick="@(()=>Counters.Remove(local))">Reset</button>
4.2 使用数据绑定
事件处理程序和 Razor 表达式可用于创建 HTML 元素和 C# 值之间的双向关系,这对于允许用户进行更改的元素(如输入和选择元素)非常有用。为 Blazor 文件夹添加名为 Bindings.razor 的 Razor 组件:
csharp
<div class="form-group">
<label>City:</label>
<input class="form-control" value="@City" @onchange="UpdateCity" />
</div>
<div class="p-2 mb-2">City Value: @City</div>
<button class="btn btn-primary" @onclick="@(() => City = "Paris")">Paris</button>
<button class="btn btn-primary" @onclick="@(() => City = "Chicago")">Chicago</button>
@code
{
public string City { get; set; } = "London";
public void UpdateCity(ChangeEventArgs e)
{
City = e.Value as String;
}
}
@onchange 属性将 UpdateCity 方法注册为来自输入元素的更改事件的处理程序。这些事件是使用 ChangeEventArgs 类描述的,该类提供了一个 Value 属性。每次接收到更改事件时,都会更新 City。
input 元素的 value 属性创建了另一个方向的关系,这样当 City 属性的值发生变化时,元素的 value 也会变化。
Pages 文件夹的 Blazor.cshtml文件中使用 Razor 组件:
csharp
<component type="typeof(MyAdvanced.Blazor.Bindings)" render-mode="Server" />
请求http://localhost:5000/pages/blazor
,更改 input 值或点击 button 会同时更改绑定的值。
涉及变更事件的双向关系可表示为数据绑定,数据绑定允许值和事件都用单个属性配置,如下:
csharp
<div class="form-group">
<label>City:</label>
<input class="form-control" @bind="City" />
</div>
<div class="p-2 mb-2">City Value: @City</div>
<button class="btn btn-primary" @onclick="@(() => City = "Paris")">Paris</button>
<button class="btn btn-primary" @onclick="@(() => City = "Chicago")">Chicago</button>
@code
{
public string City { get; set; } = "London";
//public void UpdateCity(ChangeEventArgs e)
//{
// City = e.Value as String;
//}
}
@bind 属性用于指定在更改事件触发时更新的属性,以及在 Value 属性更改时更新的属性。1.更改绑定事件
默认情况下,绑定中使用更改事件,为用户提供了合理的响应性,如下:
名称 | 描述 |
---|---|
@bind-value | 此属性用于选择数据绑定的属性 |
@bind-value:event | 此属性用于选择数据绑定的事件 |
这些属性替代了@bind,如下所示:
csharp
<input class="form-control" @bind-value="City" @bind-value:event="oninput" />
这个属性组合为 City 属性创建了一个绑定,该属性在触发 oninput 事件时(每次击键后)更新而不是只在输入元素失去焦点时更新。2.创建 DateTime 绑定
Blazor 特别支持为 DateTime 属性创建绑定,允许使用特定区域性或格式字符串表示它们。参数如下:
名称 | 描述 |
---|---|
@bind:culture | 此属性用于选择 CultureInfo 对象(用于格式化 DateTime 值) |
@bind:format | 此属性用于指定一个数据格式化字符串(用于格式化 DateTime 值) |
在 Blazor 文件夹的 Bindings.razor 文件中使用 DateTime 属性:
csharp
@using System.Globalization
<div class="form-group">
<label>City:</label>
<input class="form-control" @bind-value="City" @bind-value:event="oninput" />
</div>
<div class="p-2 mb-2">City Value: @City</div>
<button class="btn btn-primary" @onclick="@(() => City = "Paris")">Paris</button>
<button class="btn btn-primary" @onclick="@(() => City = "Chicago")">Chicago</button>
<div class="form-group mt-2">
<label>Time:</label>
<input class="form-control my-1" @bind="Time" @bind:culture="Culture"
@bind:format="MMM-dd" />
<input class="form-control my-1" @bind="Time" @bind:culture="Culture" />
<input class="form-control" type="date" @bind="Time" />
</div>
<div class="p-2 mb-2">Time Value: @Time</div>
<div class="form-group">
<label>Culture:</label>
<select class="form-control" @bind="Culture">
<option value="@CultureInfo.GetCultureInfo("en-us")">en-US</option>
<option value="@CultureInfo.GetCultureInfo("en-gb")">en-GB</option>
<option value="@CultureInfo.GetCultureInfo("fr-fr")">fr-FR</option>
</select>
</div>
@code
{
public string City { get; set; } = "London";
public DateTime Time { get; set; } = DateTime.Parse("2050/01/20 09:50");
public CultureInfo Culture { get; set; } = CultureInfo.GetCultureInfo("en-us");
}
有三个输入元素用于显示相同的 DataTime 值,其中两个使用表中的属性配置。第一个元素配置了区域和格式字符串,<input class="form-control my-1" @bind="Time" @bind:culture="Culture" @bind:format="MMM-dd" />
,使用在 select 元素中选择的区域以及显示缩写月份名称和数字日期的格式字符串来显示DateTime 属性。第二个输入元素只指定区域,这意味着将使用默认的格式化字符串。
将 type 属性设置为 date、datetime-local、month或 time 时,不应指定区域或格式字符串,因为Blazor 自动将日期值格式化为区域无关的格式,浏览器将其转换为用户的语言环境。
5 使用类文件定义组件
如果不喜欢 Razor 组件支持的代码和标记的混合,可以使用 C# 类文件来定义一部分组件或全部组件。
5.1 使用代码隐藏类
Razor 组件的 @code 部分可在单独的类文件中定义,称为代码隐藏类或代码隐藏文件。Razor 组件的代码隐藏类定义为部分类,其名称与为其提供代码的组件相同。
给 Blazor 文件夹添加名为 Split.razor 的 Razor 组件:
csharp
<ul class="list-group">
@foreach (string name in Names)
{
<li class="list-group-item">@name</li>
}
</ul>
这个文件只包含 HTML 内容和 Razor 表达式,并呈现一个它希望通过 Names 属性接收的名称列表。要为组件提供其代码,请将一个名为 Split.razor.cs 的类文件添加到 Blazor 文件夹中:
csharp
using MyAdvanced.Models;
using Microsoft.AspNetCore.Components;
using System.Collections.Generic;
using System.Linq;
namespace MyAdvanced.Blazor
{
public partial class Split
{
[Inject]
public DataContext Context { get; set; }
public IEnumerable<string> Names => Context.People.Select(p => p.Firstname);
}
}
部分类必须在与其 Razor组件相同的名称空间中定义,并且具有相同的名称。
在 Pages 文件夹的 Blazor.cshtml 文件中应用新的组件:
csharp
@page "/pages/blazor"
<h4 class="bg-primary text-white text-center p-2">Code-Behind</h4>
<component type="typeof(MyAdvanced.Blazor.Split)" render-mode="Server" />
5.2 定义 Razor 组件类
Razor 组件可以完全在类文件中定义,尽管这样做的表达性可能不如使用 Razor 表达式。Blazor 文件夹中 添加 CodeOnly.cs类:
csharp
using MyAdvanced.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;
using System.Collections.Generic;
using System.Linq;
namespace MyAdvanced.Blazor
{
public class CodeOnly : ComponentBase
{
[Inject]
public DataContext Context { get; set; }
public IEnumerable<string> Names => Context.People.Select(p => p.Firstname);
public bool Ascending { get; set; } = false;
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
IEnumerable<string> data = Ascending
? Names.OrderBy(n => n) : Names.OrderByDescending(n => n);
builder.OpenElement(1, "button");
builder.AddAttribute(2, "class", "btn btn-primary mb-2");
builder.AddAttribute(3, "onclick",
EventCallback.Factory.Create<MouseEventArgs>(this,
() => Ascending = !Ascending));
builder.AddContent(4, new MarkupString("Toggle"));
builder.CloseElement();
builder.OpenElement(5, "ul");
builder.AddAttribute(6, "class", "list-group");
foreach (string name in data)
{
builder.OpenElement(7, "li");
builder.AddAttribute(8, "class", "list-group-item");
builder.AddContent(9, new MarkupString(name));
builder.CloseElement();
}
builder.CloseElement();
}
}
}
组件的基类是 ComponentBase,通常表示为带注释的 HTML 元素的内容是通过覆盖 BuildRenderTree 方法并使用 RenderTrceBuilder 参数创建的,创建内容会很麻烦,然后在 Blazor.cshtml 中像上面一样使用。