四、了解基本 Blazor 组件

在本章中,我们将了解 Blazor 模板附带的组件,以及开始构建我们自己的组件。了解用于构建 Blazor 网站的不同技术将有助于我们开始构建组件。

Blazor 将组件用于大多数事情,因此我们将在整本书中使用从本章中获得的知识。

我们将从理论开始这一章,并以创建一个组件来显示一些博客文章结束,该组件使用我们在上一章中创建的应用编程接口, 第 3 章介绍实体框架核心

在本章中,我们将涵盖以下主题:

  • 探索组件
  • 学习 Razor 语法
  • 因素
  • 编写我们的第一个组件

技术要求

在本章中,我们将开始构建我们的组件,为此,您将需要我们在上一章中创建的代码, 第 3 章引入实体框架核心。如果你已经按照前几章的说明去做了,那么你就可以走了;如果没有,那么一定要克隆/下载 repo。本章的起点可以在ch3文件夹中找到,完成的代码可以在ch4中找到。

你可以在https://github . com/PacktPublishing/Web-Development-wit-Blazor/tree/master/chapter 04找到本章最终结果的源代码。

在本章中,我们将使用 Blazor 服务器项目,因此请确保右键单击 MyBlogServerSide 项目,并选择设置为启动项目

探索组件

在 Blazor 中,组件是一个.razor文件,它可以包含一个小的、独立的功能(代码和标记)或者可以用作页面本身。一个组件也可以承载其他组件。本章将向我们展示组件如何工作以及如何使用它们。

有三种不同的方法我们可以创建一个组件:

  • 使用 Razor 语法,代码和 HTML 共享同一个文件
  • 将代码隐藏文件与.razor文件一起使用
  • 仅使用代码隐藏文件

我们将考虑不同的选择。我们接下来要浏览的模板都使用第一个选项,.razor文件,其中代码和 HTML 混合在同一个文件中。

模板中的组件如下:

  • counter
  • FetchData

计数器

counter页面显示一个按钮和一个计数器;如果我们按下按钮,计数器就会增加。我们现在将这一页拆开,以便更容易理解。

页面顶部是@page指令,它使得直接路由到组件成为可能,正如我们在这段代码中看到的:

@page "/counter"

如果我们启动MyBlogServerSide项目并将/counter添加到 URL 的末尾,我们会看到我们可以通过使用组件的路由来直接访问它。我们也可以让路线采用参数,但我们过一会儿会回到这个问题。

接下来,让我们探索一下代码。要向页面添加代码,我们使用@code语句,在该语句中,我们可以添加普通的 C#代码,如图所示:

@code {
    private int currentCount = 0;
    private void IncrementCount()
    {
        currentCount++;
    }
}

在前面的代码块中,我们有一个私有的currentCount变量,它被设置为0。然后我们有一个叫做IncrementCount()的方法,它将currentCount变量增加1

我们使用@符号显示当前值。在 Razor 中,@符号表示是时候进行一些编码了:

<p>Current count: @currentCount</p>

正如我们所看到的,Razor 非常聪明,因为它理解代码何时停止,标记何时继续,所以不需要添加额外的东西来从代码过渡到标记(下一节将详细介绍)。

正如我们在前面的例子中所看到的,我们将 HTML 标签与@currentCount混合在一起,Razor 理解了其中的区别。接下来,我们有一个按钮,它是更改值的触发器:

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

这是一个带有引导类的 HTML 按钮(让它看起来更好一点)。@onclick正在将按钮的 onclick 事件绑定到IncrementCount()方法。如果我们使用onclick,没有@,它将引用 JavaScript 事件而不起作用。所以,当我们按下按钮时,它会调用IncrementCount()方法(由图 4.1 中的 1 描绘),该方法递增变量(由 2 描绘),通过改变变量,UI 会自动更新(由 3 描绘),如图图 4.1 :

Figure 4.1 – The flow of the counter component

图 4.1–计数器组件的流量

对于 Blazor 网络组件和 Blazor 服务器来说,counter组件的实现方式是相同的。FetchData组件有两种不同的实现方式,原因很简单,Blazor Server 项目可以直接访问服务器数据,Blazor WebAssembly 需要通过 web API 访问。

我们对我们的应用编程接口使用相同的方法,这样我们就能感受到如何利用依赖注入 ( DI )以及当我们使用 Blazor 服务器时如何直接连接到数据库。

获取数据

下一个组件我们来看看是FetchData组件。它位于Pages/FetchData.razor文件夹中。

对于 Blazor WebAssembly 和 Blazor Server 来说,FetchData组件的主要实现看起来是一样的。这两个版本中,文件的顶部行以及获取数据的方式都有所不同。对于 Blazor 服务器,它看起来像这样:

@page "/fetchdata"
@using MyBlogServerSide.Data
@inject WeatherForecastService ForecastService

它定义了一个路由,添加了一个名称空间,并注入了一个服务。我们可以在MyBlogServerSide项目的Data文件夹中找到服务。

该服务是一个创建一些随机预测数据的类;代码如下所示:

public class WeatherForecastService
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",         "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };
    public Task<WeatherForecast[]> GetForecastAsync       (DateTime startDate)
    {
        var rng = new Random();
        return Task.FromResult(Enumerable.Range(1,          5).Select(index => new WeatherForecast
        {
            Date = startDate.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        }).ToArray());
    }
}

正如我们所看到的,它会生成摘要并对温度进行随机化。

FetchData组件的code部分,我们将找到调用服务的代码:

private WeatherForecast[] forecasts;
protected override async Task OnInitializedAsync()
{
    forecasts = await       ForecastService.GetForecastAsync(DateTime.Now);
}

代码将从服务中获取数据,并填充一个名为forecastsWeatherForecast数组。

MyBlogWebAssembly.Client项目中,情况看起来有些不同。首先,文件的前几行如下所示:

@page "/fetchdata"
@using MyBlogWebAssembly.Shared
@inject HttpClient Http

该代码使用page指令定义了一个路由,向我们的共享库中添加了一个名称空间,并注入了HttpClient而不是服务。HttpClient用来从服务器获取数据,这是一个比较现实的现实场景。

HttpClientProgram.cs文件中定义,并且具有与MyBlogWebAssembly.Server项目相同的基址,因为服务器项目托管客户端项目。

获取数据如下所示:

private WeatherForecast[] forecasts;
protected override async Task OnInitializedAsync()
{
    forecasts = await Http.GetFromJsonAsync       <WeatherForecast[]>("WeatherForecast");
}

代码将获取数据并填充一个名为forecastsWeatherForecast数组。但是我们没有从服务中获取数据,而是拨打了"WeatherForecast"的网址。我们可以在MyBlogWebAddembly.Server项目中找到 web API。

控制器(Controllers/WeatherForcastController.cs)看起来像这样(与服务有很多相似之处):

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };
    private readonly ILogger<WeatherForecastController> logger;
    public WeatherForecastController       (ILogger<WeatherForecastController> logger)
    {
        this.logger = logger;
    }
    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        var rng = new Random();
        return Enumerable.Range(1, 5).Select(index => new           WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

看起来和服务差不多,但是是作为网络应用编程接口实现的。由于两个版本中的数据看起来相同,因此获取数据(在两种情况下)将使用天气预报数据填充数组。

Pages/FetchData.razor中,显示天气数据的代码在 Blazor WebAssembly 和 Blazor Server 中都是这样的:

<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from a service.</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>
}

正如我们所看到的,通过使用 Razor 语法,我们可以无缝地将代码与 HTML 混合在一起。代码检查是否有任何数据——如果有,那么它将呈现该表;如果没有,将显示加载信息。我们完全控制 HTML,Blazor 不会在生成的 HTML 中添加任何内容。

有一些组件库可以让这个过程变得简单一点,我们将在下一章 第 5 章创建高级 Blazor 组件中了解一下。

现在我们知道了示例模板是如何实现的,是时候深入研究 Razor 语法本身了。

学习 Razor 语法

我喜欢 Razor 语法的一点是它很容易混合代码和 HTML 标签。这一部分将有很多理论来帮助我们了解 Razor 语法。

要从 HTML 过渡到代码(C#),我们使用@符号。有几种方法可以将代码添加到文件中:

  • Razor 代码块
  • 隐式 Razor 表达式
  • 显式剃刀表达式
  • 表达式编码
  • 指令

Razor 代码块

我们已经看到了一些代码块。一个code块看起来是这样的:

@code {
    //your code here
}

如果我们愿意,我们可以跳过code关键字,就像这样:

@{
    //your code here
}

在这些大括号中,我们可以像这样混合 HTML 和代码:

@{
    void RenderName(string name)
    {
        <p>Name: <strong>@name</strong></p>
    }
    RenderName("Steve Sandersson");
    RenderName("Daniel Roth");
}

注意RenderName()方法是如何从代码转换到段落标记并返回代码的;这是一个隐含的转变。

如果我们想要输出没有 HTML 标签的文本,我们可以使用text标签来代替段落标签,如下例所示:

<text>Name: <strong>@name</strong></text>

这将呈现与前面代码所示相同的结果,但没有段落标记。不会渲染text标签。

隐式剃刀表达式

当我们在 HTML 标签中添加代码时,隐式 Razor 表达式就是。

我们已经在FetchData例子中看到了这一点:

<td>@forecast.Summary</td>

我们从一个<td>标签开始,然后使用@符号切换到 C#,然后回到 HTML 作为结束标签。我们可以在方法调用中使用await关键字,但是除此之外,隐式 Razor 表达式不能包含任何空格。

我们不能使用隐式表达式来调用通用的方法,因为<>会将解释为 HTML。因此,为了解决这个问题,我们可以使用显式表达式。

明确的剃刀表达式

如果我们想在代码中使用空格,我们可以使用显式 Razor 表达式。只需用@符号后跟括号( )来编写代码。所以,它看起来像这样:@()

在这个例子中,我们从当前日期减去7天:

<td>@(DateTime.Now - TimeSpan.FromDays(7))</td>

我们还可以使用显式 Razor 表达式来连接文本;例如,我们可以这样连接文本和代码:

<td>Temp@(forecast.TemperatureC)</td>

输出将是<td>Temp42</td>

使用显式表达式,我们可以使用以下语法轻松调用泛型方法:

<td>@(MyGenericMethod<string>())</td>

Razor 引擎知道我们是否在使用代码。它还确保在输出到浏览器时将字符串编码为 HTML,称为表达式编码

表达编码

如果我们把 HTML 作为一个字符串,默认会被转义。以这个代码为例:

@("<span>Hello World</span>")

呈现的 HTML 如下所示:

&lt;span&gt;Hello World&lt;/span&gt;

要从字符串中输出实际的 HTML(这是我们稍后要做的事情),您可以使用以下语法:

@((MarkupString)"<span>Hello World</span>")

通过使用MarkupString,输出将是 HTML,它将显示 HTML 标记跨度。在某些情况下,一行代码是不够的;然后我们可以使用代码块。

指令

有一堆指令改变了组件被解析的方式或者可以启用功能。这些是跟随@符号的保留关键字。我们将讨论最常见和最有用的。

我们可以使用代码隐藏来编写代码,以便在代码和布局之间获得更多的分离。我发现布局和代码在同一个.razor文件中是非常好的。在本章的后面,我们将看看如何使用代码隐藏,而不是使用 Razor 语法。

在下面的例子中,我们将看看如何使用代码隐藏来做同样的事情。

添加属性

要给我们的页面添加一个属性,我们可以使用attribute指令:

@attribute [Authorize]

如果我们使用代码隐藏文件,我们将使用以下语法:

[Authorize]

添加接口

为了实现一个接口(在本例中为IDisposable,我们将使用以下代码:

@implements IDisposable

然后我们将在@code{}部分实现接口需要的方法。

为了在代码隐藏场景中做同样的事情,我们将在类名之后添加接口,如下例所示:

public class SomeClass : IDisposable {}

继承

要继承另一个类,我们应该使用以下代码:

@inherits TypeNameOfClassToInheritFrom

为了在代码隐藏场景中做到这一点,我们将在类名之后添加我们想要继承的类:

public class SomeClass : TypeNameOfClassToInheritFrom {}

无商标消费品

我们可以将我们的组件定义为通用组件。

泛型让我们可以制作让我们定义数据类型的组件,所以组件可以和任何数据类型一起工作。

为了将组件定义为通用组件,我们添加了@typeparam指令;然后我们可以像这样在组件的代码中使用类型:

@typeparam TItem
@code
{
      [Parameter]
      public List<TItem> Data { get; set; }
}

在创建可重用组件时,泛型超级强大,我们将在 第 6 章中回到泛型,通过验证构建表单。

更改布局

如果我们想要一个页面的特定布局(不是app.razor文件中指定的默认布局),我们可以使用@layout指令:

@layout AnotherLayoutFile

这样,我们的组件将使用指定的布局文件(这仅适用于具有@page指令的组件)。

设置命名空间

默认情况下,组件的命名空间将是我们项目的默认命名空间的名称,加上文件夹结构。如果我们希望我们的组件在一个特定的名称空间中,我们可以使用以下内容:

@namespace Another.NameSpace

设置路线

我们已经触及了@page指令。如果我们希望使用网址直接访问我们的组件,我们可以使用@page指令:

@page "/theurl"

该网址可以包含参数、子文件夹等,我们将在本章稍后部分讨论这些内容。

添加 using 语句

为了给我们的组件添加一个名称空间,我们可以使用@using指令:

@using System.IO

如果我们在许多组件中使用了名称空间,那么我们可以将它们添加到_Imports.razor文件中。这样,它将在我们创建的所有组件中可用。

现在我们更了解 Razor 语法是如何工作的。别担心,我们会有足够的时间练习的。还有一个指令我在这一部分没有涉及到,那就是inject。我们已经看过几次了,但是为了涵盖所有的基础,我们首先需要了解什么是 DI 以及它是如何工作的,我们将在下一节中看到。

理解依赖注入

DI 是一种软件模式,是实现控制反转 ( IoC )的技术。

IoC 是一个通用术语,意思是我们可以指示类需要一个类实例,而不是让我们的类实例化一个对象。我们可以说我们的类要么想要一个特定的类,要么想要一个特定的接口。类的创建在别处,它将创建什么类取决于 IoC。

说到 DI,它是 IoC 的一种形式,对象(类实例)通过构造函数、参数或服务查找来传递。

在 Blazor 中,我们可以通过提供实例化对象的方法来配置 DI。在 Blazor 中,这是我们应该使用的关键架构模式。我们已经看到了一些对它的引用,例如在Startup.cs中:

services.AddSingleton<WeatherForecastService>();

这里,我们说如果任何类想要WeatherForecastService,应用应该实例化一个WeatherForecastService类型的对象。在这种情况下,我们不使用接口;相反,我们可以创建一个接口,并这样配置它:

services.AddSingleton<IWeatherForecastService ,WeatherForecastService>();

在这种情况下,如果一个类请求IWeatherForecastService的实例,应用将实例化一个WeatherForecastService对象并返回它。我们在上一章 第三章介绍实体框架核心做了这个。我们创建了一个返回MyBlogApiServerSide实例的IMyBlogApi界面;当我们实现 WebAssembly 版本时,DI 将返回另一个类。

使用 DI 有很多好处。我们的依赖关系是松散耦合的,这意味着我们不会实例化类中的另一个类。相反,我们要求一个实例,这使得编写测试以及根据平台改变实现变得更加容易。

因为我们需要将它们传递到类中,所以任何外部依赖都会更加清晰。我们还可以设置在中心位置实例化对象的方式。我们在Startup.cs(针对 Blazor 服务器)和Program.cs(针对 WebAssembly)中配置 DI。

我们可以用不同的方式配置对象的创建,例如:

  • 一个
  • 审视
  • 短暂的

一个

当我们使用 singleton 时,我们网站的所有用户的对象都是一样的。该对象将只创建一次。

要配置单例服务,请使用以下命令:

services.AddSingleton<IWeatherForecastService ,WeatherForecastService>();

当我们想要与我们站点的所有用户共享我们的对象时,我们应该使用 singleton,但是要注意状态是共享的,所以不要存储任何与特定用户或用户偏好相关的数据,因为这会影响所有用户。

审视

当我们使用 scoped 时,将为每个连接创建一个新的对象,因为 Blazor Server 需要一个连接才能工作,所以只要用户有连接,它就是同一个对象。WebAssembly 没有作用域的概念,因为没有建立连接,所以所有代码都在用户的 web 浏览器内部运行。如果我们使用 scoped,它将像 Blazor WebAssembly 的 singleton 一样工作。如果想法是将服务范围扩大到当前用户,建议仍然使用 scoped。

要配置范围服务,请使用以下命令:

services.AddScoped<IWeatherForecastService ,WeatherForecastService>();

如果我们有属于用户的数据,我们应该使用 scoped。我们可以通过使用限定范围的对象来保持用户的状态。更多关于 第十一章管理国家

短暂的

通过使用 transient,每次我们请求时都会创建一个新对象。

要配置临时服务,请使用以下命令:

services.AddTransient<IWeatherForecastService ,WeatherForecastService>();

如果我们不需要保持任何状态,也不介意每次请求创建的对象,那么我们应该使用 transient。

现在我们知道如何配置服务,我们需要通过注入服务来开始使用它。

注入服务

注入服务有三种方式。

我们已经在FetchData组件代码中看到了第一种方法。我们可以在 Razor 文件中使用@inject指令:

@inject WeatherForecastService ForecastService

这将确保我们可以访问组件中的WeatherForecastService

第二种方法是通过添加Inject属性来创建属性,如果我们使用代码隐藏的话:

[Inject]
public WeatherForecastService ForecastService { get; set; }

第三种方法是,如果我们想将一个服务注入到另一个服务中,那么我们需要使用构造函数注入服务:

public class MyService
{
    public MyService(WeatherForecastService
      weatherForecastService)
    {
    }
}

现在我们知道了 DI 是如何工作的,以及为什么我们应该使用它。

在本章中,我们已经多次提到代码隐藏。在下一节中,我们将了解如何将代码隐藏与 Razor 文件一起使用,甚至完全跳过 Razor 文件。

弄清楚代码放在哪里

我们已经看到了直接在 Razor 文件中编写代码的例子。我更喜欢这样做,除非代码变得太复杂。

有四种方法可以编写组件:

  • 在 Razor 文件中
  • 在部分班级
  • 继承一个类
  • 仅代码

在 Razor 文件中

如果我们正在编写的文件没有那么复杂,那么在编写组件时不必切换文件就好了。正如我们在本章中已经介绍过的,我们可以使用@code指令将代码直接添加到我们的 Razor 文件中。如果我们想将代码移动到代码隐藏文件,那么我们只需要更改指令。对于剩下的代码,我们可以直接进入代码隐藏类。当我开始使用 Blazor 时,在同一个文件中编写代码和标记感觉很奇怪,但我建议您在开发网络应用时尝试一下。

但是许多开发人员更喜欢代码隐藏,将代码与布局分开。为此,我们可以使用分部类。

在部分班级

我们可以创建一个与 Razor 文件同名的分部类,只需添加.cs

如果你已经下载了 第三章引入实体框架核心的源代码(或者我们可以在 GitHub 上查看代码),你可以看看MyBlogServerSide项目中的FetchDataWithCodeBehind.razor.cs。我已经将所有代码移动到代码隐藏文件中;编译时的结果将与我们将代码保存在 Razor 文件中的结果相同。这只是偏好的问题。

代码隐藏如下所示:

public partial class FetchDataWithCodeBehind
{
    [Inject]
    public WeatherForecastService ForecastService { get; set; }
    private WeatherForecast[] forecasts;
    protected override async Task OnInitializedAsync()
    {
        forecasts = await ForecastService.GetForecastAsync           (DateTime.Now);
    }
}

可以看到,我们用的不是@inject,而是[Inject]。除此之外,我刚刚从 Razor 文件中复制了代码。

这不是使用代码隐藏文件的唯一方法;我们也可以从代码隐藏文件中继承。

继承一个类

我们也可以创建一个叫完全不同的东西的类(常见的是把它叫做和 Razor 文件一样的东西,最后加上Model,在我们的 Razor 文件中继承。为了做到这一点,我们需要继承ComponentBase。在部分类的情况下,类已经从ComponentBase继承,因为 Razor 文件是这样做的。

任何字段都需要受保护或公开(非私有),以便页面能够访问它们。如果我们不需要从自己的基类继承,我的建议是使用分部类。

这是代码隐藏类声明的一个片段:

public class FetchDataWithInheritsModel:ComponentBase

我们需要从ComponentBase或者从从ComponentBase继承的类继承。

在 Razor 文件中,我们将使用@inherits指令:

@inherits FetchDataWithInheritsModel

Razor 文件现在将继承我们的代码隐藏类(这是第一个可用的创建代码隐藏类的方法)。

部分和继承选项都是将代码移动到代码隐藏文件的简单方法。但是还有另一个选项可以完全跳过 Razor 文件,只使用代码。

仅代码

Razor 文件将在编译时生成代码。如果我们愿意,我们可以跳过 Razor 步骤,完全用代码编写我们的布局。

这个文件(CounterWithoutRazor.cs)在 GitHub 上有。

计数器示例如下所示:

using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Web;
namespace MyBlogServerSide.Pages
{
    [Route("/CounterWithoutRazor")]
     public class CounterWithoutRazor: ComponentBase
     {
        protected override void BuildRenderTree
          (RenderTreeBuilder builder)
        {
            builder.AddMarkupContent(0,               "<h1>Counter</h1>\r\n\r\n");
            builder.OpenElement(1, "p");
            builder.AddContent(2, "Current count: ");
            builder.AddContent(3,currentCount);
            builder.CloseElement();
            builder.AddMarkupContent(4, "\r\n\r\n");
            builder.OpenElement(5, "button");
            builder.AddAttribute(6, "class",               "btn btn-primary");
            builder.AddAttribute(7, "onclick",              EventCallback.Factory.Create<MouseEventArgs>                (this,IncrementCount));
            builder.AddContent(8, "Click me");
            builder.CloseElement();
        }
        private int currentCount = 0;
        private void IncrementCount()
        {
            currentCount++;
        }
             }
}

Razor 文件将首先被转换成看起来与前面代码大致相同的东西,然后代码被编译。它一个接一个地添加元素,最终呈现出 HTML。

代码中的数字是 Blazor 跟踪渲染树中每个元素的方式。有些人更喜欢像前面的代码块一样编写代码,而不是使用 Razor 语法;社区中甚至有人努力简化手动编写BuildRenderTree() 函数的过程。

我的建议是永远不要手动写这个,但是我一直把它保存在书中,因为它展示了 Razor 文件是如何被编译的。现在我们知道了如何使用代码隐藏,让我们来看看 Blazor 的生命周期事件以及它们何时被执行。

生命周期事件

有几个生命周期事件我们可以用来运行我们的代码。在这一节中,我们将仔细研究它们,看看何时应该使用它们。大多数生命周期事件都有两个版本——同步和异步。

初始化和初始化同步

当组件满载时,调用OnInitialized(),然后调用OnInitializedAsync()。这是一个很好的方法来加载任何数据,因为用户界面还没有呈现。如果我们正在做任何长期运行的任务(比如从数据库中获取数据),我们应该把代码放在OnInitializedAsync()方法中。

如果参数改变,这些方法将不再运行(见OnParameterSet()OnParameterSetAsync())。

OnParametersSet 和 OnParametersSetAsync

当组件被初始化时(在OnInitialized()OnInitializedAsync()之后),每当我们改变一个参数的值时,调用OnParameterSet()OnParameterSetAsync()

例如,如果我们在OnInitialized()方法中加载数据,但它确实使用了一个参数,那么如果参数改变,数据将不会被重新加载,因为OnInitialized()将只运行一次。当然,我们需要在OnParameterSet()OnParameterSetAsync()中触发数据的重新加载,或者将加载移动到该方法。

OnAfterRender 和 OnAfterRenderAsync

组件完成渲染后,调用OnAfterRender()OnAfterRenderAsync()方法。当方法被调用时,所有的元素都被渲染,所以如果我们想要/需要调用任何 JavaScript 代码,我们必须从这些方法中调用(如果我们试图从任何其他生命周期事件方法中进行 JavaScript 互操作,我们将会得到一个错误)。我们还可以访问一个firstRender参数,这样我们就可以确保只运行一次初始化代码(只在第一次渲染时)。

应该渲染

当我们的组件被重新渲染时ShouldRender()被称为,如果它返回false,那么组件将不再被渲染。它将总是渲染一次;只有在重新呈现时,方法才会运行。

ShouldRender()没有异步选项。

现在我们知道了不同的生命周期事件何时发生以及以什么顺序发生。一个组件也可以有参数,这样,我们可以重用它们,但是使用不同的数据。

参数

一个参数使得可以向组件发送一个值。要向组件添加参数,我们使用public属性上的[Parameter]属性:

@code {
    [Parameter]
    public string MyParameter { get; set; }
}

我们也可以使用代码隐藏文件来做同样的事情。我们可以使用@page指令通过在路线中指定来添加参数:

@page "/parameterdemo/{MyParameter}"

在这种情况下,我们必须指定一个与花括号内的名称同名的参数。要在@page指令中设置参数,我们只需转到网址:/parameterdemo/THEVALUE

在某些情况下,我们希望指定另一种类型而不是字符串(字符串是默认值)。我们可以在参数名称后添加数据类型,如下所示:

@page "/parameterdemo/{MyParameter:int}"

只有当数据类型为整数时,这才会匹配路由。我们也可以使用级联参数传递参数。

级联参数

如果我们想将一个值传递给多个组件,我们可以使用级联参数。

不用[Parameter],我们可以这样用[CascadingParameter]:

[CascadingParameter]
public int MyParameter { get; set; }
To pass a value to the component, we surround it with a CascadingValue component like this:
<CascadingValue Value="MyProperty">
    <ComponentWithCascadingParameter/>
</CascadingValue> @code {
    public string MyProperty { get; set; } = "Test Value";
}

CascadingValue是我们传递给组件的值,CascadingParameter是接收该值的属性。

如我们所见,我们没有向ComponentWithCascadingParameter组件传递任何参数值;级联值将匹配数据类型相同的参数。如果我们有多个相同类型的参数,我们可以用级联参数在组件中指定参数的名称,如下所示:

[CascadingParameter(Name = "MyCascadingParameter")]

我们也可以对通过CascadingValue的组件这样做,如下所示:

<CascadingValue Value="MyProperty" Name="MyCascadingParameter">
    <ComponentWithCascadingParameter/>
</CascadingValue>

如果我们知道值不会改变,我们可以通过使用IsFixed属性来指定:

<CascadingValue Value="MyProperty" Name="MyCascadingParameter" IsFixed="True">
    <ComponentWithCascadingParameter/>
</CascadingValue>

这样,Blazor 就不会寻找变化。级联值/参数不能向上更新,只能向下更新。这意味着要更新级联值,我们需要以另一种方式实现它;从组件内部更新它不会改变层次结构中较高的任何组件。

第 5 章创建高级 Blazor 组件中,我们将查看单向解决级联值更新问题的事件。

唷!这是一个信息密集型的章节,但是现在我们知道了 Blazor 组件的基础知识。现在是时候建造一个了!

编写我们的第一个组件

我们将构建的第一个组件显示一个网站上的所有博客文章。平心而论,我们还没有写任何博客文章,但我们会暂时解决这个问题,这样我们就可以开始做一些有趣的事情。

第三章引入实体框架核心中,我们创建了一个数据库和一个 API(或接口);现在是使用它们的时候了。

我们首先想看到的是一个博文列表,所以我们希望我们的路线是"/"。索引页面已经有了这个路径,所以我们要重用这个页面。

要创建我们的第一个组件,请遵循以下说明:

  1. MyBlogServerSide项目中,打开Pages/Index.razor
  2. Replace the contents of that file with the following code:

    cs @page "/" @using MyBlog.Data.Interfaces @using MyBlog.Data.Models @inject IMyBlogApi api @code{     protected async Task AddSomePosts()     {         for (int i = 1; i <= 10; i++)         {             await api.SaveBlogPostAsync(new BlogPost()             {                 PublishDate = DateTime.Now, Title =                  $"Blog post {i}", Text = "Text"             });          }     } }

    如果我们从顶部开始,我们可以看到一个页面指令。它将确保在路线为"/"时显示组件。然后,我们有三个@using指令,引入名称空间,这样我们就可以在 Razor 文件中使用它们。然后我们注入我们的应用编程接口(使用数据接口)并将实例命名为api。在code部分,有一种方法是在我们的网站上添加 10 篇博文。接下来,我们应该列出博客文章。

  3. Add a variable that holds all our posts. In the code section, add the following:

    cs protected List<BlogPost> posts = new List<BlogPost>();

    现在我们需要加载数据。

  4. To load posts, add the following in the code section:

    cs protected override async Task OnInitializedAsync() {     posts = await api.GetBlogPostsAsync(10, 0);     await base.OnInitializedAsync(); }

    现在,当页面加载时,帖子也会被加载:10帖子和页面0(第一页)。

  5. Under the @inject row, add the following code:

    cs <button @onclick="AddSomePosts">Add some fake posts</button> <br /> <br /> <ul>     @foreach (var p in posts)     {         <li>@p.Title</li>     } </ul>

    我们从增加一个按钮开始,这样我们就可以触发AddSomePosts功能。然后我们添加一个无序列表(ul),在其中,我们循环遍历blogposts并显示标题。

  6. 现在我们可以通过按下 Ctrl + F5 ( 调试 | 启动而不调试)来运行应用。

  7. 出现的页面应该只是显示一个添加一些假帖子按钮。点击此按钮。
  8. 由于页面不会重新加载,OnInitializedAsync()也不会运行。我们需要重新加载我们的网络浏览器来显示数据。在现实世界的应用中,我们不希望用户必须重新加载浏览器,但是由于这一步只是暂时的,我不想让事情过于复杂。

干得好,我们已经创建了第一个组件!

总结

在这一章中,我们学习了很多关于 Razor 语法的知识,我们将在整本书中用到它。我们了解了 DI、指令和参数,当然,也创建了我们的第一个组件。这些知识将帮助我们理解如何创建组件以及如何重用组件。

在下一章中,我们将了解更高级的组件场景。