Templating components with RenderFragments

到目前为止,我们已经创建了基于参数生成100%呈现输出的组件,但是组件并不总是那么简单。有时,我们需要创建将消费者提供的标记与其自己的呈现输出混合在一起的组件。
将内容作为HTML编码的字符串参数传递给组件将非常混乱(更不用说不可管理了):

  1. <Collapsible content="Lots of encoded HTML for your entire view here"/>

而且,除了维护的噩梦之外,嵌入式HTML也只能是基本的HTML标记,不能是Blazor组件。基本上,这是没有用的,显然这不应该是这样做的。正确的方法是使用RenderFragment

Child content

如果我们创建一个名为Collapsible的新组件(一个完全空的.razor文件),我们就可以在Index.razor页面中使用它,如下所示:

  1. <Collapsible/>

但是如果我们想要嵌入一些内容呢?试一试,然后查看浏览器控制台输出中的错误。

  1. <Collapsible>Hello world!</Collapsible>

WASM: System.InvalidOperationException: Object of type “TemplatedComponents.Components.Collapsible” does not have a property matching the name “ChildContent”. Error output when trying to embed content in a component not designed to expect it 尝试将内容嵌入未设计为预期内容的组件时出现错误输出

The RenderFragment class

现在更改Collapsible组件,使其具有名为ChildContent的属性,这是RenderFragment的一种类型,并确保使用[Parameter]属性修饰它。

  1. @code {
  2. [Parameter]
  3. public RenderFragment ChildContent { get; set; }
  4. }

这些是Blazor用来将嵌入内容注入组件的标准。嵌入的内容可以是您想要的任何内容;纯文本、HTML元素、更多的剃刀标记(包括更多的组件),嵌入内容的内容只需添加@ChildContent就可以输出到组件标记中的任何位置。

  1. <div class="row">
  2. <a href="#" @onclick=Toggle class="col-12">@ActionText</a>
  3. @if (!Collapsed)
  4. {
  5. <div class="col-12 card card-body">
  6. @ChildContent
  7. </div>
  8. }
  9. </div>
  10. @code
  11. {
  12. [Parameter]
  13. public RenderFragment ChildContent { get; set; }
  14. [Parameter]
  15. public bool Collapsed { get; set; }
  16. string ActionText { get => Collapsed ? "Expand" : "Collapse"; }
  17. void Toggle()
  18. {
  19. Collapsed = !Collapsed;
  20. }
  21. }

浮图秀图片_blazor-university.com_20220111000024.gif

Multiple render fragments

当我们在组件内部编写标记时,Blazor会假设应该将其分配给组件上的一个参数,该参数是从RenderFragment类派生的,名为ChildContent。如果我们希望使用不同的名称或多个呈现片段,则必须在标记中显式指定参数的名称。

  1. <MyComponent>
  2. <Header>
  3. <h1>The header</h1>
  4. </Header>
  5. <Footer>
  6. This is the footer
  7. </Footer>
  8. <ChildContent>
  9. The ChildContent render fragment must now be explicitly named because we have
  10. more than one render fragment parameter in MyComponent.
  11. It doesn't have to be named ChildContent.
  12. </ChildContent>
  13. </MyComponent>

在前面的示例中,我们只需要显式指定<ChildContent>,因为我们已经显式使用了一个或多个其他呈现片段(HeaderFooter)。如果我们不想指定<Header><Footer>,那么就不需要显式命名<ChildContent>,Blazor将假定<MyComponent></MyComponent>之间的所有标记都是ChildContent的呈现片段。
有关详细信息,请参见 Passing data to RenderFragments

Create a TabControl

接下来,我们将创建一个TabControl组件。这将教您如何实现以下目标:
将数据传递到RenderFragment以提供上下文。
使用CascadingParameter将父TabControl组件传递给其子TabPage组件。
TabControl.gif

Making the TabPage aware of its parent 使TabPage能够识别其父级

第一步是创建两个组件。一个名为TabControl,另一个名为TabPageTabPage组件将需要对其父TabControl的引用,这将通过将TabControl自身设置为CascadingValue中的值来实现,并且TabPage将通过CascadingParameter获取此值。

  1. <div>This is a TabControl</div>
  2. <CascadingValue Value="this">
  3. @ChildContent
  4. </CascadingValue>
  5. @code {
  6. // Next line is needed so we are able to add <TabPage> components inside
  7. [Parameter]
  8. public RenderFragment ChildContent { get; set; }
  9. }
  1. <div>This is a TabPage</div>
  2. @ChildContent
  3. @code {
  4. [CascadingParameter]
  5. private TabControl Parent { get; set; }
  6. [Parameter]
  7. public RenderFragment ChildContent { get; set; }
  8. protected override void OnInitialized()
  9. {
  10. if (Parent == null)
  11. throw new ArgumentNullException(nameof(Parent), "TabPage must exist within a TabControl");
  12. base.OnInitialized();
  13. }
  14. }

Making the TabControl aware of its pages 使TabControl了解其页面

更改TabPage组件,使其通过将以下行添加到其OnInitialized方法的末尾来通知其父组件的存在。

  1. Parent.AddPage(this);

更改TabControl组件以添加AddPage方法并存储引用。另外,让我们添加一个ActivePage属性。

  1. public TabPage ActivePage { get; set; }
  2. List<TabPage> Pages = new List<TabPage>();
  3. internal void AddPage(TabPage tabPage)
  4. {
  5. Pages.Add(tabPage);
  6. if (Pages.Count == 1)
  7. ActivePage = tabPage;
  8. StateHasChanged();
  9. }

Rendering a tab for each TabPage

TabPage组件添加一个Text参数,以便其父TabControl知道在激活每个页面的按钮内显示哪些文本。

  1. [Parameter]
  2. public string Text { get; set; }

然后将以下标记添加到TabControl(恰好在呈现ChildContent的位置上方),这将呈现选项卡,并在单击选项卡时更改选择哪个TabPage

  1. <div class="btn-group" role="group">
  2. @foreach (TabPage tabPage in Pages)
  3. {
  4. <button type="button"
  5. class="btn @GetButtonClass(tabPage)"
  6. @onclick=@( () => ActivatePage(tabPage) )>
  7. @tabPage.Text
  8. </button>
  9. }
  10. </div>

标记将创建一个标准的Bootstrap按钮组,然后为每个TabPage创建一个具有以下显著功能的按钮:

  1. CSS类设置为”btn”,并追加GetButtonClass方法返回的任何内容。如果选项卡是ActivePage,则为“btn-primary”,否则为“btn-sencondary”。
  2. 单击该按钮时,它将激活为其创建该按钮的页面。

    注意:@onclick需要一个无参数的方法,因此在@()中使用lambda表达式来使用正确的TabPage执行ActivatePage

  3. 按钮的文本设置为TabPageText属性的值。

并将以下内容添加到TabControl的代码部分。

  1. string GetButtonClass(TabPage page)
  2. {
  3. return page == ActivePage ? "btn-primary" : "btn-secondary";
  4. }
  5. void ActivatePage(TabPage page)
  6. {
  7. ActivePage = page;
  8. }

Using the TabControl

将以下标记添加到页面并运行应用程序。

  1. <TabControl>
  2. <TabPage Text="Tab 1">
  3. <h1>The first tab</h1>
  4. </TabPage>
  5. <TabPage Text="Tab 2">
  6. <h1>The second tab</h1>
  7. </TabPage>
  8. <TabPage Text="Tab 3">
  9. <h1>The third tab</h1>
  10. </TabPage>
  11. </TabControl>

Showing only the active page

目前,TabControl将显示所有TabPages。要解决此问题,只需更改TabPage中的标记,以便仅在ChildContent是其父TabControlActivePage时才呈现其ChildContent

  1. @if (Parent.ActivePage == this)
  2. {
  3. @ChildContent
  4. }

Passing data to RenderFragment

到目前为止,我们已经使用了RenderFragments,它仅仅包含子标记,然后在呈现组件时按原样包含这些标记。除了标准的RenderFragment类之外,还有一个通用的RenderFragment<T>类,可用于将数据传递到RenderFragment

Allowing the user to template tabs

更改TabControl组件,并在ChildContent参数下添加新的TabTextTemplate参数属性。

  1. [Parameter]
  2. public RenderFragment ChildContent { get; set; }
  3. [Parameter]
  4. public RenderFragment<TabPage> TabTextTemplate { get; set; }

然后更改foreach循环中的标记。我们需要做的是检查TabTextTemplate是否已经设置;如果没有,我们照常呈现,如果已经设置,那么我们执行TabTextTemplate ``RenderFragment,从foreach循环传入TabPage

  1. <CascadingValue Value="this">
  2. <div class="btn-group" role="group">
  3. @foreach (TabPage tabPage in Pages)
  4. {
  5. <button type="button"
  6. class="btn @GetButtonClass(tabPage)"
  7. @onclick=@( () => ActivatePage(tabPage) )>
  8. @if (TabTextTemplate != null)
  9. {
  10. @TabTextTemplate(tabPage)
  11. }
  12. else
  13. {
  14. @tabPage.Text
  15. }
  16. </button>
  17. }
  18. </div>
  19. @ChildContent
  20. </CascadingValue>

要设置TabTextTemplate,我们需要在使用TabControl的页面中编辑标记。这只需在<TabControl>元素中添加<TabTextTemplate>元素即可完成,只要将TabPage的标记呈现到TabControl的选项卡中,该模板中的所有内容都将被视为要使用的RenderFragment

  1. <TabControl>
  2. <TabTextTemplate>
  3. Hello
  4. </TabTextTemplate>
  5. <TabPage Text="Tab 1">
  6. <h1>The first tab</h1>
  7. </TabPage>
  8. <TabPage Text="Tab 2">
  9. <h1>The second tab</h1>
  10. </TabPage>
  11. <TabPage Text="Tab 3">
  12. <h1>The third tab</h1>
  13. </TabPage>
  14. </TabControl>

但是一旦这样做,编译器就会抱怨以下错误消息。

Unrecognized child content inside component “TabControl”. The component “TabControl” accepts child content through the following top-level items: “ChildContent”, “TabTextTemplate”.

如果您的组件中只有一个RenderFragment参数,并且它被命名为ChildContent,那么Blazor将假定每当我们使用该组件并在开始标记和结束标记之间包含我们想要将其分配给ChildContent的内容时。但是,一旦在消费者的标记中有了两个RenderFragments,Blazor就不能假设所有内容都应该分配给ChildContent参数。此时,组件的用户必须显式创建一个<ChildContent>元素来保存内容。
为了明确意图,让我们将ChildContent属性重命名为Tabs

  1. <TabControl>
  2. <TabTextTemplate>
  3. Hello
  4. </TabTextTemplate>
  5. <ChildContent>
  6. <TabPage Text="Tab 1">
  7. <h1>The first tab</h1>
  8. </TabPage>
  9. <TabPage Text="Tab 2">
  10. <h1>The second tab</h1>
  11. </TabPage>
  12. <TabPage Text="Tab 3">
  13. <h1>The third tab</h1>
  14. </TabPage>
  15. </ChildContent>
  16. </TabControl>

Accessing context in the RenderFragment

到目前为止,TabControl组件将只显示文本”Hello!”用于每个选项卡页的选项卡。我们需要的是访问正在呈现的TabPage,这样我们就可以输出其Text属性的值。请注意TabControl组件中TabTextTemplate的使用。

  1. @if (TabTextTemplate != null)
  2. {
  3. @TabTextTemplate(tabPage)
  4. }
  5. else
  6. {
  7. @tabPage.Text
  8. }

foreach循环中创建了一个HTML<button>,在该按钮中,前面的代码用于输出应该显示给用户单击的内容。如果TabTextTemplateNULL,则呈现@TabPage.Text,但如果TabTextTemplate不为NULL(组件用户已指定模板),则呈现模板,并传入上下文循环的当前tabPage
如果使用RenderFragment<T>类的泛型版本,则在呈现该片段时必须传递<T>值。传递给片段的值可以通过一个名为context的特殊变量获得。然后可以使用该选项来准确确定要呈现的内容。在我们的示例中,我们希望呈现带有一些附加标记的TabPage.Text属性。

  1. <TabTextTemplate>
  2. <img src="/images/tab.png"/> @context.Text
  3. </TabTextTemplate>

TemplatedTabControl.gif

Avoiding @context name conflicts

如果名称上下文与组件中的另一个标识符冲突,则可以通过使用RenderFragment上的Context属性指示Blazor根据具体情况使用不同的名称。
例如,前面演示的TabTextTemplate标记可以改为如下编写。

  1. <TabTextTemplate Context="TheTab">
  2. <img src="/images/tab.png"/> @TheTab.Text
  3. </TabTextTemplate>

Using @typeparam to create generic components

Blazor在构建过程中将其.razor文件转换为.cs文件。因为我们的razor文件不需要声明类名(它是从文件名推断出来的),所以我们需要一种方法来额外指定何时生成的类应该是泛型类。@typeparam指令允许我们在类上定义一个或多个逗号分隔的泛型参数。这在与通用RenderFragment<T>类结合使用时特别有用。

Outlining our generic DataList component 概述我们的泛型DataList组件

首先,我们需要在/Shared文件夹中创建一个DataList.razor文件,并将其标识为具有单个称为TItem的泛型参数的泛型类。我们还将添加一个[Parameter]属性,预期为IEnumerable<TItem>

  1. @typeparam TItem
  2. @code
  3. {
  4. [Parameter]
  5. public IEnumerable<TItem> Data { get; set; }
  6. }

Using the generic component

创建具有三个属性的Person类。

  1. public class Person
  2. {
  3. public string Salutation { get; set; }
  4. public string GivenName { get; set; }
  5. public string FamilyName { get; set; }
  6. }

Index页面中,创建一些我们希望显示给用户的Person实例,并将这些实例传递给我们的新DataList组件。

  1. <DataList Data=@People/>
  2. @code
  3. {
  4. private IEnumerable<Person> People;
  5. protected override void OnInitialized()
  6. {
  7. base.OnInitialized();
  8. People = new Person[]
  9. {
  10. new Person { Salutation = "Mr", GivenName = "Bob", FamilyName = "Geldof" },
  11. new Person { Salutation = "Mrs", GivenName = "Angela", FamilyName = "Rippon" },
  12. new Person { Salutation = "Mr", GivenName = "Freddie", FamilyName = "Mercury" }
  13. };
  14. }
  15. }

Rendering the data in our component using RenderFragment

最后,我们将添加RenderFragment<TItem>属性并将其标记为[Parameter],以便使用Rzor文件可以指定一个模板来呈现Data属性中的每个TItem
最终的DataList.razor组件标记将如下所示。

  1. @typeparam TItem
  2. <ul>
  3. @foreach(TItem item in Data ?? Array.Empty<TItem>())
  4. {
  5. <li>@ChildContent(item)</li>
  6. }
  7. </ul>
  8. @code
  9. {
  10. [Parameter]
  11. public IEnumerable<TItem> Data { get; set; }
  12. [Parameter]
  13. public RenderFragment<TItem> ChildContent { get; set; }
  14. }
  • Line 1
    指定此组件为泛型组件,并且只有一个名为TItem的泛型参数。
  • Lines 10-11
    声明名为Data[Parameter]属性,该属性是Item类型的可枚举属性。
  • Lines 13-14
    声明一个名为ChildContent[Parameter]属性,该属性是一个RenderFragment<TItem>-这样我们就可以将TItem的实例传递给它,并让它为我们提供一些呈现的HTML来输出。
  • Line 3
    迭代Data属性,并针对每个元素通过将当前元素传递给RenderFragment<TItem>名为ChildContent的内容来呈现该内容。

    Final source code

    Index.razor

    注意:添加第5行是为了指定我们希望为每个元素呈现的ChildContext。元素本身通过@context变量传递,因此RenderFragment<TItem>实际上是一个RenderFragment<Person>-因此@contextPerson,因此我们获得了类型安全编译和智能感知的好处。

  1. @page "/"
  2. <h1>A generic list of Person</h1>
  3. <DataList Data=@People>
  4. @context.Salutation @context.FamilyName, @context.GivenName
  5. </DataList>
  6. @code
  7. {
  8. private IEnumerable<Person> People;
  9. protected override void OnInitialized()
  10. {
  11. base.OnInitialized();
  12. People = new Person[]
  13. {
  14. new Person { Salutation = "Mr", GivenName = "Bob", FamilyName = "Geldof" },
  15. new Person { Salutation = "Mrs", GivenName = "Angela", FamilyName = "Rippon" },
  16. new Person { Salutation = "Mr", GivenName = "Freddie", FamilyName = "Mercury" }
  17. };
  18. }
  19. }

DataList.razor

  1. @typeparam TItem
  2. <ul>
  3. @foreach(TItem item in Data ?? Array.Empty<TItem>())
  4. {
  5. <li>@ChildContent(item)</li>
  6. }
  7. </ul>
  8. @code
  9. {
  10. [Parameter]
  11. public IEnumerable<TItem> Data { get; set; }
  12. [Parameter]
  13. public RenderFragment<TItem> ChildContent { get; set; }
  14. }

Generated output

  1. <h1>A generic list of Person</h1>
  2. <ul>
  3. <li>Mr Geldof, Bob</li>
  4. <li>Mrs Rippon, Angela</li>
  5. <li>Mr Mercury, Freddie</li>
  6. </ul>

Explicitly specifying generic parameter types

因为razor文件转换为C#类,所以我们不需要为DataList期望的泛型参数指定类型,因为它是由我们设置data=(Some instance of IEnumerable<TItem>)的编译器推断出来的。如果我们确实需要显式指定泛型参数类型,可以编写以下代码。

  1. <SomeGenericComponent TParam1=Person TParam2=Supplier TItem=etc/>

Passing placeholders to RenderFragments

来源:此页面的灵感来自用户ℳisterℳagoo在Twitter上发布的一篇帖子。

起初,考虑声明类型RenderFragment<RenderFragment>[Parameter]属性可能看起来不直观,或者可能有点奇怪。

  1. [Parameter]
  2. public RenderFragment<RenderFragment> ChildContent { get; set; }

事实是,如果您曾经创建过Custom Blazor布局,那么您已经熟悉了类似的概念。
RenderFragment<T>中的<T>作为@context变量传递到用户指定的标记中。布局使用名称@Body而不是@context,但@Body实际上是一个RenderFragment。要证明这一点,请编辑/Shared/MainLayout.razor文件并将@Body更改为以下内容。

  1. @Body.GetType().Name

现在,我们看到的不是@Body呈现的内容,而是恰好是RenderFragment的类名。
虽然这并不完全是Blazor布局的工作方式,但对于帮助理解声明类型RenderFragment<RenderFragment>[Parameter]属性背后的原则,这是一个有用的比较。

  1. <div class="our-main-layout">
  2. @Body
  3. </div>

在前面的虚构布局中,我们可以将整个标记想象为某个父组件的ChildContent RenderFragment,而第3行上的@Body等同于@context(命名为Body),它是我们可以选择注入的RenderFragment。在本例中,我们选择将Body注入HTML<div>元素中。
相当于我们自己的就是

  1. <OurComponent>
  2. <p>
  3. @context
  4. </p>
  5. </OurComponent>

或者,如果我们希望使用名称Body(或任何其他名称)而不是context,我们可以指定要用于上下文的名称。请参见将数据传递给RenderFragment底部的避免@context名称冲突部分。

  1. <OurComponent Context="FragmentWeNeedToRender">
  2. <div class="our-wrapped-fragment">
  3. @FragmentWeNeedToRender
  4. </div>
  5. </OurComponent>

Creating a working example

首先,创建一个我们可以用来绑定一些数据的类。

  1. public class Person
  2. {
  3. public string Salutation { get; set; }
  4. public string GivenName { get; set; }
  5. public string FamilyName { get; set; }
  6. }

Creating a simple templated repeating component

此详细信息类似于使用@typeparam创建泛型组件一节中介绍的内容。在这里,我们将对共享内容进行快速润色,因此,如果您还没有读过,请先阅读。
接下来,我们需要在/Shared中创建一个名为DataList.razor的新组件。该组件将是一个泛型组件(使用@typeparam),它将接受IEnumerable<TItem>并迭代可枚举数,以使用其使用者指定的模板呈现每个项目的内容。

  1. @typeparam TItem
  2. <ul>
  3. @foreach (TItem item in Data ?? Array.Empty<TItem>())
  4. {
  5. @ItemTemplate(item)
  6. }
  7. </ul>
  8. @code
  9. {
  10. [Parameter]
  11. public IEnumerable<TItem> Data { get; set; }
  12. [Parameter]
  13. public RenderFragment<TItem> ItemTemplate { get; set; }
  14. }

此组件可能由我们的索引页使用,如下所示:

  1. @page "/"
  2. <DataList Data=@Data>
  3. <ItemTemplate>
  4. <li @key=context>@context.Salutation @context.GivenName @context.FamilyName</li>
  5. </ItemTemplate>
  6. </DataList>
  7. @code
  8. {
  9. private IEnumerable<Person> People;
  10. protected override void OnInitialized()
  11. {
  12. base.OnInitialized();
  13. People = new Person[]
  14. {
  15. new Person { Salutation = "Mr", GivenName = "Bob", FamilyName = "Geldof" },
  16. new Person { Salutation = "Mrs", GivenName = "Angela", FamilyName = "Rippon" },
  17. new Person { Salutation = "Mr", GivenName = "Freddie", FamilyName = "Mercury" }
  18. };
  19. }
  20. }

注意:<li>元素中的@key=context是出于性能目的,应该在呈现列表时使用。有关更多信息,请参见使用@Key进行优化

The problem

如果我们的DataList组件不只输出项目列表,该怎么办呢?也许它有一个页脚,允许用户使用“上一页”/“下一页”按钮一次显示一页元素,还有一个页脚显示总共有多少个项目?
我们可以简单地将这个额外的标记添加到我们的DataList组件中,但是如果我们的组件的使用者也希望以不同的方式呈现列表,该怎么办呢?也许他们需要在一个地方以HTML

的形式显示分页列表,而在其他地方以HTML
    的形式显示?

    1. <div class="paged-data-list">
    2. <div class="paged-data-list_header>
    3. @Data.Count() item(s)
    4. </div>
    5. <div class="paged-data-list_body">
    6. <!-- Consumer wants either a <table> or a <ul> here -->
    7. @foreach(TItem item in CurrentPageOfData)
    8. {
    9. @ItemTemplate(item)
    10. }
    11. <!-- Consumer wants either a </table> or a </ul> here -->
    12. </div>
    13. <div class="paged-data-list_footer">
    14. <button type="button" etc>Prev</button>
    15. <button type="button" etc>Next</button>
    16. </div>
    17. </div>

    这正是需要我们让用户传入RenderFragment<RenderFragment>的用例。

    Rendering a RenderFragment within a RenderFragment

    现在我们有了ItemTemplate,它接收类型为Person@context以呈现每个元素,我们需要允许组件的使用者指定在第一个元素之前和最后一个元素之后包含什么内容-如前面第6行和第11行的代码示例中突出显示的那样。
    消费组件看起来与以下两个示例中的任何一个类似

    1. <DataList Data=@People>
    2. <ItemTemplate Context="person">
    3. <li @key=person>
    4. @person.Salutation @person.FamilyName, @person.GivenName
    5. </li>
    6. </ItemTeplate>
    7. </DataList>
    8. <DataList Data=@People>
    9. <ListTemplate Context="allPeople>
    10. <ol Type="A">
    11. @allPeople
    12. </ul>
    13. </ListTemplate>
    14. <ItemTemplate Context="person">
    15. <li @key=person>
    16. @person.Salutation @person.FamilyName, @person.GivenName
    17. </li>
    18. </ItemTemplate>
    19. </DataList>
    • Line 2

    定义要用于列表的模板。在本例中,我们用<ul></ul>包装内部内容(数据项“allPeople”)。

    • Line 4

    通过执行通过上下文变量传递的RenderFragment(我们选择将其称为“allPeople”)来指示呈现项标记的位置。

    • Line 7

    为每个项目指定一个模板。在本例中,上下文将是Person的实例,因此我们选择通过指定context="person"来使用名称”person”来引用上下文。

    Passing rendered content as a placeholder for the consumer to display

    DataList组件需要呈现Data属性中的每一项,然后将其传递给使用者以决定在其ListTemplate中的什么位置呈现该输出。或者,更准确地说,DataList需要传递一个RenderFragment,该RenderFragment将在执行时呈现项的标记。
    首先,我们将向DataList组件添加ListTemplate属性。

    1. [Parameter]
    2. public RenderFragment<RenderFragment> ListTemplate { get; set; }

    接下来,我们将更改我们的DataList,使其在使用者没有指定ListTemplate时使用<ul><li>作为默认值,之后我们将处理使用者确实想要使用自定义ListTemplate的场景。

    Rendering using a default list template

    这是最简单的部分。我们所需要做的就是编写标记,就好像组件上没有ListTemplate这样的东西,就像我们通常会做的那样-但只有在ListTemplate属性为空的情况下才会这样。

    1. @typeparam TItem
    2. @if (ListTemplate == null)
    3. {
    4. <ul>
    5. @foreach (TItem item in Data ?? Array.Empty<TItem>())
    6. {
    7. @ItemTemplate(item)
    8. }
    9. </ul>
    10. }
    11. @code
    12. {
    13. [Parameter]
    14. public IEnumerable<TItem> Data { get; set; }
    15. [Parameter]
    16. public RenderFragment<TItem> ItemTemplate { get; set; }
    17. [Parameter]
    18. public RenderFragment<RenderFragment> ListTemplate { get; set; }
    19. }
    • Line 2

    检查ListTemplate是否为空

    • Lines 4-9

    如果ListTemplate为空,那么我们呈现一个标准的<ul>列表,然后使用ItemTemplate呈现数据中的元素。

    👍Rendering with a custom ListTemplate

    RenderFragment<TItem>期望我们在每次呈现时传递一个TItem实例。这很容易做到,因为我们有一个IEnumerable<TItem>,我们可以从其中提取要呈现的值,但是当我们需要将RenderFragment的实例传递给我们的模板时,我们该怎么做呢?
    要定义非泛型RenderFragment,我们可以使用标准的Razor转义序列来表示HTML,即@:

    1. RenderFragment rf = @<h1>Hello</h1>;

    要定义RenderFragment<T>,我们需要使用传入T实例的lambda

    1. RenderFragment<Person> rf =
    2. person => @<h1>Hello @person.Name</h1>;

    那么,我们如何返回一个RenderFragment,该RenderFragment仅在呈现数据属性时循环遍历数据属性中的项呢?为此,我们需要使用wig-pog语法。

    The wig-pig

    image.pngWIG-PIG是一组字符,Razor渲染引擎可以使用它们来表示C#文件中的一大块Razor标记。由于显而易见的原因,此字符序列仅在.razor文件中有效。任何人的视觉上的相似之处,无论是活着的还是死去的,都纯属巧合。

    注意:@:@{实际上是两个字符序列。第一个@:告诉Razor解析器将下面的文本视为Razor标记,然后@{是C#代码挡路的开始-它显然会在某个地方以互补的}结束。最终,这给我们提供了一大块Razor标记,相当于其中包含C#代码的RenderFragment,可以执行循环等操作。

    调用@ListItem(...)时在我们的Razor文件中,我们可以使用wig-pIG语法将就地RenderFragment作为参数传递。

    1. @ListTemplate(
    2. @:@{
    3. foreach (TItem item in Data ?? Array.Empty<TItem>())
    4. {
    5. @item.ToString()
    6. }
    7. }
    8. )

    使用此语法,我们在<ListTemplate>中呈现由消费者指定的标记,并传入将呈现数据中所有元素的RenderFragment。在前面的代码中,它只是对数据中的每一项调用ToString()。但是,理想情况下,我们希望使用消费者为我们提供的ItemTemplate

    1. @ListTemplate(
    2. @:@{
    3. foreach (TItem item in Data ?? Array.Empty<TItem>())
    4. {
    5. @ItemTemplate(item)
    6. }
    7. }
    8. )

    DataList.razor

    1. @typeparam TItem
    2. @if (ListTemplate == null)
    3. {
    4. <ul>
    5. @foreach (TItem item in Data ?? Array.Empty<TItem>())
    6. {
    7. @ItemTemplate(item)
    8. }
    9. </ul>
    10. }
    11. else
    12. {
    13. @ListTemplate(
    14. @:@{
    15. foreach (TItem item in Data ?? Array.Empty<TItem>())
    16. {
    17. @ItemTemplate(item)
    18. }
    19. }
    20. )
    21. }
    22. @code
    23. {
    24. [Parameter]
    25. public IEnumerable<TItem> Data { get; set; }
    26. [Parameter]
    27. public RenderFragment<TItem> ItemTemplate { get; set; }
    28. [Parameter]
    29. public RenderFragment<RenderFragment> ListTemplate { get; set; }
    30. }

    Consuming our DataList

    Sample 1: A simple list

    1. <DataList Data=@People>
    2. <ItemTemplate>
    3. <li @key=context>@context.Salutation @context.GivenName @context.FamilyName</li>
    4. </ItemTemplate>
    5. </DataList>

    生成以下内容

    1. <ul>
    2. <li>Mr Bob Geldof</li>
    3. <li>Mrs Angela Rippon</li>
    4. <li>Mr Freddie Mercury</li>
    5. </ul>

    image.png

    Sample 2: An HTML table

    1. <DataList Data=@People>
    2. <ListTemplate Context="listOfPeople">
    3. <table border=1 cellpadding=4>
    4. <thead>
    5. <tr>
    6. <th>Salutation</th>
    7. <th>Given name</th>
    8. <th>Family name</th>
    9. </tr>
    10. </thead>
    11. <tbody>
    12. @listOfPeople
    13. </tbody>
    14. </table>
    15. </ListTemplate>
    16. <ItemTemplate Context="person">
    17. <tr @key=@person>
    18. <td>@person.Salutation</td>
    19. <td>@person.GivenName</td>
    20. <td>@person.FamilyName</td>
    21. </tr>
    22. </ItemTemplate>
    23. </DataList>

    生成以下内容

    1. <table cellpadding="4" border="1">
    2. <thead>
    3. <tr>
    4. <th>Salutation</th>
    5. <th>Given name</th>
    6. <th>Family name</th>
    7. </tr>
    8. </thead>
    9. <tbody>
    10. <tr>
    11. <td>Mr</td>
    12. <td>Bob</td>
    13. <td>Geldof</td>
    14. </tr>
    15. <tr>
    16. <td>Mrs</td>
    17. <td>Angela</td>
    18. <td>Rippon</td>
    19. </tr>
    20. <tr>
    21. <td>Mr</td>
    22. <td>Freddie</td>
    23. <td>Mercury</td>
    24. </tr>
    25. </tbody>
    26. </table>

    image.png

    Assigning defaults when RenderFragments are not specified

    目前,我们必须在视图中使用@if语句检查ListTemplate是否为空,甚至不检查是否设置了ItemTemplate。这两种方法都不是理想的。
    相反,如果使用者没有设置RenderFragment属性,我们应该将它们设置为所需的默认值。这样,我们的组件的呈现逻辑可以保持简单得多。

    1. @ListTemplate(
    2. @:@{
    3. foreach(TItem item in CurrentPage)
    4. {
    5. @ItemTemplate(item)
    6. }
    7. }
    8. )

    覆盖组件中的OnParametersSet(),并确保ItemTemplate属性不为空。为此,我们创建了一个lambda,它接收TItem并返回RenderFragment(使用wig-pig语法)。

    1. protected override void OnParametersSet()
    2. {
    3. if (ItemTemplate == null)
    4. {
    5. ItemTemplate = (item) => @:@{
    6. <li @key=item>@item.ToString()</li>}
    7. ;
    8. }
    9. }

    为了确保ListTemplate不为空,我们创建了一个lamba,它接收RenderFragment,然后忽略它并返回自定义的wig-pigRenderFragment

    1. if (ListTemplate == null)
    2. {
    3. ListTemplate = _ => @:@{
    4. <ul>
    5. @foreach(TItem item in CurrentPage)
    6. {
    7. @ItemTemplate(item)
    8. }
    9. </ul>
    10. }
    11. ;
    12. }

    备注:;不能与前面的}位于同一行,否则Razor解析器无法正确解析源代码。这已被报告为错误,并有望在不久的将来得到修复。

    Summary

    当消费者希望在其标记中标识将在呈现期间传递给他们的内容的占位符(例如Blazor布局中的正文占位符)时,应使用RenderFragment<RenderFragment>技术。
    希望本节已经帮助您理解了何时使用此技术,以及如何使用wig-pigg@:@{语法生成就地RenderFragments

    Additional reading

    本节的源代码包括一个使用PagedDataList组件的附加示例页