第三章-Blazor 应用程序的组件和结构

2022/9/5 1:24:38

本文主要是介绍第三章-Blazor 应用程序的组件和结构,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

什么是 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 前面显示一个图标,如图所示,然后显示一个调查链接。

image

@code 部分只包含一个属性 Title,它使用单向数据绑定在组件中呈现。 请注意 Title 属性上的 [Parameter] 属性。 对于想要将其公共属性公开给父组件的组件,这是必需的。 这样,我们就可以将数据传递给嵌套组件,例如 Index 组件如何将 Title 传递给 SurveyPrompt 组件。

使用 Razor 构建简单的警报组件

使用 Visual Studio 创建新组件

打开 MyFirstBlazor 解决方案。 右键单击 Pages 文件夹并选择 Add ➤ New Item...。 Add New Item 窗口应打开,如图所示。

image

选择 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 所示,用于警报组件及其参数!

image

完成 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 所示。

image

分离视图和视图模型

您可能不喜欢这种标记(视图)和代码(视图模型)的混合。 如果您愿意,可以使用两个单独的文件,一个用于使用 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">&times;</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应自动隐藏,如图所示。

image

image

如果您的项目仍未更新,您可以通过添加断点或一些 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 组件。 最后一个组件对 CounterDataGrandMother 一无所知。 尽管如此,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 按钮时,CounterDataCount 属性会递增。 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 组件有几个方法可以覆盖以捕获组件的生命周期。 在本节中,我们将研究这些生命周期钩子,因为理解它们非常重要。将代码放在错误的生命周期钩子中可能会破坏您的组件。
您还应该记住,每个组件的每个生命周期方法至少调用一次。 即使是没有参数的组件,也会看到像 SetParametersAsyncOnParametersSetAsync 这样的方法至少被调用一次。

生命周期概述

让我们从大局开始。 我从清单 3-24 和 3-25 创建了一个 LifeCycle 组件来进行试验,它在浏览器的控制台上显示了每个生命周期挂钩。 所以列出了每个生命周期方法,除了两个生命周期方法的异步版本:OnInitializedAsyncOnParametersSetAsync。 如果你想跟随,你可以使用本书附带的示例代码。

清单 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 方法,该方法会传递一个布尔值,该布尔值在第一次渲染时为真。

整个过程如图所示。

image

我的 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

创建组件并设置参数后,将调用 OnInitializedOnInitializedAsync 方法。 如果您想在创建组件后进行一些一次性的额外初始化,请实现这些方法之一,例如,从服务器获取一些数据,例如项目中的 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

如果在更改参数后需要一个或多个参数来查找数据,则使用 OnParametersSetOnParametersSetAsync 而不是 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 完全呈现组件后调用 OnAfterRenderOnAfterRenderAsync 方法。 这意味着浏览器的 DOM 已使用对 Blazor 组件所做的更改进行了更新。 使用这些方法调用需要从 DOM 访问元素的 JavaScript 代码。 此方法采用布尔 firstRender 参数,它允许您仅附加一次 JavaScript 事件处理程序。

使用清单 3-33 中所示的 OnAfterRender 调用同步方法,例如在 JavaScript 中。

清单 3-33 OnAfterRender 生命周期钩子

protected override void OnAfterRender(bool firstRender)
{
}

使用清单 3-34 所示的 OnAfterRenderAsync 调用异步方法,用于例如,返回 Promiseobservable 的 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 运行时调用 OnInitializedAsyncOnParametersSetAsync 之类的异步方法时,它会等待该方法并渲染组件。唯一的例外是 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 参数并不奇怪。 这个组件也显示了一个按钮,但是这个按钮的外观和行为在我们使用它的地方会有所不同。 这就是为什么它有一个 ButtonTitleButtonClass 参数来改变按钮的外观,它还有一个 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 组件会显示 TitleItems 集合中的所有比萨饼,因此它将这些作为参数。 它还接受一个 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>InputTextValidationMessage

现在我们准备完成索引组件。 清单 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 组件的 EditFormEditForm 提供了一个 EditContext 类型的级联值,InputText 组件使用这个 EditContext 进行验证等操作。

每当输入组件之一发生更改时,它都会调用 EditContext.NotifyFieldChanged 方法。 这就是事情变得有趣的地方,因为EditContext 有一个 OnFieldChanged 事件,每次模型的属性更改时都会触发该事件。

让我们构建一个使用 EditContextOnFieldChanged 事件来通知我们更改的组件。 这样,我们不必为每个 Input 实现 ValueChanged 事件。

在客户端项目的 Pages 文件夹中添加一个新类,并将其命名为 InputWatcher,实现如清单 3-49 所示。 InputWatcher 类有一个参数 FieldChanged,类型为 EventCallback<string>InputWatcher 接收与 InputText 组件使用的相同的 EditContext 实例(作为级联参数)。 通过订阅 EditContextFieldChanged 事件,所有的工作都将由 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



这篇关于第三章-Blazor 应用程序的组件和结构的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程