什么是 Blazor 组件?
简单来说,Blazor 中的每个 razor 文件都是一个组件。 就是这么简单!
Blazor 中的 razor 文件包含标记,并且在 @code 部分中有代码。 我们在 MyFirstBlazor 项目中使用的每个页面都是一个组件! 并且可以通过将其他组件添加为子组件来构建组件。
从 ComponentBase
类派生的任何类都将成为 Blazor 组件; 稍后,我们将建立一个这样的例子。 当您使用 razor 文件时,生成的类也将派生自 ComponentBase
。
还记得上一章中的 MyFirstBlazor 项目吗? 在 Visual Studio(或 Code)中创建一个新的,就像它一样,让我们看看其中的一些组件。
打开 Index.razor
,如清单 3-1 所示。 请参阅调查提示? 这是 Blazor 模板中的组件之一。 它需要一个参数 Title,我们可以设置我们想要使用组件的位置。 让我们好好看看 SurveyPrompt
组件。
清单 3-1 Index 页面
@page "/"
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
检查 SurveyPrompt 组件
如清单 3-2 所示打开 SurveyPrompt.razor
,它可以在客户端项目的 Shared
文件夹中找到。 该组件被称为 SurveyPrompt
,因为组件以它所在的 razor
文件命名。
清单 3-2
SurveyPrompt
组件
<div class="alert alert-secondary mt-4" role="alert">
<span class="oi oi-pencil mr-2" aria-hidden="true"></span>
<strong>@Title</strong>
<span class="text-nowrap">
Please take our
<a target="_blank" class="font-weight-bold"
href="https://go.microsoft.com/fwlink/?linkid=2148851">
brief survey
</a>
</span>
and tell us what you think.
</div>
@code {
// Demonstrates how a parent component can supply parameters
[Parameter]
public string Title { get; set; }
}
查看 Razor 标记。 这是一个简单的 Blazor 组件,它在 Title 前面显示一个图标,如图所示,然后显示一个调查链接。
@code
部分只包含一个属性 Title,它使用单向数据绑定在组件中呈现。 请注意 Title 属性上的 [Parameter]
属性。 对于想要将其公共属性公开给父组件的组件,这是必需的。 这样,我们就可以将数据传递给嵌套组件,例如 Index 组件如何将 Title 传递给 SurveyPrompt 组件。
使用 Razor 构建简单的警报组件
使用 Visual Studio 创建新组件
打开 MyFirstBlazor 解决方案。 右键单击 Pages 文件夹并选择 Add ➤ New Item...。 Add New Item 窗口应打开,如图所示。
选择 Razor 组件并将其命名为 Alert.razor
。 单击添加。
实现Alert组件
从 Alert.razor
中删除所有现有内容,并将其替换为清单 3-3。 让我们看看这个组件。
Alert 组件的第一行使用@if
来隐藏或显示其内部内容。如果您想有条件地显示内容,这是一种常用技术。 因此,如果 Show
公共属性(实际参数)为 false
,则不会显示整个组件。 这允许我们在需要时“隐藏”组件。
我们的 Alert
组件将在 <div>
元素中显示一些内容作为警报(使用引导样式),那么我们如何将这些内容传递给 Alert 组件?
在@if
中,有一个以@ChildContent
作为其子元素的<div>
元素。 如果您想访问 Alert
组件中的嵌套元素,您可以使用 @ChildContent
,正如您在清单 3-4 中使用 Alert 组件时所看到的那样。
Blazor 规定此属性/参数应命名为 @ChildContent
并且它必须是 RenderFragment
类型,因为这是 Blazor 引擎传递它的方式。
清单 3-3 Alert组件
@if (Show)
{
<div class="alert alert-secondary mt-4" role="alert">
@ChildContent
</div>
}
@code {
[Parameter]
public bool Show { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; } = default!;
}
返回到 Index.razor 添加元素。
当您开始键入时,Visual Studio 和 Code 足够智能,可以为您提供 IntelliSense,如图 3-3 所示,用于警报组件及其参数!
完成 Alert 并添加一个按钮,如清单 3-4 所示。
清单 3-4 在 Index.razor 中使用我们的警报组件
@page "/"
<h1>Hello, world!</h1>
<Alert Show="@ShowAlert">
<span class="oi oi-check mr-2" aria-hidden="true"></span>
<strong>Blazor is so cool!</strong>
</Alert>
<button @onclick="ToggleAlert" class="btn btn-success">Toggle</button>
@code {
public bool ShowAlert { get; set; } = true;
public void ToggleAlert() => ShowAlert = !ShowAlert;
}
在 <Alert>
标记内,有一个 <span>
使用 open-iconic 字体显示复选标记图标和一个显示简单消息的 <strong>
元素。 这些将被设置为 Alert 组件的 @ChildContent
属性。
构建并运行您的项目。 当你点击 <button>
时,它会调用 ToggleAlert
方法,该方法将隐藏和显示 Alert,如图 3-4 所示。
分离视图和视图模型
您可能不喜欢这种标记(视图)和代码(视图模型)的混合。 如果您愿意,可以使用两个单独的文件,一个用于使用 razor 的视图,另一个用于使用 C# 的视图模型。 视图将显示来自视图模型的数据,并且视图中的事件处理程序将调用来自视图模型的方法。
有些人更喜欢这种工作方式,因为它更像 MVVM 模式。
每个 Blazor razor 文件都会生成到 C# 分部类中。 如果要将代码与razor 文件分开,请将代码放在与组件同名的部分类中。 C# 编译器会将两个文件中的代码合并到一个类中。 让我们试试这个!
创建 DismissibleAlert 组件
如果您还没有这样做,请打开 MyFirstBlazor
解决方案。 在 Visual Studio 中,右键单击 Pages
文件夹并选择 Add ➤ New Item...。 Add New Item 对话框应打开, 这一次,选择 Razor 组件并将其命名为 DismissibleAlert.razor
。 此外,添加一个新的 C# 类,并调用文件 DismissibleAlert.razor.cs
。
Dismissible
是带有小 x 按钮的警报,用户可以单击该按钮来关闭警报。 它与之前的 Alert
组件非常相似。 将 razor
文件中的标记替换为清单 3-5。
清单 3-5 Dismissible.razor 的标记
@if (Show)
{
<div class="alert alert-secondary alert-dismissible fade show mt-4"
role="alert">
@ChildContent
<button type="button"
class="close"
data-dismiss="alert"
aria-label="Close"
@onclick="Dismiss">
<span aria-hidden="true">×</span>
</button>
</div>
}
没有@code
部分,因为您将在 .cs 文件中编写它。
将 Dismissible.razor.cs 中的 C# 代码替换为清单 3-6。
清单 3-6 Dismissible.razor.cs 的代码
using Microsoft.AspNetCore.Components;
namespace Components.Pages
{
public partial class Dismissible
{
[Parameter]
public bool Show { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; } = default!;
public void Dismiss()
=> Show = false;
}
}
请注意,这是一个与 Blazor 组件同名的部分类! 因此,您可以将代码放在部分类中,而不是将代码放在剃刀文件的 @code
部分。
哪种型号最好? 我不认为任何一个比另一个更好。 这更多的是品味问题。选择你喜欢的那个。 我确实更喜欢代码分离模型(我个人的看法),因为我认为 C# 编辑器具有更好的功能来保持我的代码可维护和清洁。
理解父子通信
父组件和子组件通常通过数据绑定进行通信。 例如,在示例 3-7 中,我们使用了 Dismissible,它通过父组件的 ShowAlert 属性与父组件通信。 单击“切换”按钮将隐藏和显示警报。 您可以通过使用清单 3-7 替换 Index.razor 的内容(只需将 Alert 替换为 Dismissible)来尝试此操作。
清单 3-7 使用 Dismissible
@page "/"
<h1>Hello, world!</h1>
<Dismissible Show="@ShowAlert">
<span class="oi oi-check mr-2" aria-hidden="true"></span>
<strong>Blazor is so cool!</strong>
<Dismissible/>
<button @onclick="ToggleAlert" class="btn btn-success">Toggle</button>
@code {
public bool ShowAlert { get; set; } = true;
public void ToggleAlert() => ShowAlert = !ShowAlert;
}
添加定时器组件
首先将一个名为 Timer
的新类添加到 Pages
文件夹中,如清单 3-8 所示。 计时器没有任何可视部分,因此我们甚至不需要 .razor 文件来构建视图。
Blazor
组件是继承 ComponentBase
类的类。 由于我们要将 Timer
类用作 Blazor
组件,因此需要从 ComponentBase
继承。
此 Timer
类将在一定秒数 (TimeInSeconds) 过期后调用委托 (Tick)。 Tick 参数属于 Action
类型,它是 .NET 的内置委托类型之一。 Action
只是一个返回没有参数的 void
的方法。还有其他通用的Action
类型,例如Action<T>
,它是一个返回void的方法,带有一个T类型的参数。这允许父组件设置Action,因此子组件将执行Action(在这种情况下,在TimeInSeconds之后 已过期)。
清单 3-8 Timer类
using Microsoft.AspNetCore.Components;
using System;
using System.Threading;
namespace Components.Pages
{
public class Timer : ComponentBase
{
[Parameter]
public double TimeInSeconds { get; set; }
[Parameter]
public Action Tick { get; set; } = default!;
protected override void OnInitialized()
{
var timer = new System.Threading.Timer(
callback: (_) => InvokeAsync(() => Tick?.Invoke()),
state: null,
dueTime: TimeSpan.FromSeconds(TimeInSeconds),
period: Timeout.InfiniteTimeSpan);
}
}
}
现在将 Timer
组件添加到 Index
页面,如清单 3-9 所示。 通过此更改,Timer
组件将在 5 秒后调用 ToggleAlert
方法。
清单 3-9 添加Timer组件以关闭Alert
@page "/"
<h1>Hello, world!</h1>
<Dismissible Show="@ShowAlert">
<span class="oi oi-check mr-2" aria-hidden="true"></span>
<strong>Blazor is so cool!</strong>
</Dismissible>
<button @onclick="ToggleAlert" class="btn btn-success">Toggle</button>
<Timer TimeInSeconds="5" Tick="ToggleAlert"/>
@code {
public bool ShowAlert { get; set; } = true;
public void ToggleAlert()
{
Console.WriteLine("*** Toggle ***");
ShowAlert = !ShowAlert;
}
}
运行应用程序并等待至少 5 秒。 alert不隐藏! 为什么?!
查看示例 3-9 中 Dismissible
的标记。它根据 Show
参数显示组件,而这个是通过数据绑定设置的。是否调用了 ToggleAlert
方法? 再次运行 Blazor 网站,并立即在控制台选项卡上打开浏览器的调试器。片刻之后,您应该会看到 Console.WriteLine
输出出现。 所以ToggleAlert
方法确实被调用了。
想想这个。 我们使用 Timer
异步调用方法。当计时器触发时,我们将 Index 组件的 ShowAlert
属性设置为 false
。 但是我们仍然需要更新 UI。 您可以通过调用 StateHasChanged
方法手动触发 UI
进行更新。
这个非常重要!Blazor 运行时会在触发事件(例如单击按钮)时自动更新 UI。Blazor 运行时还会更新其自己的异步方法的 UI,但不会更新其他异步方法(如 Timer)。
是时候修复我们的应用程序了。 在 ToggleAlert
方法中添加对 StateHasChanged
的调用,如清单 3-10 所示。
清单 3-10 添加 StateHasChanged
public void ToggleAlert()
{
ShowAlert = !ShowAlert;
StateHasChanged();
}
再次运行并等待,5秒后,警报消失!
老实说,我不喜欢以前解决我们问题的方法。 因为子组件调用了ToggleAlert
方法,所以我们需要手动调用StateHasChanged
。有没有更好的办法? 我们甚至还没有解决另一个问题。 当用户在计时器触发 Tick
方法之前关闭警报时,它应该在 5 秒后重新出现,因为它将 ShowAlert
设置true
!
我们将解决这两个问题,但首先,我们需要了解组件之间的双向数据绑定。
在组件之间使用双向数据绑定
当用户单击 Dismissible
组件的关闭按钮时,它按预期将自己的 Show
属性设置为 false
。 问题是父索引组件的 ShowAlert
保持为true
。 更改 Dismissible
本地 Show
属性的值不会更新 Index
组件的 ShowAlert
属性。 我们需要的是组件之间的双向数据绑定,而 Blazor 就有。
对于双向数据绑定,更改 Show
参数的值将更新父级的 ShowAlert
属性的值,反之亦然。
您可以使用@bind-<<NameOfProperty>>
语法(我们已经在上一章的 InputTitle 组件中使用过)来对子组件的任何属性进行数据绑定。 这将使用双向数据绑定。 所以更新索引页面以使用双向数据绑定,如清单 3-11 所示。
清单 3-11 使用双向数据绑定
<Dismissible @bind-Show="ShowAlert">
<span class="oi oi-check mr-2" aria-hidden="true"></span>
<strong>Blazor is so cool!</strong>
</Dismissible>
运行网站。 但是,您不会看到任何有效页面。 Blazor 运行时遇到问题。 您可以通过打开浏览器的调试器来发现问题。检查控制台。 您将看到一堆红色消息,其中一条说明:
Object of type 'Components.Pages.Dismissible' does not have a property
matching the name 'ShowChanged'.
支持双向数据绑定的属性需要一种方法来告诉父级属性已更改。 子组件使用委托,因此当属性发生更改时,父组件可以通过 Blazor 运行时安装自己的更改处理程序(就像事件一样)。 然后,此更改处理程序将更新父组件的数据绑定属性。 子组件负责在属性更改时调用 Changed 委托。
打开 Dismissible
类及其实现以匹配清单 3-12。 有两个变化。 首先,Show
属性现在使用属性的“完整”实现,因为我们需要实现 setter,它会在其值更改时调用 ShowChanged
委托。
其次,我们添加一个应该调用的额外参数<<yourproperty>>
更改了 Action<<typeofyourproperty>>
类型。 例如,属性命名为 bool
类型的 Show
,所以我们添加 Action<bool>
类型的 ShowChanged
。
清单 3-12 具有双向绑定支持的
Dismissible
类
using Microsoft.AspNetCore.Components;
using System;
namespace Components.Pages
{
public partial class Dismissible
{
private bool show;
[Parameter]
public bool Show
{
get => show;
set
{
if (value != show)
{
show = value;
ShowChanged?.Invoke(show);
}
}
}
[Parameter]
public Action<bool>? ShowChanged { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; } = default!;
public void Dismiss() => Show = false;
}
}
每当某人或某事更改 Show 属性的值时,该属性的设置器都会触发 ShowChanged
委托。 这意味着父组件可以将一些代码(当您使用双向数据绑定时为您执行)注入 ShowChanged
委托属性,该属性将在属性更改(内部或外部)时调用。
注意 属性设置器检查值是否已更改。 仅在发生实际更改时才触发 Changed
委托。 这将避免可能出现的 Changed
处理的无限循环。
现在,当 Dismissible Show
属性更改时,Blazor 将更新父级的 ShowAlert
属性,因为我们使用的是双向数据绑定。
当 Timer 触发时,我们仍然需要解决问题。
一种方法(但还有更好的方法)在示例 3-13 中。 在这里,只要 ShowAlert
属性获得新值,我们就会调用 StateHasChanged
。 这更好,因为我们在更新 ShowAlert
属性的任何地方都会更新 UI。
清单 3-13
ShowAlert
更改值时更新 UI
@page "/"
<h1>Hello, world!</h1>
<DismissibleAlert @bind-Show="ShowAlert">
<span class="oi oi-check mr-2" aria-hidden="true"></span>
<strong>Blazor is so cool!</strong>
</DismissibleAlert>
<button @onclick="ToggleAlert" class="btn btn-success">Toggle</button>
<Timer TimeInSeconds="5" Tick="ToggleAlert" />
@code {
private bool showAlert = true;
public bool ShowAlert
{
get => showAlert;
set
{
if (value != showAlert)
{
showAlert = value;
StateHasChanged();
}
}
}
public void ToggleAlert()
{
ShowAlert = !ShowAlert;
}
}
Run.。 等待 5 秒钟。
Alert应自动隐藏,如图所示。
如果您的项目仍未更新,您可以通过添加断点或一些 Console.WriteLine
语句来调试客户端 Blazor 项目。 这些将出现在浏览器的控制台窗口中。
使用 EventCallback<T>
现在,使用上一节中的 DismissibleAlert 组件,我们一直在使用 @bind-Show 语法在组件之间使用双向数据绑定,并且我们使用 ShowChanged 回调通知父组件 Show 属性已更改。 为了让父级更新其 UI,我们还在父级的 ShowAlert 属性被修改时添加了对 StateHasChanged 的调用。 但是有更好的方法!
更新 Dismissible Alert 组件 Show Changed,如清单 3-14 所示。
清单 3-14 使用
EventCallback<T>
using Microsoft.AspNetCore.Components;
using System;
namespace Components.Pages
{
public partial class DismissibleAlert
{
private bool show;
[Parameter]
public bool Show
{
get => show;
set
{
if (value != show)
{
show = value;
ShowChanged?.InvokeAsync(show);
}
}
}
[Parameter]
public EventCallback<bool>? ShowChanged { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; } = default!;
public void Dismiss() => Show = false;
}
}
因此,我们不使用 Action<T>
委托,而是使用 EventCallback<T>
类型。 首先,这个类型是值类型,所以我们不需要检查null
。 而不是 Invoke
方法,它有一个 InvokeAsync
方法,它解决了一些在此时不重要的特殊问题。
清单 3-15 改进的Timer组件
using Microsoft.AspNetCore.Components;
using System;
using System.Threading;
namespace Components.Pages
{
public class Timer : ComponentBase
{
[Parameter]
public double TimeInSeconds { get; set; }
[Parameter]
public EventCallback Tick { get; set; } = default!;
protected override void OnInitialized()
{
var timer = new System.Threading.Timer(
callback: (_) => InvokeAsync(() => Tick.InvokeAsync()),
state: null,
dueTime: TimeSpan.FromSeconds(TimeInSeconds),
period: Timeout.InfiniteTimeSpan);
}
}
}
最后,通过删除对 StateHasChanged
的调用来更新 Index
组件的 ShowAlert
属性,如清单 3-16 所示(我们可以再次使用自动属性)。
清单 3-16 具有简单
ShowAlert
属性的索引
@page "/"
<h1>Hello, world!</h1>
<DismissibleAlert @bind-Show="ShowAlert">
<span class="oi oi-check mr-2" aria-hidden="true"></span>
<strong>Blazor is so cool!</strong>
</DismissibleAlert>
<button @onclick="ToggleAlert" class="btn btn-success">Toggle</button>
<Timer TimeInSeconds="5" Tick="ToggleAlert" />
@code {
public bool ShowAlert { get; set; } = true;
public void ToggleAlert()
{
ShowAlert = !ShowAlert;
}
}
构建并运行。 等待 5 秒钟。 警报应该隐藏!
通常,您应该更喜欢 EventCallback<T>
而非普通委托来进行父子通信,例如事件和双向数据绑定。 规则有例外(例如,EventCallback
触发组件的事实,重新渲染可能是一个问题,然后使用委托可能是解决方案)。
引用子组件
通常,您应该更喜欢数据绑定以使组件相互通信。 这样,一个组件不需要知道关于另一个组件的任何信息,除了数据绑定。 它还使 Blazor 运行时负责使用更改更新组件。
但是,您也可以直接与子组件交互。 让我们看一个例子:我们希望通过调用 Dismiss
方法让可关闭的警报消失。 更新您的代码以匹配示例 3-17,其中我们使用 @ref
语法在字段中放置对组件的引用。 请确保该字段是组件的类型。
清单 3-17 引用子组件
@page "/"
<h1>Hello, world!</h1>
<DismissibleAlert @bind-Show="ShowAlert" @ref="alert">
<span class="oi oi-check mr-2" aria-hidden="true"></span>
<strong>Blazor is so cool!</strong>
</DismissibleAlert>
<button @onclick="ToggleAlert" class="btn btn-success">Toggle</button>
<Timer TimeInSeconds="5" Tick="@(() => alert.Dismiss())" />
@code {
public bool ShowAlert { get; set; } = true;
public void ToggleAlert()
{
ShowAlert = !ShowAlert;
}
private DismissibleAlert alert = default!;
}
在此示例中,Blazor 运行时会将对 DismissibleAlert
组件的引用放在警报字段中。 您可以使用 @ref
语法指示 Blazor 执行此操作。当计时器在 5 秒后调用其 Tick 参数时,我们使用此引用来调用 DismissibleAlert
的 Dismiss 方法。
与级联参数通信
当更高级别的组件想要将数据传递给直接子级时,生活很容易。只需使用数据绑定。 但是当更高级别的组件需要与更深的嵌套组件共享一些数据时,使用数据绑定传递数据需要每个中间组件通过参数公开该数据并将其传递到下一个级别。 当您拥有多个级别的组件时,这不仅不方便,而且谁说您可以控制这些组件? Blazor 通过级联值和参数解决了这个问题。 让我们看一个例子。
打开 MyFirstBlazor 并添加清单 3-18 中的 CounterData
类。
清单 3-18 CounterData 类
using System;
namespace Components
{
public class CounterData
{
private int count;
public int Count
{
get => this.count;
set
{
if (value != count)
{
this.count = value;
CountChanged?.Invoke(this.count);
}
}
}
public Action<int>? CountChanged { get; set; }
}
}
使用 CascadingValue 组件
我们的顶级组件(称为 GrandMother
)希望将此数据作为级联值传递给任何后代组件。 您可以为此使用 Blazor 内置 CascadingValue
组件。 查看清单 3-19 中使用 CascadingValue
组件的示例。 在这里,我们将 GrandMother
的数据字段(CounterData 类型)作为级联值传递。 作为 ChildContent
一部分的任何组件现在都可以从 GrandMother
访问 CounterData
实例。
清单 3-19 使用
CascadingValue
组件将数据传递给后代
<h3>GrandMother</h3>
@data.Count
<CascadingValue Value="@this.data">
@ChildContent
</CascadingValue>
@code {
public CounterData data = new CounterData { Count = 10 };
protected override void OnInitialized()
{
this.data.CountChanged += (newCount) =>
this.StateHasChanged();
}
[Parameter]
public RenderFragment ChildContent { get; set; } = default!;
}
打开 Index.razor
并添加 GrandMother
组件,如清单 3-20 所示。 该组件有两个子组件,一个是直接的 GrandChild
组件(我们将在此之后构建),另一个是包装在 DismissibleAlert
组件中的 GrandChild
组件。 最后一个组件对 CounterData
或 GrandMother
一无所知。 尽管如此,GrandMother 组件将能够将其级联值传递给 GrandChild
组件。
清单 3-20 使用 GrandMother 组件
@page "/"
<h1>Hello, world!</h1>
<DismissibleAlert @bind-Show="ShowAlert" @ref="alert">
<span class="oi oi-check mr-2" aria-hidden="true"></span>
<strong>Blazor is so cool!</strong>
</DismissibleAlert>
<button @onclick="ToggleAlert" class="btn btn-success">Toggle</button>
<Timer TimeInSeconds="5" Tick="@(() => alert.Dismiss())" />
<GrandMother>
<GrandChild/>
<DismissibleAlert Show="true">
<GrandChild/>
</DismissibleAlert>
</GrandMother>
@code {
public bool ShowAlert { get; set; } = true;
public void ToggleAlert()
{
ShowAlert = !ShowAlert;
}
private DismissibleAlert alert = default!;
}
GrandChild
组件可以在清单 3-21 中找到(请将此作为另一个组件添加到 Pages
文件夹中)。 这个组件有一个 CounterData
类型的属性,它会通过添加 CascadingParameter
属性从 GrandMother
接收它。现在 GrandMother
和 GrandChild(ren) 共享同一个 CounterData
实例。 如果这看起来很神奇,CascadingParameter
将搜索 CascadingParameter
类型的所有级联属性。 如果有多个级联属性,您可以添加一个名称以获得更具体的匹配,我们将在接下来讨论。
清单 3-21 接收级联值
<h3>GrandChild</h3>
<button @onclick="Increment">Inc</button>
@code {
[CascadingParameter()]
public CounterData gmData { get; set; } = default!;
private void Increment()
{
gmData.Count += 1;
}
}
当您单击 GrandChild
的 Inc 按钮时,CounterData
的 Count
属性会递增。 GrandMother
组件希望在每次递增时显示此值,因此 CounterData
通知 GrandMother
更改。 GrandMother
组件订阅这些更改并调用 StateHasChanged
来更新自己。共享对象如何处理此通知取决于您; 例如,CounterData
使用委托。 您也可以使用 INotifyPropertyChanged
。 如果你不熟悉这个接口,很多 .NET 应用程序都使用它来通知相关方某个属性已更改。 例如,Windows Presentation Foundation (WPF) 严重依赖此接口。
解决歧义
如果有多个组件暴露相同类型的级联值怎么办? 在这种情况下,您可以命名级联值。 例如,您可以命名 GrandMother
的级联值,如清单 3-22 所示。
清单 3-22 在 GrandMother 中使用命名级联值
<CascadingValue Value="@this.data" Name="gm">
@ChildContent
</CascadingValue>
然后 GrandChild
组件应该接收到如清单 3-23 中所示的级联值。
清单 3-23 接收命名的级联值
[CascadingParameter(Name = "gm")]
public CounterData gmData { get; set; } = default!;
理解组件生命周期
Blazor 组件具有与任何其他 .NET 对象一样的生命周期。一个组件诞生,经历了几次变化,然后死亡。 Blazor 组件有几个方法可以覆盖以捕获组件的生命周期。 在本节中,我们将研究这些生命周期钩子,因为理解它们非常重要。将代码放在错误的生命周期钩子中可能会破坏您的组件。
您还应该记住,每个组件的每个生命周期方法至少调用一次。 即使是没有参数的组件,也会看到像 SetParametersAsync
和 OnParametersSetAsync
这样的方法至少被调用一次。
生命周期概述
让我们从大局开始。 我从清单 3-24 和 3-25 创建了一个 LifeCycle
组件来进行试验,它在浏览器的控制台上显示了每个生命周期挂钩。 所以列出了每个生命周期方法,除了两个生命周期方法的异步版本:OnInitializedAsync
和 OnParametersSetAsync
。 如果你想跟随,你可以使用本书附带的示例代码。
清单 3-24
LifeCycle
组件的代码
using Microsoft.AspNetCore.Components;
using System;
using System.Threading.Tasks;
namespace Components.Pages
{
public partial class LifeCycle
{
public LifeCycle()
{
Console.WriteLine("Inside constructor");
}
private int counter;
[Parameter]
public int Counter
{
get => counter;
set
{
counter = value;
Console.WriteLine($"Counter set to {counter}");
}
}
public override Task SetParametersAsync(ParameterView parameters)
{
Console.WriteLine("SetParametersAsync called");
return base.SetParametersAsync(parameters);
}
protected override void OnParametersSet()
=> Console.WriteLine("OnParametersSet called");
protected override void OnInitialized()
=> Console.WriteLine("OnInitialized called");
protected override void OnAfterRender(bool firstRender)
=> Console.WriteLine($"OnAfterRender called with firstRender ={firstRender}");
protected override bool ShouldRender()
{
Console.WriteLine($"ShouldRender called");
return true;
}
public void Dispose()
=> Console.WriteLine("Disposed");
}
}
清单 3-25
LifeCycle
组件的标记
@implements IDisposable
<h3>LifeCycle @Counter</h3>
清单 3-25 还展示了如何使用 @implements
语法在组件中实现接口。 我还将这个组件添加到 Index
组件中,如清单 3-26 所示。 让我们运行它并检查输出。
清单 3-26 使用生命周期组件
@page "/"
...
<LifeCycle Counter="@counter" />
<button class="btn btn-primary" @onclick="Increment">Increment</button>
@code {
private int counter = 1;
public void Increment()
{
counter += 1;
}
...
}
创建 Index
组件时,它将创建嵌套的 LifeCycle
组件,从而产生以下输出:
Inside constructor
SetParametersAsync called
Counter set to 1
OnInitialized called
OnParametersSet called
OnAfterRender called with firstRender = True
LifeCycle
组件被构造(调用构造函数),然后 Blazor 调用 SetParametersAsync
方法。 此方法通常会导致调用参数设置器,这就是我们看到 Counter
属性输出的原因。
然后 Blazor 运行时调用 OnInitialized
方法(以及为简单起见我省略的异步 OnInitializedAsync
)。 在此之后,调用 OnParametersSet
方法(以及异步 OnParametersSetAsync
方法)。 现在组件已准备好呈现,Blazor 运行时呈现它。 最后,调用渲染 OnAfterRender
方法,该方法会传递一个布尔值,该布尔值在第一次渲染时为真。
整个过程如图所示。
我的 Index 组件有一个 Increment 按钮,当我单击此按钮时,输出如下:
SetParametersAsync called
Counter set to 2
OnParametersSet called
ShouldRender called
OnAfterRender called with firstRender = False
因为我单击了 Index
组件的 Increment
按钮,所以它会调用单击处理程序,然后重新呈现自身。 但首先它会在 LifeCycle
组件上设置参数,这会导致再次调用 SetParametersAsync
方法(这会设置 Counter
参数)。 在此之后,它调用 OnParametersSet
方法以指示所有参数都已更新(以及异步 OnParametersSetAsync
方法)。
现在,Blazor 运行时是否应该渲染组件? 为此,它调用 ShouldRender
方法,如果返回 true
,它将渲染 LifeCycle
组件(然后是 OnAfterRender
方法)。
单击“Increment”按钮会再次生成该序列。
现在切换到 FetchData
组件会导致 LifeCycle
组件被破坏,从而产生以下输出:
Disposed
现在让我们分别看一下这些方法。
SetParametersAsync
如果需要在设置参数之前执行一些代码,可以重写 SetParametersAsync
方法。 SetParametersAsync
方法的默认实现将设置在 ParameterView
参数中具有值的每个 [Parameter]
和 [CascadingParameter]
。 其他参数(在 ParameterView
中没有值)保持不变。
您可以在 ParameterView
参数中找到参数,该参数的行为类似于字典。 让我们看一下示例 3-27 中的示例。 此示例使用 SetParametersAsync
方法检查参数,寻找“计数器”参数。 如果这个参数是偶数,我们调用base
方法; 否则,我们什么也不做,结果是一个偶数的 Counter
。
有一个障碍; 当您不调用基本方法时,UI
不会更新,因此如果您希望组件更新,则应调用 StateHasChanged
。 最初,我们的 LifeCycle
组件可能会收到一个奇数值,这就是我们第一次调用 StateHasChanged
的原因。
再说一点:如果 LifeCycle
组件有其他参数,你的实现仍然负责设置这些参数,因为我们不会在每种情况下都调用基本的 SetParametersAsync
方法。
清单 3-27 覆盖 SetParametersAsync
private bool firstParametersSet = true;
public override Task SetParametersAsync(ParameterView parameters)
{
Console.WriteLine("SetParametersAsync called");
if (parameters.TryGetValue(nameof(Counter), out int counter))
{
// ignore odd values
if (counter % 2 == 0)
{
return base.SetParametersAsync(parameters);
}
if(firstParametersSet)
{
firstParametersSet = false;
StateHasChanged(); // Force render
}
}
return Task.CompletedTask;
}
OnInitialized and OnInitializedAsync
创建组件并设置参数后,将调用 OnInitialized
和 OnInitializedAsync
方法。 如果您想在创建组件后进行一些一次性的额外初始化,请实现这些方法之一,例如,从服务器获取一些数据,例如项目中的 FetchData
组件。 OnInitialized
方法仅在创建组件后调用一次。
将 OnInitialized
用于同步代码,如清单 3-28 所示。 在这里,我们执行同步代码,例如获取当前的 DateTime
。
清单 3-28 OnInitialized 生命周期钩子
DateTime created;
protected override void OnInitialized()
{
created = DateTime.Now;
}
使用 OnInitializedAsync
(清单 3-29)调用异步方法,例如,进行异步 REST 调用(我们将在后续章节中讨论如何进行 REST 调用)。
清单 3-29
OnInitializedAsync
生命周期钩子
protected override async Task OnInitializedAsync()
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>
("sample-data/weather.json");
}
OnParametersSet and OnParametersSetAsync
如果在更改参数后需要一个或多个参数来查找数据,则使用 OnParametersSet
或 OnParametersSetAsync
而不是 On Initialized
/OnInitializedAsync
方法。 每次数据绑定更新您的一个或多个参数时,这些方法都会再次被调用,因此它们非常适合计算属性、过滤等。例如,您可以有一个 DepartmentSelector
组件,允许用户从公司中选择一个部门 以及另一个将所选部门作为参数的 EmployeeList
组件。 然后,EmployeeList
组件可以在其 OnParametersSetAsync
方法中获取该部门的员工。
如果您只调用同步方法,请使用 OnParametersSet
(清单 3-30)。
清单 3-30 OnParametersSet 方法
DateTime lastUpdate;
protected override void OnParametersSet()
{
lastUpdate = DateTime.Now;
Console.WriteLine("OnParametersSet called");
}
如果需要调用异步方法,请使用 OnParametersSetAsync
(清单 3-31)。例如,从依赖于参数值的数据库中检索值应该以异步方式完成。 一般来说,任何使用时间超过 60 毫秒的方法都应该异步完成。
清单 3-31 OnParametersSetAsync 方法
[Parameter]
public DateTime Date { get; set; }
protected override async Task OnParametersSetAsync()
{
forecasts = await weatherService.GetForcasts(Date);
}
ShouldRender
ShouldRender
方法返回一个布尔值,指示是否应该重新渲染组件。 请注意,第一次渲染会忽略此 ShouldRender
方法,因此组件将至少渲染一次。 默认实现始终返回 true
。您希望重写此方法以阻止组件重新渲染。
让我们对 LifeCycle
组件进行更改,如清单 3-32 所示。 我们只希望它显示奇数值。 所以当 counter
是偶数时,我们告诉 Blazor
引擎不要渲染这个组件。
清单 3-32 实现 ShouldRender 方法
public override Task SetParametersAsync(ParameterView parameters)
{
shouldRender = true;
if (parameters.TryGetValue(nameof(Counter), out int counter))
{
// ignore odd values
if (counter % 2 == 0)
{
shouldRender = false;
}
}
return base.SetParametersAsync(parameters);
}
private bool shouldRender;
protected override bool ShouldRender()
{
return shouldRender;
}
OnAfterRender and OnAfterRenderAsync
在 Blazor 完全呈现组件后调用 OnAfterRender
和 OnAfterRenderAsync
方法。 这意味着浏览器的 DOM 已使用对 Blazor 组件所做的更改进行了更新。 使用这些方法调用需要从 DOM
访问元素的 JavaScript 代码。 此方法采用布尔 firstRender 参数,它允许您仅附加一次 JavaScript 事件处理程序。
使用清单 3-33 中所示的 OnAfterRender
调用同步方法,例如在 JavaScript 中。
清单 3-33 OnAfterRender 生命周期钩子
protected override void OnAfterRender(bool firstRender)
{
}
使用清单 3-34 所示的 OnAfterRenderAsync
调用异步方法,用于例如,返回 Promise
或 observable
的 JavaScript 方法。
清单 3-34 OnAfterRenderAsync 生命周期钩子
protected override Task OnAfterRenderAsync(bool firstRender)
{
}
IDisposable
如果您需要在从 UI 中删除组件时运行一些清理代码,请实现 IDisposable
接口。 您可以使用 @implements
语法在 razor 中实现此接口,例如清单 3-25。 通常,您将@implements
放在 .razor
文件的顶部,但如果您使用代码分离,您也可以在部分类上声明它。
大多数情况下,依赖注入会负责调用 Dispose
,因此,如果您只需要释放依赖项,通常不需要实现 IDisposable
。
IDisposable
接口要求您实现一个 Dispose
方法,如清单 3-35 所示。
清单 3-35 实现 Dispose 方法
public void Dispose()
{
// Cleanup code here
}
关于异步方法的一句话
当 Blazor 运行时调用 OnInitializedAsync
和 OnParametersSetAsync
之类的异步方法时,它会等待该方法并渲染组件。唯一的例外是 OnAfterRenderAsync
方法,它不会触发渲染(否则会导致无限渲染循环 )。
这就是您应该始终检查在异步方法中初始化的变量是否存在空值的原因。 清单 3-36 中的 FetchData
组件就是一个很好的例子。 预测字段在 OnInitializedAsync
方法中被初始化,因此在该方法完成之前,预测字段为空。这意味着我们应该检查该字段的空值,如示例 3-37 所示。
清单 3-36 初始化预测
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>
("sample-data/weather.json");
}
清单 3-37 检查 Null 的预测
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
将 PizzaPlace 重构为组件
创建一个组件以显示比萨饼列表
打开上一章中的 PizzaPlace Blazor 项目。 您也可以从本书中的代码示例开始; 查找包含完成版本的第 2 章。从查看 Index.razor 开始。 这是我们的主要组件,可以说它包含三个主要部分:菜单、购物篮和客户信息。
菜单遍历比萨饼列表,并显示每个比萨饼,并带有一个可供订购的按钮。 购物篮还显示比萨饼列表(但现在来自购物篮),并带有一个按钮以将其从订单中删除。 看起来两者有共同点; 他们需要通过单击按钮来显示具有您选择的操作的比萨饼。 因此,让我们创建一个组件来显示比萨饼列表,使用嵌套组件来显示比萨饼的详细信息。
我们还看到,我们可以将组件拆分为带有标记的 razor
文件和带有代码的 C# 文件。 让我们在这里做!
将一个名为 PizzaItem.razor
的新组件添加到 Pages 文件夹。 同时创建一个名为 PizzaItem.razor.cs
的新类。 用清单 3-38 中的代码替换这个类。 您应该能够从 Index 复制大部分代码。
清单 3-38 PizzaItem 组件的代码
using Microsoft.AspNetCore.Components;
using PizzaPlace.Shared;
namespace PizzaPlace.Client.Pages
{
public partial class PizzaItem
{
[Parameter]
public Pizza Pizza { get; set; } = default!;
[Parameter]
public string ButtonTitle { get; set; } = default!;
[Parameter]
public string ButtonClass { get; set; } = default!;
[Parameter]
public EventCallback<Pizza> Selected { get; set; }
private string SpicinessImage(Spiciness spiciness)
=> $"images/{spiciness.ToString().ToLower()}.png";
}
}
现在将 razor
文件替换为清单 3-39 中的内容。 您可以从 Index
组件(第一个 @foreach
中的部分)复制大部分标记并进行一些更改。
清单 3-39 PizzaItem 组件
<div class="row">
<div class="col">
@Pizza.Name
</div>
<div class="col text-right">
@($"{Pizza.Price:0.00}")
</div>
<div class="col"></div>
<div class="col">
<img src="@SpicinessImage(Pizza.Spiciness)"
alt="@Pizza.Spiciness" />
</div>
<div class="col">
<button class="@ButtonClass"
@onclick="@(() => Selected.InvokeAsync(Pizza))">
Add
</button>
</div>
</div>
PizzaItem
组件将显示一个比萨饼,因此它有一个 Pizza
参数并不奇怪。 这个组件也显示了一个按钮,但是这个按钮的外观和行为在我们使用它的地方会有所不同。 这就是为什么它有一个 ButtonTitle
和 ButtonClass
参数来改变按钮的外观,它还有一个 EventCallback<Pizza>
类型的 Selected
事件回调,当你单击按钮时它会被调用。 你还记得我们为什么使用 EventCallback<T>
而不是 Action<T>
吗? 请注意,此组件做好一件事,而且只有一件事:显示比萨饼并允许通过单击按钮对比萨饼执行操作。
我们现在可以使用这个组件来显示菜单(比萨饼列表)。 将一个名为 PizzaList.razor
(和 PizzaList.razor.cs
)的新组件添加到 Pages
文件夹中,如清单 3-40 和 3-41 所示。
清单 3-40 PizzaList 组件的代码
using Microsoft.AspNetCore.Components;
using PizzaPlace.Shared;
using System.Collections.Generic;
namespace PizzaPlace.Client.Pages
{
public partial class PizzaList
{
[Parameter]
public string Title { get; set; } = default!;
[Parameter]
public IEnumerable<Pizza> Items { get; set; } = default!;
[Parameter]
public string ButtonClass { get; set; } = default!;
[Parameter]
public string ButtonTitle { get; set; } = default!;
[Parameter]
public EventCallback<Pizza> Selected { get; set; }
}
}
清单 3-41 PizzaList 组件的标记
@if (Items is null || !Items.Any())
{
<div>Loading...</div>
}
else
{
<h1>@Title</h1>
@foreach (var pizza in Items)
{
<PizzaItem Pizza="@pizza"
ButtonClass="@ButtonClass"
ButtonTitle="@ButtonTitle"
Selected="@Selected" />
}
}
首先注意@if
的使用。 在这里,我们需要决定如果 Items
属性(它是一个 IEnumerable<Pizza>
)为 null 或空,该怎么办。 在这种情况下,我们将显示一个加载 UI,假设稍后将填充 Items
集合。
否则,PizzaList
组件会显示 Title
和 Items
集合中的所有比萨饼,因此它将这些作为参数。 它还接受一个 Selected
事件回调,您可以通过单击比萨饼旁边的按钮来调用该回调。 请注意,PizzaList 组件重用 PizzaItem
组件来显示每个比萨饼,并且 PizzaList Selected
事件回调直接传递给 PizzaItem Selected 事件回调。 按钮参数也是如此。 Index
组件会设置这个回调,它会被 PizzaItem 组件执行。
准备好 PizzaItem 和 PizzaList 组件后,我们可以在 Index 中使用它们,如清单 3-42 所示。
清单 3-42 在 Index.razor 中使用 PizzaList 组件
<!-- Menu -->
<PizzaList Title="Our Selection of Pizzas"
Items="@State.Menu.Pizzas"
ButtonTitle="Order"
ButtonClass="btn btn-success pl-4 pr-4"
Selected="@AddToBasket" />
<!-- End menu -->
运行应用程序并尝试订购披萨。 您选择的比萨饼应添加到购物篮中。 感谢 EventCallback<T>
类型,无需调用 StateHasChanged
。 如果我们使用 Action<T>
或 Func<T>
,UI 将不会更新,并且您需要在收到来自子组件的事件时调用 StateHasChanged
!
显示 ShoppingBasket 组件
将一个名为 ShoppingBasket.razor
的新 razor
组件(和代码隐藏文件)添加到 Pages
文件夹,并将其内容更改为清单 3-43 和 3-44。
清单 3-43 ShoppingBasket 组件的代码
using Microsoft.AspNetCore.Components;
using PizzaPlace.Shared;
using System;
using System.Collections.Generic;
using System.Linq;
namespace PizzaPlace.Client.Pages
{
public partial class ShoppingBasket
{
[Parameter]
public IEnumerable<int> Orders { get; set; } = default!;
[Parameter]
public EventCallback<int> Selected { get; set; } = default!;
[Parameter]
public Func<int, Pizza> GetPizzaFromId { get; set; }
= default!;
private IEnumerable<(Pizza pizza, int pos)> Pizzas
{ get; set; } = default!;
private decimal TotalPrice { get; set; } = default!;
protected override void OnParametersSet()
{
Pizzas = Orders.Select((id, pos)
=> (pizza: GetPizzaFromId(id), pos: pos));
TotalPrice = Pizzas.Select(tuple
=> tuple.pizza.Price).Sum();
}
}
清单 3-44 ShoppingBasket 组件的标记
@if (Orders is not null && Orders.Any())
{
<h1 class="">Your current order</h1>
@foreach (var (pizza, pos) in Pizzas)
{
<div class="row mb-2">
<div class="col">
@pizza.Name
</div>
<div class="col text-right">
@($"{pizza.Price:0.00}")
</div>
<div class="col"></div>
<div class="col"></div>
<div class="col">
<button class="btn btn-danger"
@onclick="@(() => Selected.InvokeAsync(pos))">
Remove
</button>
</div>
</div>
}
<div class="row">
<div class="col"></div>
<div class="col"><hr /></div>
<div class="col"> </div>
<div class="col"> </div>
</div>
<div class="row">
<div class="col"> Total:</div>
<div class="col text-right font-weight-bold">@
($"{TotalPrice:0.00}")</div>
<div class="col"> </div>
<div class="col"> </div>
<div class="col"> </div>
</div>
}
ShoppingBasket
组件类似于 PizzaList
组件,但有一些很大的不同(这就是我们不重用 PizzaList
组件的原因。我们将在下一章中这样做)。 ShoppingBasket
类(来自共享项目的那个)仅使用比萨饼的 id
来跟踪订单,因此我们需要一些东西来获取比萨饼对象。 这是通过 GetPizzaFromId
委托完成的(同样,我们不希望该组件对其他类有太多了解)。 另一个变化是 OnParametersSet
方法。 OnParametersSet
方法在组件的参数设置完成后被调用。 在这里,我们覆盖它以构建我们在数据绑定期间需要的 (pizza,position) 元组列表并计算订单的总价格。
元组只是 C# 中的另一种类型。 但是使用现代 C#,我们得到了这种非常方便的语法; 例如, IEnumerable<(Pizza Pizza, int pos)>
意味着我们有一个类型是比萨饼和位置对的列表。 将元组视为匿名类型的一个很好的替代品,它允许您快速拥有编译器生成的类型。
在 Index 中使用 ShoppingBasket
组件很简单,如清单 3-45 所示。
清单 3-45 使用 ShoppingBasket 组件
<!-- Shopping Basket -->
<ShoppingBasket Orders="@State.Basket.Orders"
GetPizzaFromId="@State.Menu.GetPizza"
Selected="@RemoveFromBasket" />
<!-- End shopping basket -->
再次运行您的项目。一切都应该仍然有效(并且看起来相同)。
添加 CustomerEntry 组件
将一个新的 CustomerEntry 组件添加到 Pages
文件夹中,如清单 3-46 和 3-47 所示。
清单 3-46 CustomerEntry 组件的代码
using Microsoft.AspNetCore.Components;
using PizzaPlace.Shared;
namespace PizzaPlace.Client.Pages
{
public partial class CustomerEntry
{
[Parameter]
public string Title { get; set; } = default!;
[Parameter]
public string ButtonTitle { get; set; } = default!;
[Parameter]
public string ButtonClass { get; set; } = default!;
[Parameter]
public Customer Customer { get; set; } = default!;
[Parameter]
public EventCallback ValidSubmit { get; set; } = default!;
}
}
清单 3-47 CustomerEntry 组件的标记
<h1 class="mt-2 mb-2">@Title</h1>
<EditForm Model="@Customer"
OnValidSubmit="@ValidSubmit">
<DataAnnotationsValidator />
<fieldset>
<div class="row mb-2">
<label class="col-2" for="name">Name:</label>
<InputText class="form-control col-6"
@bind-Value="@Customer.Name" />
</div>
<div class="row mb-2">
<div class="col-6 offset-2">
<ValidationMessage For="@(() => Customer.Name)" />
</div>
</div>
<div class="row mb-2">
<label class="col-2" for="street">Street:</label>
<InputText class="form-control col-6"
@bind-Value="@Customer.Street" />
</div>
<div class="row mb-2">
<div class="col-6 offset-2">
<ValidationMessage For="@(() => Customer.Street)" />
</div>
</div>
<div class="row mb-2">
<label class="col-2" for="city">City:</label>
<InputText class="form-control col-6"
@bind-Value="@Customer.City" />
</div>
<div class="row mb-2">
<div class="col-6 offset-2">
<ValidationMessage For="@(() => Customer.City)" />
</div>
</div>
<div class="row mb-2">
<button class="@ButtonClass">@ButtonTitle</button>
</div>
</fieldset>
</EditForm>
CustomerEntry
组件对每个客户属性使用 <label>
、InputText
和 ValidationMessage
。
现在我们准备完成索引组件。 清单 3-48 显示了整个 Index.razor 文件。
清单 3-48 Index 组件
@page "/"
<!-- Menu -->
<PizzaList Title="Our Selection of Pizzas"
Items="@State.Menu.Pizzas"
ButtonTitle="Order"
ButtonClass="btn btn-success pl-4 pr-4"
Selected="@AddToBasket" />
<!-- End menu -->
<!-- Shopping Basket -->
<ShoppingBasket Orders="@State.Basket.Orders"
GetPizzaFromId="@State.Menu.GetPizza"
Selected="@RemoveFromBasket" />
<!-- End shopping basket -->
<!-- Customer entry -->
<CustomerEntry Title="Please enter your details below"
Customer="@State.Basket.Customer"
ButtonTitle="Checkout"
ButtonClass="mx-auto w-25 btn btn-success"
ValidSubmit="PlaceOrder" />
<!-- End customer entry -->
@State.ToJson()
@code {
private State State { get; } = new State();
protected override void OnInitialized()
{
State.Menu.Add(
new Pizza(1, "Pepperoni", 8.99M, Spiciness.Spicy));
State.Menu.Add(new Pizza(2, "Margarita", 7.99M, Spiciness.None));
State.Menu.Add(
new Pizza(3, "Diabolo", 9.99M, Spiciness.Hot));
}
private void AddToBasket(Pizza pizza)
=> State.Basket.Add(pizza.Id);
private void RemoveFromBasket(int pos)
=> State.Basket.RemoveAt(pos);
private void PlaceOrder()
{
Console.WriteLine("Placing order");
}
}
构建并运行 PizzaPlace 应用程序。 事情应该像以前一样工作,除了一件事。 还记得上一章的调试技巧吗? 当您更改客户名称时,此提示不会正确更新。 只有在按下按钮后才会更新。 让我们解决这个问题。
使用级联属性
问题如下。 每当用户编辑客户的属性时,我们希望 CustomerEntry
组件触发 CustomerChanged
事件回调。 这样,UI 中的其他组件会因为客户的更改而更新。 但是我们如何才能检测到这些变化呢? 如果我们使用 <input>
元素,我们可以使用 onchanged
事件,但不幸的是,InputText
组件没有此事件。它确实有 ValueChanged
事件,但我不想在这里使用它(否则,我可以 不向您展示如何使用级联属性)。
再次查看 CustomerEntry
组件。 您会看到一个带有嵌套 InputText
组件的 EditForm
。 EditForm
提供了一个 EditContext
类型的级联值,InputText
组件使用这个 EditContext
进行验证等操作。
每当输入组件之一发生更改时,它都会调用 EditContext.NotifyFieldChanged
方法。 这就是事情变得有趣的地方,因为EditContext
有一个 OnFieldChanged
事件,每次模型的属性更改时都会触发该事件。
让我们构建一个使用 EditContext
的 OnFieldChanged
事件来通知我们更改的组件。 这样,我们不必为每个 Input
实现 ValueChanged
事件。
在客户端项目的 Pages
文件夹中添加一个新类,并将其命名为 InputWatcher
,实现如清单 3-49 所示。 InputWatcher
类有一个参数 FieldChanged
,类型为 EventCallback<string>
。 InputWatcher
接收与 InputText
组件使用的相同的 EditContext
实例(作为级联参数)。 通过订阅 EditContext
的 FieldChanged
事件,所有的工作都将由 EditContext
实例完成。
清单 3-49 InputWatcher 组件
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
namespace PizzaPlace.Client.Pages
{
public class InputWatcher : ComponentBase
{
private EditContext editContext = default!;
[CascadingParameter]
public EditContext EditContext
{
get => this.editContext;
set
{
this.editContext = value;
EditContext.OnFieldChanged += async (sender, e) =>
{
await FieldChanged.InvokeAsync(e.FieldIdentifier
.FieldName);
};
}
}
[Parameter]
public EventCallback<string> FieldChanged { get; set; }
public bool Validate()
=> EditContext?.Validate() ?? false;
}
}
设置 EditContext
属性后,InputWatcher
只需注册 FieldChanged
事件并调用其自己的 FieldChanged
事件回调。
让我们在 CustomerEntry
组件中使用 InputWatcher
。 在 EditForm
组件中添加 InputWatcher
组件,并添加一个 FieldChanged
事件回调,如清单 3-50 和 3-51 所示。 InputWatcher
组件调用 FieldChanged
方法,该方法触发 CustomerChanged
回调。
清单 3-50 使客户参数双向绑定
using Microsoft.AspNetCore.Components;
using PizzaPlace.Shared;
namespace PizzaPlace.Client.Pages
{
public partial class CustomerEntry
{
...
[Parameter]
public EventCallback<Customer> CustomerChanged { get; set; }
private void FieldChanged(string fieldName)
{
CustomerChanged.InvokeAsync(Customer);
}
}
}
清单 3-51 带有 CustomerChanged 回调的 CustomerEntry 组件
<EditForm Model="@Customer"
OnValidSubmit="@ValidSubmit">
<DataAnnotationsValidator />
<InputWatcher FieldChanged="@FieldChanged" />
为了完成这个故事,在 Index 组件中为 Customer 属性使用双向数据绑定,如清单 3-52 所示。
清单 3-52 为客户使用双向数据绑定
<CustomerEntry Title="Please enter your details below"
@bind-Customer="@State.Basket.Customer"
ButtonTitle="Checkout"
ButtonClass="mx-auto w-25 btn btn-success"
ValidSubmit="PlaceOrder" />
构建并运行。 当您对客户进行更改时,当您退出控件时,您应该会在调试提示中看到客户更新。 嘿,这一点都不难!
禁用提交按钮
只要存在验证错误,您可能希望禁用提交按钮。 我们新引入的 InputWatcher 允许我们这样做。 在清单 3-49 中查找 Validate
方法。 此方法调用 EditContext.Validate
方法。 我们将使用它来启用/禁用提交按钮。
首先对清单 3-53 和 3-54 进行更改。 首先,我们添加对 InputWatcher
的引用,因为每次字段更改时我们都需要调用 Validate
方法。 此外,添加一个布尔字段 isInvalid
,并通过将其绑定到按钮的禁用属性来使用它来禁用按钮。 最后,每次字段更改时,我们都会通过调用 Validate
方法更新 isInvalid
。
清单 3-53 禁用提交按钮
using Microsoft.AspNetCore.Components;
using PizzaPlace.Shared;
namespace PizzaPlace.Client.Pages
{
public partial class CustomerEntry
{
...
private void FieldChanged(string fieldName)
{
CustomerChanged.InvokeAsync(Customer);
isInvalid = !inputWatcher.Validate();
}
private InputWatcher inputWatcher = default!;
bool isInvalid = true;
}
}
清单 3-54 禁用提交按钮
<h1 class="mt-2 mb-2">@Title</h1>
<EditForm Model="@Customer"
OnValidSubmit="@ValidSubmit">
<DataAnnotationsValidator />
<InputWatcher FieldChanged="@FieldChanged" @ref="@inputWatcher" />
<fieldset>
<div class="row mb-2">
<label class="col-2" for="name">Name:</label>
<InputText class="form-control col-6"
@bind-Value="@Customer.Name" />
</div>
<div class="row mb-2">
<div class="col-6 offset-2">
<ValidationMessage For="@(() => Customer.Name)" />
</div>
</div>
<div class="row mb-2">
<label class="col-2" for="street">Street:</label>
<InputText class="form-control col-6"
@bind-Value="@Customer.Street" />
</div>
<div class="row mb-2">
<div class="col-6 offset-2">
<ValidationMessage For="@(() => Customer.Street)" />
</div>
</div>
<div class="row mb-2">
<label class="col-2" for="city">City:</label>
<InputText class="form-control col-6"
@bind-Value="@Customer.City" />
</div>
<div class="row mb-2">
<div class="col-6 offset-2">
<ValidationMessage For="@(() => Customer.City)" />
</div>
</div>
<div class="row mb-2">
<button class="@ButtonClass" disabled="@isInvalid">
@ButtonTitle
</button>
</div>
</fieldset>
</EditForm>
运行您的应用程序,并使某些客户属性无效(即空白)。 当您按下提交按钮(也称为结帐)时,您将收到验证错误,并且该按钮将自行禁用。 修复验证错误后,提交按钮将再次启用。 如果要立即启用按钮,请将 isInvalid
的初始值更改为 false
。