Templating components with RenderFragments
到目前为止,我们已经创建了基于参数生成100%呈现输出的组件,但是组件并不总是那么简单。有时,我们需要创建将消费者提供的标记与其自己的呈现输出混合在一起的组件。
将内容作为HTML编码的字符串参数传递给组件将非常混乱(更不用说不可管理了):
<Collapsible content="Lots of encoded HTML for your entire view here"/>
而且,除了维护的噩梦之外,嵌入式HTML也只能是基本的HTML标记,不能是Blazor组件。基本上,这是没有用的,显然这不应该是这样做的。正确的方法是使用RenderFragment
。
Child content
如果我们创建一个名为Collapsible的新组件(一个完全空的.razor文件),我们就可以在Index.razor页面中使用它,如下所示:
<Collapsible/>
但是如果我们想要嵌入一些内容呢?试一试,然后查看浏览器控制台输出中的错误。
<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]
属性修饰它。
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
}
这些是Blazor用来将嵌入内容注入组件的标准。嵌入的内容可以是您想要的任何内容;纯文本、HTML元素、更多的剃刀标记(包括更多的组件),嵌入内容的内容只需添加@ChildContent
就可以输出到组件标记中的任何位置。
<div class="row">
<a href="#" @onclick=Toggle class="col-12">@ActionText</a>
@if (!Collapsed)
{
<div class="col-12 card card-body">
@ChildContent
</div>
}
</div>
@code
{
[Parameter]
public RenderFragment ChildContent { get; set; }
[Parameter]
public bool Collapsed { get; set; }
string ActionText { get => Collapsed ? "Expand" : "Collapse"; }
void Toggle()
{
Collapsed = !Collapsed;
}
}
Multiple render fragments
当我们在组件内部编写标记时,Blazor会假设应该将其分配给组件上的一个参数,该参数是从RenderFragment
类派生的,名为ChildContent
。如果我们希望使用不同的名称或多个呈现片段,则必须在标记中显式指定参数的名称。
<MyComponent>
<Header>
<h1>The header</h1>
</Header>
<Footer>
This is the footer
</Footer>
<ChildContent>
The ChildContent render fragment must now be explicitly named because we have
more than one render fragment parameter in MyComponent.
It doesn't have to be named ChildContent.
</ChildContent>
</MyComponent>
在前面的示例中,我们只需要显式指定<ChildContent>
,因为我们已经显式使用了一个或多个其他呈现片段(Header
和Footer
)。如果我们不想指定<Header>
和<Footer>
,那么就不需要显式命名<ChildContent>
,Blazor将假定<MyComponent>
和</MyComponent>
之间的所有标记都是ChildContent
的呈现片段。
有关详细信息,请参见 Passing data to RenderFragments。
Create a TabControl
接下来,我们将创建一个TabControl组件。这将教您如何实现以下目标:
将数据传递到RenderFragment
以提供上下文。
使用CascadingParameter
将父TabControl组件传递给其子TabPage组件。
Making the TabPage aware of its parent 使TabPage能够识别其父级
第一步是创建两个组件。一个名为TabControl,另一个名为TabPage。TabPage组件将需要对其父TabControl的引用,这将通过将TabControl自身设置为CascadingValue
中的值来实现,并且TabPage将通过CascadingParameter
获取此值。
<div>This is a TabControl</div>
<CascadingValue Value="this">
@ChildContent
</CascadingValue>
@code {
// Next line is needed so we are able to add <TabPage> components inside
[Parameter]
public RenderFragment ChildContent { get; set; }
}
<div>This is a TabPage</div>
@ChildContent
@code {
[CascadingParameter]
private TabControl Parent { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
protected override void OnInitialized()
{
if (Parent == null)
throw new ArgumentNullException(nameof(Parent), "TabPage must exist within a TabControl");
base.OnInitialized();
}
}
Making the TabControl aware of its pages 使TabControl了解其页面
更改TabPage组件,使其通过将以下行添加到其OnInitialized
方法的末尾来通知其父组件的存在。
Parent.AddPage(this);
更改TabControl组件以添加AddPage
方法并存储引用。另外,让我们添加一个ActivePage
属性。
public TabPage ActivePage { get; set; }
List<TabPage> Pages = new List<TabPage>();
internal void AddPage(TabPage tabPage)
{
Pages.Add(tabPage);
if (Pages.Count == 1)
ActivePage = tabPage;
StateHasChanged();
}
Rendering a tab for each TabPage
向TabPage组件添加一个Text
参数,以便其父TabControl知道在激活每个页面的按钮内显示哪些文本。
[Parameter]
public string Text { get; set; }
然后将以下标记添加到TabControl(恰好在呈现ChildContent
的位置上方),这将呈现选项卡,并在单击选项卡时更改选择哪个TabPage。
<div class="btn-group" role="group">
@foreach (TabPage tabPage in Pages)
{
<button type="button"
class="btn @GetButtonClass(tabPage)"
@onclick=@( () => ActivatePage(tabPage) )>
@tabPage.Text
</button>
}
</div>
标记将创建一个标准的Bootstrap按钮组,然后为每个TabPage创建一个具有以下显著功能的按钮:
- CSS类设置为”btn”,并追加
GetButtonClass
方法返回的任何内容。如果选项卡是ActivePage
,则为“btn-primary”,否则为“btn-sencondary”。 单击该按钮时,它将激活为其创建该按钮的页面。
注意:
@onclick
需要一个无参数的方法,因此在@()
中使用lambda表达式来使用正确的TabPage执行ActivatePage
。按钮的文本设置为TabPage的
Text
属性的值。
并将以下内容添加到TabControl的代码部分。
string GetButtonClass(TabPage page)
{
return page == ActivePage ? "btn-primary" : "btn-secondary";
}
void ActivatePage(TabPage page)
{
ActivePage = page;
}
Using the TabControl
将以下标记添加到页面并运行应用程序。
<TabControl>
<TabPage Text="Tab 1">
<h1>The first tab</h1>
</TabPage>
<TabPage Text="Tab 2">
<h1>The second tab</h1>
</TabPage>
<TabPage Text="Tab 3">
<h1>The third tab</h1>
</TabPage>
</TabControl>
Showing only the active page
目前,TabControl将显示所有TabPages。要解决此问题,只需更改TabPage中的标记,以便仅在ChildContent
是其父TabControl的ActivePage
时才呈现其ChildContent
。
@if (Parent.ActivePage == this)
{
@ChildContent
}
Passing data to RenderFragment
到目前为止,我们已经使用了RenderFragments
,它仅仅包含子标记,然后在呈现组件时按原样包含这些标记。除了标准的RenderFragment
类之外,还有一个通用的RenderFragment<T>
类,可用于将数据传递到RenderFragment
。
Allowing the user to template tabs
更改TabControl组件,并在ChildContent
参数下添加新的TabTextTemplate
参数属性。
[Parameter]
public RenderFragment ChildContent { get; set; }
[Parameter]
public RenderFragment<TabPage> TabTextTemplate { get; set; }
然后更改foreach
循环中的标记。我们需要做的是检查TabTextTemplate
是否已经设置;如果没有,我们照常呈现,如果已经设置,那么我们执行TabTextTemplate ``RenderFragment
,从foreach
循环传入TabPage。
<CascadingValue Value="this">
<div class="btn-group" role="group">
@foreach (TabPage tabPage in Pages)
{
<button type="button"
class="btn @GetButtonClass(tabPage)"
@onclick=@( () => ActivatePage(tabPage) )>
@if (TabTextTemplate != null)
{
@TabTextTemplate(tabPage)
}
else
{
@tabPage.Text
}
</button>
}
</div>
@ChildContent
</CascadingValue>
要设置TabTextTemplate
,我们需要在使用TabControl的页面中编辑标记。这只需在<TabControl>
元素中添加<TabTextTemplate>
元素即可完成,只要将TabPage的标记呈现到TabControl的选项卡中,该模板中的所有内容都将被视为要使用的RenderFragment
。
<TabControl>
<TabTextTemplate>
Hello
</TabTextTemplate>
<TabPage Text="Tab 1">
<h1>The first tab</h1>
</TabPage>
<TabPage Text="Tab 2">
<h1>The second tab</h1>
</TabPage>
<TabPage Text="Tab 3">
<h1>The third tab</h1>
</TabPage>
</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
。
<TabControl>
<TabTextTemplate>
Hello
</TabTextTemplate>
<ChildContent>
<TabPage Text="Tab 1">
<h1>The first tab</h1>
</TabPage>
<TabPage Text="Tab 2">
<h1>The second tab</h1>
</TabPage>
<TabPage Text="Tab 3">
<h1>The third tab</h1>
</TabPage>
</ChildContent>
</TabControl>
Accessing context in the RenderFragment
到目前为止,TabControl组件将只显示文本”Hello!”用于每个选项卡页的选项卡。我们需要的是访问正在呈现的TabPage,这样我们就可以输出其Text属性的值。请注意TabControl组件中TabTextTemplate
的使用。
@if (TabTextTemplate != null)
{
@TabTextTemplate(tabPage)
}
else
{
@tabPage.Text
}
在foreach
循环中创建了一个HTML<button>
,在该按钮中,前面的代码用于输出应该显示给用户单击的内容。如果TabTextTemplate
为NULL
,则呈现@TabPage.Text
,但如果TabTextTemplate
不为NULL(组件用户已指定模板),则呈现模板,并传入上下文循环的当前tabPage。
如果使用RenderFragment<T>
类的泛型版本,则在呈现该片段时必须传递<T>
值。传递给片段的值可以通过一个名为context
的特殊变量获得。然后可以使用该选项来准确确定要呈现的内容。在我们的示例中,我们希望呈现带有一些附加标记的TabPage.Text
属性。
<TabTextTemplate>
<img src="/images/tab.png"/> @context.Text
</TabTextTemplate>
Avoiding @context name conflicts
如果名称上下文与组件中的另一个标识符冲突,则可以通过使用RenderFragment
上的Context
属性指示Blazor根据具体情况使用不同的名称。
例如,前面演示的TabTextTemplate
标记可以改为如下编写。
<TabTextTemplate Context="TheTab">
<img src="/images/tab.png"/> @TheTab.Text
</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>
。
@typeparam TItem
@code
{
[Parameter]
public IEnumerable<TItem> Data { get; set; }
}
Using the generic component
创建具有三个属性的Person
类。
public class Person
{
public string Salutation { get; set; }
public string GivenName { get; set; }
public string FamilyName { get; set; }
}
在Index页面中,创建一些我们希望显示给用户的Person
实例,并将这些实例传递给我们的新DataList
组件。
<DataList Data=@People/>
@code
{
private IEnumerable<Person> People;
protected override void OnInitialized()
{
base.OnInitialized();
People = new Person[]
{
new Person { Salutation = "Mr", GivenName = "Bob", FamilyName = "Geldof" },
new Person { Salutation = "Mrs", GivenName = "Angela", FamilyName = "Rippon" },
new Person { Salutation = "Mr", GivenName = "Freddie", FamilyName = "Mercury" }
};
}
}
Rendering the data in our component using RenderFragment
最后,我们将添加RenderFragment<TItem>
属性并将其标记为[Parameter]
,以便使用Rzor文件可以指定一个模板来呈现Data
属性中的每个TItem
。
最终的DataList.razor组件标记将如下所示。
@typeparam TItem
<ul>
@foreach(TItem item in Data ?? Array.Empty<TItem>())
{
<li>@ChildContent(item)</li>
}
</ul>
@code
{
[Parameter]
public IEnumerable<TItem> Data { get; set; }
[Parameter]
public RenderFragment<TItem> ChildContent { get; set; }
}
- 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>
-因此@context
是Person
,因此我们获得了类型安全编译和智能感知的好处。
@page "/"
<h1>A generic list of Person</h1>
<DataList Data=@People>
@context.Salutation @context.FamilyName, @context.GivenName
</DataList>
@code
{
private IEnumerable<Person> People;
protected override void OnInitialized()
{
base.OnInitialized();
People = new Person[]
{
new Person { Salutation = "Mr", GivenName = "Bob", FamilyName = "Geldof" },
new Person { Salutation = "Mrs", GivenName = "Angela", FamilyName = "Rippon" },
new Person { Salutation = "Mr", GivenName = "Freddie", FamilyName = "Mercury" }
};
}
}
DataList.razor
@typeparam TItem
<ul>
@foreach(TItem item in Data ?? Array.Empty<TItem>())
{
<li>@ChildContent(item)</li>
}
</ul>
@code
{
[Parameter]
public IEnumerable<TItem> Data { get; set; }
[Parameter]
public RenderFragment<TItem> ChildContent { get; set; }
}
Generated output
<h1>A generic list of Person</h1>
<ul>
<li>Mr Geldof, Bob</li>
<li>Mrs Rippon, Angela</li>
<li>Mr Mercury, Freddie</li>
</ul>
Explicitly specifying generic parameter types
因为razor
文件转换为C#
类,所以我们不需要为DataList
期望的泛型参数指定类型,因为它是由我们设置data=(Some instance of IEnumerable<TItem>)
的编译器推断出来的。如果我们确实需要显式指定泛型参数类型,可以编写以下代码。
<SomeGenericComponent TParam1=Person TParam2=Supplier TItem=etc/>
Passing placeholders to RenderFragments
来源:此页面的灵感来自用户ℳisterℳagoo在Twitter上发布的一篇帖子。
起初,考虑声明类型RenderFragment<RenderFragment>
的[Parameter]
属性可能看起来不直观,或者可能有点奇怪。
[Parameter]
public RenderFragment<RenderFragment> ChildContent { get; set; }
事实是,如果您曾经创建过Custom Blazor布局,那么您已经熟悉了类似的概念。RenderFragment<T>
中的<T>
作为@context
变量传递到用户指定的标记中。布局使用名称@Body而不是@context
,但@Body
实际上是一个RenderFragment
。要证明这一点,请编辑/Shared/MainLayout.razor文件并将@Body
更改为以下内容。
@Body.GetType().Name
现在,我们看到的不是@Body
呈现的内容,而是恰好是RenderFragment
的类名。
虽然这并不完全是Blazor布局的工作方式,但对于帮助理解声明类型RenderFragment<RenderFragment>
的[Parameter]
属性背后的原则,这是一个有用的比较。
<div class="our-main-layout">
@Body
</div>
在前面的虚构布局中,我们可以将整个标记想象为某个父组件的ChildContent RenderFragment
,而第3行上的@Body
等同于@context
(命名为Body),它是我们可以选择注入的RenderFragment
。在本例中,我们选择将Body注入HTML<div>
元素中。
相当于我们自己的就是
<OurComponent>
<p>
@context
</p>
</OurComponent>
或者,如果我们希望使用名称Body(或任何其他名称)而不是context,我们可以指定要用于上下文的名称。请参见将数据传递给RenderFragment底部的避免@context
名称冲突部分。
<OurComponent Context="FragmentWeNeedToRender">
<div class="our-wrapped-fragment">
@FragmentWeNeedToRender
</div>
</OurComponent>
Creating a working example
首先,创建一个我们可以用来绑定一些数据的类。
public class Person
{
public string Salutation { get; set; }
public string GivenName { get; set; }
public string FamilyName { get; set; }
}
Creating a simple templated repeating component
此详细信息类似于使用@typeparam创建泛型组件一节中介绍的内容。在这里,我们将对共享内容进行快速润色,因此,如果您还没有读过,请先阅读。
接下来,我们需要在/Shared中创建一个名为DataList.razor的新组件。该组件将是一个泛型组件(使用@typeparam
),它将接受IEnumerable<TItem>
并迭代可枚举数,以使用其使用者指定的模板呈现每个项目的内容。
@typeparam TItem
<ul>
@foreach (TItem item in Data ?? Array.Empty<TItem>())
{
@ItemTemplate(item)
}
</ul>
@code
{
[Parameter]
public IEnumerable<TItem> Data { get; set; }
[Parameter]
public RenderFragment<TItem> ItemTemplate { get; set; }
}
此组件可能由我们的索引页使用,如下所示:
@page "/"
<DataList Data=@Data>
<ItemTemplate>
<li @key=context>@context.Salutation @context.GivenName @context.FamilyName</li>
</ItemTemplate>
</DataList>
@code
{
private IEnumerable<Person> People;
protected override void OnInitialized()
{
base.OnInitialized();
People = new Person[]
{
new Person { Salutation = "Mr", GivenName = "Bob", FamilyName = "Geldof" },
new Person { Salutation = "Mrs", GivenName = "Angela", FamilyName = "Rippon" },
new Person { Salutation = "Mr", GivenName = "Freddie", FamilyName = "Mercury" }
};
}
}
注意:
<li>
元素中的@key=context
是出于性能目的,应该在呈现列表时使用。有关更多信息,请参见使用@Key进行优化。
The problem
如果我们的DataList组件不只输出项目列表,该怎么办呢?也许它有一个页脚,允许用户使用“上一页”/“下一页”按钮一次显示一页元素,还有一个页脚显示总共有多少个项目?
我们可以简单地将这个额外的标记添加到我们的DataList组件中,但是如果我们的组件的使用者也希望以不同的方式呈现列表,该怎么办呢?也许他们需要在一个地方以HTML
- 的形式显示?
<div class="paged-data-list">
<div class="paged-data-list_header>
@Data.Count() item(s)
</div>
<div class="paged-data-list_body">
<!-- Consumer wants either a <table> or a <ul> here -->
@foreach(TItem item in CurrentPageOfData)
{
@ItemTemplate(item)
}
<!-- Consumer wants either a </table> or a </ul> here -->
</div>
<div class="paged-data-list_footer">
<button type="button" etc>Prev</button>
<button type="button" etc>Next</button>
</div>
</div>
这正是需要我们让用户传入RenderFragment<RenderFragment>
的用例。
Rendering a RenderFragment within a RenderFragment
现在我们有了ItemTemplate
,它接收类型为Person
的@context
以呈现每个元素,我们需要允许组件的使用者指定在第一个元素之前和最后一个元素之后包含什么内容-如前面第6行和第11行的代码示例中突出显示的那样。
消费组件看起来与以下两个示例中的任何一个类似
<DataList Data=@People>
<ItemTemplate Context="person">
<li @key=person>
@person.Salutation @person.FamilyName, @person.GivenName
</li>
</ItemTeplate>
</DataList>
<DataList Data=@People>
<ListTemplate Context="allPeople>
<ol Type="A">
@allPeople
</ul>
</ListTemplate>
<ItemTemplate Context="person">
<li @key=person>
@person.Salutation @person.FamilyName, @person.GivenName
</li>
</ItemTemplate>
</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
属性。
[Parameter]
public RenderFragment<RenderFragment> ListTemplate { get; set; }
接下来,我们将更改我们的DataList,使其在使用者没有指定ListTemplate
时使用<ul>
和<li>
作为默认值,之后我们将处理使用者确实想要使用自定义ListTemplate
的场景。
Rendering using a default list template
这是最简单的部分。我们所需要做的就是编写标记,就好像组件上没有ListTemplate
这样的东西,就像我们通常会做的那样-但只有在ListTemplate
属性为空的情况下才会这样。
@typeparam TItem
@if (ListTemplate == null)
{
<ul>
@foreach (TItem item in Data ?? Array.Empty<TItem>())
{
@ItemTemplate(item)
}
</ul>
}
@code
{
[Parameter]
public IEnumerable<TItem> Data { get; set; }
[Parameter]
public RenderFragment<TItem> ItemTemplate { get; set; }
[Parameter]
public RenderFragment<RenderFragment> ListTemplate { get; set; }
}
- Line 2
检查ListTemplate
是否为空
- Lines 4-9
如果ListTemplate
为空,那么我们呈现一个标准的<ul>
列表,然后使用ItemTemplate
呈现数据中的元素。
👍Rendering with a custom ListTemplate
RenderFragment<TItem>
期望我们在每次呈现时传递一个TItem
实例。这很容易做到,因为我们有一个IEnumerable<TItem>
,我们可以从其中提取要呈现的值,但是当我们需要将RenderFragment
的实例传递给我们的模板时,我们该怎么做呢?
要定义非泛型RenderFragment
,我们可以使用标准的Razor转义序列来表示HTML,即@:
RenderFragment rf = @<h1>Hello</h1>;
要定义RenderFragment<T>
,我们需要使用传入T
实例的lambda
RenderFragment<Person> rf =
person => @<h1>Hello @person.Name</h1>;
那么,我们如何返回一个RenderFragment
,该RenderFragment
仅在呈现数据属性时循环遍历数据属性中的项呢?为此,我们需要使用wig-pog语法。
The wig-pig
WIG-PIG是一组字符,Razor渲染引擎可以使用它们来表示C#文件中的一大块Razor标记。由于显而易见的原因,此字符序列仅在
.razor
文件中有效。任何人的视觉上的相似之处,无论是活着的还是死去的,都纯属巧合。
注意:
@:@{
实际上是两个字符序列。第一个@:
告诉Razor解析器将下面的文本视为Razor标记,然后@{
是C#代码挡路的开始-它显然会在某个地方以互补的}
结束。最终,这给我们提供了一大块Razor标记,相当于其中包含C#代码的RenderFragment
,可以执行循环等操作。
调用@ListItem(...)
时在我们的Razor文件中,我们可以使用wig-pIG语法将就地RenderFragment
作为参数传递。
@ListTemplate(
@:@{
foreach (TItem item in Data ?? Array.Empty<TItem>())
{
@item.ToString()
}
}
)
使用此语法,我们在<ListTemplate>
中呈现由消费者指定的标记,并传入将呈现数据中所有元素的RenderFragment
。在前面的代码中,它只是对数据中的每一项调用ToString()
。但是,理想情况下,我们希望使用消费者为我们提供的ItemTemplate
。
@ListTemplate(
@:@{
foreach (TItem item in Data ?? Array.Empty<TItem>())
{
@ItemTemplate(item)
}
}
)
DataList.razor
@typeparam TItem
@if (ListTemplate == null)
{
<ul>
@foreach (TItem item in Data ?? Array.Empty<TItem>())
{
@ItemTemplate(item)
}
</ul>
}
else
{
@ListTemplate(
@:@{
foreach (TItem item in Data ?? Array.Empty<TItem>())
{
@ItemTemplate(item)
}
}
)
}
@code
{
[Parameter]
public IEnumerable<TItem> Data { get; set; }
[Parameter]
public RenderFragment<TItem> ItemTemplate { get; set; }
[Parameter]
public RenderFragment<RenderFragment> ListTemplate { get; set; }
}
Consuming our DataList
Sample 1: A simple list
<DataList Data=@People>
<ItemTemplate>
<li @key=context>@context.Salutation @context.GivenName @context.FamilyName</li>
</ItemTemplate>
</DataList>
生成以下内容
<ul>
<li>Mr Bob Geldof</li>
<li>Mrs Angela Rippon</li>
<li>Mr Freddie Mercury</li>
</ul>
Sample 2: An HTML table
<DataList Data=@People>
<ListTemplate Context="listOfPeople">
<table border=1 cellpadding=4>
<thead>
<tr>
<th>Salutation</th>
<th>Given name</th>
<th>Family name</th>
</tr>
</thead>
<tbody>
@listOfPeople
</tbody>
</table>
</ListTemplate>
<ItemTemplate Context="person">
<tr @key=@person>
<td>@person.Salutation</td>
<td>@person.GivenName</td>
<td>@person.FamilyName</td>
</tr>
</ItemTemplate>
</DataList>
生成以下内容
<table cellpadding="4" border="1">
<thead>
<tr>
<th>Salutation</th>
<th>Given name</th>
<th>Family name</th>
</tr>
</thead>
<tbody>
<tr>
<td>Mr</td>
<td>Bob</td>
<td>Geldof</td>
</tr>
<tr>
<td>Mrs</td>
<td>Angela</td>
<td>Rippon</td>
</tr>
<tr>
<td>Mr</td>
<td>Freddie</td>
<td>Mercury</td>
</tr>
</tbody>
</table>
Assigning defaults when RenderFragments are not specified
目前,我们必须在视图中使用@if
语句检查ListTemplate
是否为空,甚至不检查是否设置了ItemTemplate
。这两种方法都不是理想的。
相反,如果使用者没有设置RenderFragment
属性,我们应该将它们设置为所需的默认值。这样,我们的组件的呈现逻辑可以保持简单得多。
@ListTemplate(
@:@{
foreach(TItem item in CurrentPage)
{
@ItemTemplate(item)
}
}
)
覆盖组件中的OnParametersSet()
,并确保ItemTemplate
属性不为空。为此,我们创建了一个lambda
,它接收TItem
并返回RenderFragment
(使用wig-pig语法)。
protected override void OnParametersSet()
{
if (ItemTemplate == null)
{
ItemTemplate = (item) => @:@{
<li @key=item>@item.ToString()</li>}
;
}
}
为了确保ListTemplate
不为空,我们创建了一个lamba,它接收RenderFragment
,然后忽略它并返回自定义的wig-pigRenderFragment
。
if (ListTemplate == null)
{
ListTemplate = _ => @:@{
<ul>
@foreach(TItem item in CurrentPage)
{
@ItemTemplate(item)
}
</ul>
}
;
}
备注:
;
不能与前面的}
位于同一行,否则Razor解析器无法正确解析源代码。这已被报告为错误,并有望在不久的将来得到修复。
Summary
当消费者希望在其标记中标识将在呈现期间传递给他们的内容的占位符(例如Blazor布局中的正文占位符)时,应使用RenderFragment<RenderFragment>
技术。
希望本节已经帮助您理解了何时使用此技术,以及如何使用wig-pigg@:@{
语法生成就地RenderFragments
。
Additional reading
本节的源代码包括一个使用PagedDataList组件的附加示例页。