2019 年 3 月

第 34 卷,第 3 期

[领先技术]

分层 Blazor 组件

作者 Dino Esposito

Dino Esposito作为加入单页应用程序 (SPA) 队伍的最新框架,Blazor 有机会在其他框架(如 Angular 和 React)的最佳特性基础之上构建而成。尽管 Blazor 背后的核心概念是利用 C# 和 Razor 来生成 SPA 应用程序,但明显受到其他框架启发的一个方面是使用组件。

Blazor 组件是使用 Razor 语言编写而成,具体方式与生成 MVC 视图大致相同,而这正是让开发人员真正感兴趣的地方所在。在 ASP.NET Core 中,可以通过名为标记帮助器的新语言项目,实现前所未有的表达水平。标记帮助器是 C# 类,旨在通过分析给定标记树,将它转换为有效的 HTML5。可能会在创建复杂的定制 HTML 区块时面对的所有分支,都是在代码中进行处理;而且开发人员在文本文件中编写的所有内容都是纯文本标记。使用标记帮助器,代码片段数明显减少。虽然标记帮助器很有用,但仍存在一些编程缺陷,而 Blazor 组件则绝妙地消除了这些缺陷。在本文中,我将生成新的 Blazor 组件,以通过 Bootstrap 4 框架服务显示模式对话框。在此过程中,我将处理 Blazor 模板化组件和级联参数。

标记帮助器的缺陷

在我的“编程 ASP.NET Core”(Microsoft 出版社于 2018 年出版)一书中,我介绍了一个示例标记帮助器,它的作用几乎与前面介绍的相同。它将模式对话框的临时非 HTML 标记转换为 Bootstrap 专用标记(请访问 bit.ly/2RxmWJS)。

输入标记和相应输出之间的任何转换都是通过 C# 代码执行的。标记帮助器实际上是纯 C# 类,它继承自基类 TagHelper,并替代单一方法。问题在于,必须在代码中表达转换和标记组合。尽管这很大地提高了灵活性,但任何更改也都需要通过编译步骤完成。具体而言,需要使用 C# 代码来描述 DIV 树及其所有属性集和子元素。

在 Blazor 中,事情变得容易多了,因为无需为了创建复杂元素(如 Bootstrap 模式对话框)的更易记标记语法,而无奈地使用标记帮助器。接下来将介绍如何在 Blazor 中创建模式组件。

模式对话框

目的是要创建包装 Bootstrap 模式对话框组件的 Blazor 可重用组件。图 1 展示了熟悉的 HTML5 标记树,这是为 Bootstrap(3.x 和 4.x 版本)正常运行所必需。

图 1:模式对话框的 Bootstrap 标记

<button type="button" class="btn btn-primary"
        data-toggle="modal"
        data-target="#exampleModal">
  Open modal
</button>
<div class="modal">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title">Modal title</h5>
        <button type="button" class="close" data-dismiss="modal">
          <span>&times;</span>
        </button>
      </div>
      <div class="modal-body">
        <p>Modal body text goes here.</p>
      </div>
      <div class="modal-footer">
        <button type="button"
                class="btn btn-secondary"
                data-dismiss="modal">Close</button>
      </div>
    </div>
  </div>
</div>

没有 Web 开发人员乐意跨多个视图和页面一遍一遍地重复循环访问此标记区块。大部分标记是纯布局,且唯一的变量信息是要显示的文本,以及一些样式和按钮。下面是更易记且更具表达性的标记:

<Modal>
  <Toggle class="btn"> Open </Toggle>
  <Content>
    <HeaderTemplate> ... </HeaderTemplate>
    <BodyTemplate> ... </BodyTemplate>
    <FooterTemplate> ... </FooterTemplate>
  </Content>
</Modal>

模式组件的构成元素在更具表达性的标记代码中立即可见。此标记包含包装器 Modal 元素及其两个子级子树:一个用于切换按钮,一个用于实际内容。

根据模式的 Bootstrap 语法,任何对话框都需要显示触发器。通常情况下,触发器是使用一对数据切换属性和数据目标属性进行修饰的按钮元素。不过,模式也可以通过 JavaScript 触发。Toggle 子组件仅用作触发器标记的容器。相反,Content 子组件包装整个对话框的内容,并拆分为三段:页眉、正文和页脚。

总之,根据上面的代码片段,生成的 UI 由标记为“打开”的主按钮组成。 在获得单击后,此按钮便会立即弹出填充有以下三层的 DIV:页眉、正文和页脚。

必须处理模板化组件和级联参数,才能创建模式对话框所需的嵌套组件。请注意,必须运行 Blazor 0.7.0 或更高版本,才能使用级联参数。

模式组件

接下来看看图 2**** 中的代码。此标记相当简洁,并在模板化标记区块周围添加 DIV 元素。图 2 中的 modal.cshtml 文件声明 ChildContent 模板属性,用于收集(很明显)任何子内容。此标记的结果是将区块周围用来收集切换标记和实际内容的 DIV 元素推送出去,以在对话框中显示。

图 2:模式组件的源代码

<CascadingValue Value="@Context">
  <div>
    @ChildContent
  </div>
</CascadingValue>
@functions
{
  protected override void OnInit()
  {
    Context = new ModalContext
    {
      Id = Id,
      AutoClose = AutoClose
    };
  }
  ModalContext Context { get; set; }
  [Parameter] private string Id { get; set; }
  [Parameter] private bool AutoClose { get; set; }
  [Parameter] RenderFragment ChildContent { get; set; }
}

此容器组件貌似不是很有用。不过,在 Bootstrap 对话框所需的标记结构方面,它起到至关重要的作用。Toggle 和 Content 组件共用同一 ID,用来唯一标识模式对话框。使用包装器组件,可以仅在一个位置捕获 ID,并将它沿树向下级联。但在这种特殊情况下,ID 甚至不是要通过最靠中心标记层进行级联的唯一参数。模式对话框可视需要在页眉处添加“关闭”按钮,并添加与对话框大小或动画相关的其他属性。所有此类信息都可以在自定义数据传输对象中组合,并通过树进行级联。

ModalContext 类用于收集此关闭按钮的 ID 和布尔值,如下面的代码所示:

public class ModalContext
{
  public string Id { get; set; }
  public bool AutoClose { get; set; }
}

CascadingValue 元素捕获所提供的表达式,并自动将它与所有显式绑定到它的最靠中心组件共享。如果不使用级联参数功能,必须在任何需要的位置显式注入复杂的分层组件中的任何共享值。如果不使用此功能,必须指明同一 ID 两次,如下面的代码所示:

<Modal>
  <Toggle id="myModal" class="btn btn-primary btn-lg">
    ...
  </Toggle>
  <Content id="myModal">
    ...
  </Content>
</Modal>

如果必须沿由多个子组件组成的复杂组件的层次结构传递同一组值,级联值很有帮助。请注意,必须在一个容器中组合级联值;因此,如果需要传递多个标量值,应先定义容器对象。图 3**** 展示了参数如何通过模式组件的层次结构进行流动。

分层组件中的级联值
图 3:分层组件中的级联值

模式组件内部

Toggle 和 Content 组件负责以递归方式分析 Modal 组件的内部内容。下面是 Toggle.cshtml 组件的源代码:

<button class="@Class"
        data-toggle="modal"
        data-target="#@OutermostEnv.Id">
  @ChildContent
</button>
@functions
{
  [CascadingParameter] protected ModalContext OutermostEnv { get; set; }
  [Parameter] string Class { get; set; }
  [Parameter] RenderFragment ChildContent { get; set; }
}

在当前实现中,toggle 元素的样式是通过公共属性 Class 进行设置。按钮的内容是通过模板化属性 ChildContent 进行捕获。请注意,在 Blazor 中,模板属性 ChildContent 自动捕获父元素的整个子标记。此外,Blazor 中的模板属性是 RenderFragment 类型的属性。

上面源代码中有趣的地方是,绑定到级联值。使用 CascadingParameter 属性来修饰组件属性(如 OutermostEnv)。然后,此属性填充有来自最靠中心级别的级联值。这样一来,OutermostEnv 需要使用分配给 ModalContext 实例的值,此实例是在根组件的 Init 方法中刚创建的(见前面的图 2)。

在 Toggle 组件中,Id 级联值用于设置数据目标属性的值。在 Bootstrap 行话中,对话框切换按钮的数据目标属性标识,要在用户单击切换按钮时弹出的 DIV 的 ID。

模式对话框的内容

Bootstrap 对话框最多由三个垂直布局的 DIV 区块组成:页眉、正文和页脚。所有这些区块都是可选的,但建议至少定义一个,以便为用户提供最少程度的反馈。此时,模板化组件便刚好适合。下面是从 Content.cshtml 文件生成的 Content 组件的公共接口:

@functions
{
  [CascadingParameter] ModalContext OutermostEnv { get; set; }
  [Parameter] RenderFragment HeaderTemplate { get; set; }
  [Parameter] RenderFragment BodyTemplate { get; set; }
  [Parameter] RenderFragment FooterTemplate { get; set; }
}

OutermostEnv 级联参数会带来在 Content 组件范围之外定义的数据。其中同时使用了 ID 和 AutoClose 属性。Id 值用于标识对话框的最外面容器。使用 ID 签名的 DIV 会在模式触发时弹出。相反,AutoClose 值用于控制 IF 语句,此语句决定了是否应在标题栏中显示“关闭”按钮。

最后,三个 RenderFragment 模板属性定义可自定义区域(页眉、页脚和正文)的实际内容。

如图 4**** 所示,在呈现模式对话框的预期 Bootstrap 标记方面,Content 组件承担了大部分工作。它定义总体 HTML 布局,并使用模板属性导入标记的详细信息(页眉、页脚和正文标记),这些信息可确保给定对话框是唯一的。由于有了 Blazor 模板,任何实际标记都可以指定为调用方页中的内联内容。请注意,有关调用方页(在示例应用程序中称为 Cascade)的源代码,请参阅前面的图 3。

图 4:Content 组件的标记

<div class="modal" id="@OutermostEnv.Id">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <h5 class="modal-title">
          @HeaderTemplate
        </h5>
        @if (OutermostEnv.AutoClose)
        {
          <button type="button" class="close"
                  data-dismiss="modal">
            <span>&times;</span>
          </button>
        }
      </div>
      <div class="modal-body">
        @BodyTemplate
      </div>
      <div class="modal-footer">
        @FooterTemplate
      </div>
    </div>
  </div>
</div>

详细了解级联值和级联参数

级联值解决了沿子组件堆栈向下有效流动值的问题。级联值可以在复杂层次结构中的各种级别处进行定义,并能从上级组件流向它的所有后代。每个上级元素都可以定义一个级联值(可能是收集多个标量值的复杂对象)。

为了利用级联值,后代组件声明级联参数。级联参数是使用 CascadingParameter 属性进行修饰的公共属性或受保护属性。级联值可以与 Name 属性相关联,如下所示:

<CascadingValue Value=@Context Name="ModalDialogGlobals">
  ...
</CascadingValue>

在这种情况下,后代会使用 Name 属性来检索级联值,如下所示:

[CascadingParameter(Name = "ModalDialogGlobals")]
ModalContext OutermostEnv { get; set; }

如果未指定名称,级联值按类型绑定到级联参数。

总结

级联值专为分层组件而设计,但同时分层的模板化组件实际上是开发人员应编写的最常见类型 Blazor 组件。本文展示了级联参数以及分层的模板化组件,但同时也介绍了使用 Razor 组件通过更高级别语法表达特定标记片段的强大功能。具体而言,我生成了用于呈现 Bootstrap 模式对话框的自定义标记语法。请注意,可使用经典 ASP.NET MVC 中的标记帮助器或 HTML 帮助器,在纯 ASP.NET Core 中实现相同的效果。

可以从 bit.ly/2FdGZat 获取本文的源代码。


Dino Esposito 在他 25 年的职业生涯中撰写了超过 20 本的书籍和 1000 篇的文章。Esposito 不仅是舞台剧《事业中断》的作者,还是 BaxEnergy 的数字策略分析师,正忙于编写有助于建设环保世界的软件。可以在 Twitter 上关注他 (@despos)。

衷心感谢以下 Microsoft 技术专家对本文的审阅:Daniel Roth