打开MASA Blazor的正确姿势5:插槽/模板
依照Vuetify的习惯,MASABlazor将RenderFragment称为插槽。RenderFragment是Blazor的一个难点,而MASA Blazor是二次开发的组件库,大量使用到插槽,所以有必要掌握这个功能。注:在Blazor中,一般将RenderFragment称为模板。
一、插槽RenderFragment的定义和使用
组件式开发模式,带来了复用、灵活、性能等优势,但也增加了组件之间数据传递的复杂度。组件之间数据传递的方式一般包括以下四种:①通过属性实现父传子,②通过事件实现子传父,③通过插槽实现传递UI片断,④通过状态管理实现跨组件数据共享。
1、默认插槽
//子组件Child.razor
//使用RenderFragment类型的属性参数,接收父组子传递过来的内容
//@ChildContent就像是一个预留的插槽,具体渲染什么内容,由调用Child组件的父组件传入
//父组件在使用Child组件时,传入具体的插槽内容,可以是普通文本、响应式数据、HTML原生元素、自定义组件及其任意组合
//默认插槽,属性名称约定为ChildContent,不能使用其它名称
<div>@ChildContent</div>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
}
//父组件Index.razor
//在子组件Child的标签体位置,定义传入子组件插槽中的内容。【<Child>这个位置</Child>】
<Child>普通文本</Child>
<Child>响应式数据:@msg</Child>
<Child>
<h5>任意HTML原生元素和自定义组件标签</h5>
<Counter></Counter>
</Child>
@code {
private string msg = "响应式数据";
}
2、具名插槽
//子组件Child.razor
//定义多个RenderFragment类型的属性参数(插槽)
//具名插槽可以任意命名。而默认插槽则只能使用ChildContent
<div>@Header</div>
<div>@Body</div>
<div>@Footer</div>
<div>@ChildContent</div>
@code {
[Parameter]
public RenderFragment? Header { get; set; }
[Parameter]
public RenderFragment? Body { get; set; }
[Parameter]
public RenderFragment? Footer { get; set; }
[Parameter]
public RenderFragment? ChildContent { get; set; }
}
//父组件Index.razor
//使用标签方式传递具名插槽,标签名就是子组件的插槽名。
<Child>
<Header>
<h1>这里是Header片断</h1>
</Header>
<Body>
<h3>这里是Body片断</h3>
</Body>
<Footer>
<h5>这里是Footer片断</h5>
</Footer>
<h1>其它未具名的内容,都由默认插槽ChildContent接收</h1>
</Child>
@code {
}
3、RenderFragment的本质
- RenderFragment的本质是一个委托,参数是RenderTreeBuilder对象
- 源码【public delegate void RenderFragment(RenderTreeBuilder builder);】
- RenderTreeBuilder是Blazor中用于渲染UI的对象
- 在视图层调用@HelloContent,实际上就是在调用委托,而这个委托执行RenderTreeBuilder的一系列渲染UI的方法
- RenderFragment委托的方法体,正是我们在默认插槽或具体插槽中传入组件的内容。
//以下三种方法是等效的
//方法一:
<div><h1>hello world</h1></div>
//方法二:
<div>@HelloContent</div>
@code{
private RenderFragment HelloContent=(RenderTreeBuilder builder)=>{
builder.OpenElement(0,"h1");
builder.AddContent(1, "hello world");
builder.CloseElement();
};
}
//方法三:
//注意这种方式,参数名称必须为__builder
<div>@HelloContent</div>
@code {
private RenderFragment HelloContent = (RenderTreeBuilder __builder) =>
{
<h1>hello world </h1>
};
}
4、更复杂的泛型RenderFragment<T>
的本质
RenderFragment<TValue>
的本质可以理解为是委托的委托,返回值是一个RenderFragment- 源码【public delegate RenderFragment RenderFragment
(TValue value);】 - 在视图层调用@HelloContent("world"),将实参传入HelloContent委托
- 通过这种方法,可以实现将数据从插槽传到父组件。
- 泛型参数T可以是任意类型。
//将参数传入到委托的方法体中(如上我们知道,方法体中执行的一系列UI渲染,就是从父组件传入到插槽的内容)
//HelloContent委托,定义形参msg
//调用@HelloContent()时,传入实参
<div>@HelloContent("world")</div>
@code {
private RenderFragment<string> HelloContent = (msg) => (RenderTreeBuilder builder) =>
{
builder.OpenElement(0, "div");
builder.AddContent(1, "hello " + msg);
builder.CloseElement();
};
}
5、泛型RenderFragment<T>
的小案例
//子组件Child.razor
@ChildContent(msg)
@code {
[Parameter]
public RenderFragment<string>? ChildContent { get; set; }
private string msg = "hello world";
}
//父组件Index.razor
//获取子组件传递过来的msg,有两种方式
//方式一:通过@context
<Child>
<div>
@context
</div>
</Child>
/*方法二:通过定义Context属性
<Child Context="msg"> //msg可以任意命名
<div>
@msg
</div>
</Child>
*/
6、泛型RenderFragment<T>
的进一步抽象:泛型参数还可以是泛型,不指定具体类型
//子组件Child.razor
@typeparam T //定义泛型参数T
@ChildContent((T)msg) //将msg强制转化为T泛型
@code {
[Parameter]
public RenderFragment<T>? ChildContent { get; set; }
private object msg = "hello world";
}
//父组件Index.razor
<Child T="string"> //指定泛型类型
<div>
@context
</div>
</Child>
二、MASA Blazor中插槽的应用
1、以简单的Dialogs为例,直接解读代码
<div class="text-center">
<MDialog @bind-Value="dialog" Width="500">
<!--具名插槽ActivatorContent
1、在插槽中使用了@context,说明这个插槽是一个泛型RenderFragment<T>
2、查看组件API,可以看到插槽的类型为RenderFragment<ActivatorProps>
3、ActivatorProps类定义了一个字典类型属性Attrs,ActivatorProps{Dictionary<string,object> Attrs}
4、MDialog组件或其继承类,创建ActivatorProps对象,并通过@ActivatorContent(activatorProps),传递ActivatorProps对象
5、@attributes,属性展开。属性值须为字典类型,展开后key为属性名,value为属性值。
6、@attributes="@context.Attrs",读取字典值,展开后,设置为MButton组件的属性。
-->
<ActivatorContent>
<MButton
Color="red lighten-2"
Dark
@attributes="@context.Attrs">
Click Me
</MButton>
</ActivatorContent>
<!--默认插槽ChildContent-->
<ChildContent>
<MCard>
<MCardTitle Class="text-h5 grey lighten-2">卡片标题</MCardTitle>
<MCardText>卡片内容</MCardText>
</MCard>
</ChildContent>
</MDialog>
</div>
Dialog的API文档-插槽部分:
2、插槽应用最复杂的应该是Table组件,有机会可以读一下源码