Blazor动态插槽实现
背景
.Net MVC一开始就让我觉得别扭,跑去写了好几年的Vue之后,Blazor终于来了,不用再来回切换vs和vs code了,马上试用,发现有一些地方还是让人不爽(反应迟钝的红色波浪)
特别是我个人是很喜欢偷懒的,所以CRUD业务很喜欢封装成组件来使用,当我用Vue的思维来编写偷懒组件的时候,发现官方没有实现动态插槽填充。。。
这是Vue中Element UI表格的再封装
<template>
<div>
<el-table
:show-summary="showsummary"
:stripe="stripe"
:border="border"
:size="size"
v-loading="tableParameter.isLoding"
:data="getDatas"
ref="Table"
:height="height === '' ? resizeHeight : height"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column v-if="needSelector" type="selection" width="50" />
<el-table-column label="序号" v-if="needIndex" type="index" width="60" />
<el-table-column
:fixed="item.fixed"
v-for="(item, index) in tableParameter.columnInfos"
:key="index"
:prop="item.prop"
:label="item.name"
:width="item.width"
>
<template #header v-if="item.hSlot">
<slot :name="item.hSlot"> </slot>
</template>
<template #default="scope">
<slot :name="index" :rowData="scope.row">
{{ scope.row[item.prop] }}
</slot>
</template>
</el-table-column>
</el-table>
</div>
</template>
How To Used
const columnInfos = [
{
name: '主题',
prop: 'someProp',
width: '200',
},
{
name: '类型',
prop: 'type',
width: 'auto',
},
]
<GlobalPagingTable :tableParameter="tableParameter" height="460px" @search="search" :needSelector="false">
<template #someProp="{ rowData }">
<div>
{{ rowData.someProp }}
</div>
</template>
</GlobalPagingTable>
Blazor实现
到了Blazor,就发现这样做难做了,Blazor的传值插槽(反正我管他叫插槽):
[Parameter]
public RenderFragment<TModel>? ChildContent { get; set; } = null;
每个插槽约定一个位置,使用表格的时候,每一列都应有一个插槽,用来动态填充某些内容,在Blazor中,插槽的支持是编码阶段就绑定死的,List<RenderFragment<TModel>> 肯定是不支持的,没法在Razor视图中填充List的RenderFragment,那就只能曲线救国了:
<Table @ref="Table"
TItem="TModel"
DataSource="@TableParameters!.Datas"
Total="TableParameters.Total"
Loading="TableParameters.Loading"
HidePagination="TableParameters.NeedPager"
ScrollY="@Height"
Size=@TableParameters.Size
ScrollX="@TableParameters.ScrollX"
Bordered="@TableParameters.Bordered"
SelectedRows="TableParameters.SelectedDatas"
SelectedRowsChanged="OnSelectedRowsChanged"
OnChange="OnChange">
@{
if (TableParameters.MultiSelection)
{
<Selection Key="@(typeof(TModel).GetProperty("Id")?.GetValue(context) as string)" />
}
}
@foreach (var tableColumn in TableParameters.TableColumnInfos!)
{
if (tableColumn.HasSlot && ChildContent != null)
{
<PropertyColumn Fixed="@tableColumn.Fixed" Title="@tableColumn.Header" Property="GetPropertyExpression(tableColumn.Prop!)" Sortable="tableColumn.Sortable">
<CascadingValue Value="@(new SlotModel<TModel>() { ColumnInfo = tableColumn, Model = @context })">
@(ChildContent(new SlotModel<TModel>() { ColumnInfo = tableColumn, Model = @context }))
</CascadingValue>
</PropertyColumn>
}
else
{
<PropertyColumn Fixed="@tableColumn.Fixed" Title="@tableColumn.Header" Property="GetPropertyExpression(tableColumn.Prop!)" Sortable="tableColumn.Sortable" />
}
}
</Table>
只看重点:
if (tableColumn.HasSlot && ChildContent != null)
{
<PropertyColumn Fixed="@tableColumn.Fixed" Title="@tableColumn.Header" Property="GetPropertyExpression(tableColumn.Prop!)" Sortable="tableColumn.Sortable">
<CascadingValue Value="@(new SlotModel<TModel>() { ColumnInfo = tableColumn, Model = @context })">
@(ChildContent(new SlotModel<TModel>() { ColumnInfo = tableColumn, Model = @context }))
</CascadingValue>
</PropertyColumn>
}
看视图使用
public override TableParameters<IdentityServerApiScopeDQM> TableParameters { get; set; } = new TableParameters<IdentityServerApiScopeDQM>()
{
Length = 15,
TableColumnInfos = new List<TableColumnInfo>()
{
new TableColumnInfo(){ Prop = nameof(IdentityServerApiScopeDQM.Name), Header = "名称", Sortable = true, Width = "200px"},
new TableColumnInfo(){ Prop = nameof(IdentityServerApiScopeDQM.DisplayName), Header = "显示名称", Sortable = true},
new TableColumnInfo(){ Prop = nameof(IdentityServerApiScopeDQM.Id), Header = "Id"},
new TableColumnInfo(){ Prop = nameof(IdentityServerApiScopeDQM.Required), Header = "是否必须", Sortable = true, HasSlot = true},
new TableColumnInfo(){ Prop = nameof(IdentityServerApiScopeDQM.Emphasize), Header = "是否重要", Sortable = true, HasSlot = true},
new TableColumnInfo(){ Prop = nameof(IdentityServerApiScopeDQM.SortCode), Header = "排序码", Sortable = true},
new TableColumnInfo(){ Prop = nameof(IdentityServerApiScopeDQM.Description), Header = "简介"},
new TableColumnInfo(){ Prop = "Operation", Header = "操作", HasSlot = true},
},
};
<LightPointTable @ref="LightPointTableRef" TModel="IdentityServerApiScopeDQM" @bind-TableParameters="@TableParameters" Search="Search">
<LightPointTdSlot TModel="IdentityServerApiScopeDQM" Name="Required">
@(context.Model.Required ? "是" : "否")
</LightPointTdSlot>
<LightPointTdSlot TModel="IdentityServerApiScopeDQM" Name="Emphasize">
@(context.Model.Emphasize ? "是" : "否")
</LightPointTdSlot>
<LightPointTdSlot TModel="IdentityServerApiScopeDQM" Name="Operation">
<Button Type="@ButtonType.Primary" OnClick="()=> OpenForm(context.Model)">编辑</Button>
<Button Type="@ButtonType.Primary" Danger OnClick="()=> Delete(context.Model.Id)">删除</Button>
</LightPointTdSlot>
</LightPointTable>
只需要将LightPointTdSlot的Name属性对应上TableColumnInfo中的Prop属性即可,是不是就跟vue用template很类似了?LightPointTdSlot的实现也很简单:
@{
if (SlotModel?.ColumnInfo?.Prop == Name)
{
@ChildContent
}
}
@typeparam TModel where TModel : class, new()
@code {
[Parameter]
public string? Name { get; set; }
[Parameter]
public RenderFragment? ChildContent { get; set; }
[CascadingParameter]
public SlotModel<TModel>? SlotModel { get; set; }
}
基于以上的写法,我们就可以在Blazor中愉快的使用类似于Vue的template #xxx具名插槽的语法啦
了解了实现原理之后,我们同时也可以写出动态表单了(注意,这里是用了Ant Blazor,其中的数据绑定比较开放,不保证每一种UI框架封装的基本控件都有这么open的数据绑定方法):
<Form Model="@Model"
Layout="@Layout"
WrapperColSpan="WrapperColSpan"
LabelColSpan="LabelColSpan"
ValidateMode="@FormValidateMode.Rules"
LabelAlign="LabelAlign"
ValidateOnChange="@ValidateOnChange"
OnFinish="OnSubmit"
OnFinishFailed="OnSubmitFailed"
@ref="FormRef"
Size="@Size">
<GridRow Gutter="16">
@foreach (var formItem in FormItemConfigs)
{
if (!formItem.IsHiddenExpression(Model))
{
<GridCol Class="gutter-row" Span="formItem.Span">
@{
var type = typeof(TModel).GetProperty(formItem.Prop)?.PropertyType;
var setPropertyFunc = CreateSetPropertyAction(formItem.Prop);
var getPropertyFunc = CreateGetPropertyFunc(formItem.Prop);
#region 文本框
if (formItem.FormItemType == FormItemType.文本框)
{
if (type == typeof(string))
{
<FormItem Label="@formItem.Label" Rules="@(formItem.FormValidationRules.ToArray())">
<Input ValueExpression="GetPropertyExpression<string>(formItem.Prop)"
ValueChanged="@(GetEventCallBack<string>(formItem))"
Disabled="@formItem.Disabled"
DefaultValue="(string)getPropertyFunc!(Model)"
Style="width : 100%"
TValue="string"
Placeholder="@formItem.Placeholder" />
</FormItem>
}
else if (type == typeof(int))
{
<FormItem Label="@formItem.Label" Rules="@(formItem.FormValidationRules.ToArray())">
<AntDesign.InputNumber ValueExpression="GetPropertyExpression<int>(formItem.Prop)"
ValueChanged="@(GetEventCallBack<int>(formItem))"
Disabled="@formItem.Disabled"
DefaultValue="(int)getPropertyFunc!(Model)"
TValue="int"
Style="width : 100%"
Min="(int)formItem.Min" Max="(int)formItem.Max" PlaceHolder="@formItem.Placeholder"></AntDesign.InputNumber>
</FormItem>
}
else if (type == typeof(float))
{
<FormItem Label="@formItem.Label" Rules="@(formItem.FormValidationRules.ToArray())">
<AntDesign.InputNumber ValueExpression="GetPropertyExpression<float>(formItem.Prop)"
ValueChanged="@(GetEventCallBack<float>(formItem))"
Disabled="@formItem.Disabled"
DefaultValue="(float)getPropertyFunc!(Model)"
TValue="float"
Style="width : 100%"
Min="(float)formItem.Min" Max="(float)formItem.Max" PlaceHolder="@formItem.Placeholder"></AntDesign.InputNumber>
</FormItem>
}
}
else if (formItem.FormItemType == FormItemType.多行文本框)
{
<FormItem Label="@formItem.Label" Rules="@(formItem.FormValidationRules.ToArray())">
<TextArea Rows="formItem.Rows" ValueExpression="GetPropertyExpression<string>(formItem.Prop)"
ValueChanged="@(GetEventCallBack<string>(formItem))" Disabled="@formItem.Disabled"
Style="width : 100%"
DefaultValue="@((string)getPropertyFunc!(Model))"
MaxLength="@((int)formItem.Max)" />
</FormItem>
}
else if (formItem.FormItemType == FormItemType.密码输入框)
{
<FormItem Label="@formItem.Label" Rules="@(formItem.FormValidationRules.ToArray())">
<Input ValueExpression="GetPropertyExpression<string>(formItem.Prop)"
ValueChanged="@(GetEventCallBack<string>(formItem))"
Disabled="@formItem.Disabled"
DefaultValue="(string)getPropertyFunc!(Model)"
Type="password"
Style="width : 100%"
Placeholder="@formItem.Placeholder" />
</FormItem>
}
#endregion
<!-- 这里省略一堆其他类型表单控件的判断实现 -->
#region 填充块
else if (formItem.FormItemType == FormItemType.填充块)
{
<div></div>
}
#endregion
#region 自定义组件
else if (formItem.FormItemType == FormItemType.自定义组件 && ChildContent != null)
{
<FormItem Label="@formItem.Label" Rules="@(formItem.FormValidationRules.ToArray())">
<CascadingValue Value="@(new FormItemSlotModel<TModel>() { FormItemConfig = formItem, Model = @context })">
@(
ChildContent(new FormItemSlotModel<TModel>() { FormItemConfig = formItem, Model = @context })
)
</CascadingValue>
</FormItem>
}
#endregion
}
</GridCol>
}
}
</GridRow>
</Form>
使用:
public override List<FormItemConfig> FormItemConfigs { get; set; } = new List<FormItemConfig>()
{
new FormItemConfig(){ FormItemType = FormItemType.文本框, Label = "名字", Placeholder = "请输入名字", Prop = "Name", FormValidationRules = Rules.Required, Span = 8 },
new FormItemConfig(){ FormItemType = FormItemType.文本框, Label = "排序码", Placeholder = "请输入排序码", Prop = nameof(ApplicationUserDCM.SortCode), FormValidationRules = Rules.Required, Span = 8},
new FormItemConfig(){ FormItemType = FormItemType.文本框, Label = "用户名", Placeholder = "请输入用户名", Prop = "UserName", FormValidationRules = Rules.Required, Span = 8 },
new FormItemConfig(){ FormItemType = FormItemType.密码输入框, Label = "密码", Placeholder = "请输入密码", Prop = nameof(ApplicationUserDCM.Password), FormValidationRules = Rules.Required, Span = 8, IsHiddenExpression = (model) => ((ApplicationUserDCM)model).Id != Guid.Empty },
new FormItemConfig(){ FormItemType = FormItemType.密码输入框, Label = "再次确认", Placeholder = "请再次输入密码确认", Prop = nameof(ApplicationUserDCM.ConfirmPassword), FormValidationRules = new List<FormValidationRule>()
{
new FormValidationRule(){ Required = true, Type = FormFieldType.String, Message = "{0}是必填的"},
},
Span = 8, IsHiddenExpression = (model) => ((ApplicationUserDCM)model).Id != Guid.Empty },
new FormItemConfig(){ FormItemType = FormItemType.开关, Label = "是否启用", Placeholder = "请输入密码", Prop = nameof(ApplicationUserDCM.IsEnable), Span = 8 },
new FormItemConfig(){ FormItemType = FormItemType.开关, Label = "是否开启双因子认证", Placeholder = "是否开启双因子认证", Prop = nameof(ApplicationUserDCM.TwoFactorEnabled), Span = 8 },
new FormItemConfig(){ FormItemType = FormItemType.开关, Label = "是否开启多因子认证", Placeholder = "是否开启多因子认证", Prop = nameof(ApplicationUserDCM.MutilFactorEnabled), Span = 8 },
new FormItemConfig(){ FormItemType = FormItemType.文本框, Label = "邮箱", Placeholder = "邮箱", Prop = nameof(ApplicationUserDCM.Email), Span = 8 },
new FormItemConfig(){ FormItemType = FormItemType.开关, Label = "邮箱是否已经校验", Placeholder = "邮箱是否已经校验", Prop = nameof(ApplicationUserDCM.EmailConfirmed), Span = 8 },
new FormItemConfig(){ FormItemType = FormItemType.文本框, Label = "手机号码", Placeholder = "手机号码", Prop = nameof(ApplicationUserDCM.PhoneNumber), Span = 8 },
new FormItemConfig(){ FormItemType = FormItemType.开关, Label = "手机号码是否已经校验", Placeholder = "手机号码是否已经校验", Prop = nameof(ApplicationUserDCM.PhoneNumberConfirmed), Span = 8 },
new FormItemConfig(){ FormItemType = FormItemType.自定义组件, Label = "角色绑定", Placeholder = "角色绑定", Prop = "Roles", Span = 24 },
new FormItemConfig(){ FormItemType = FormItemType.多行文本框, Label = "备注", Placeholder = "请输入备注", Prop = nameof(ApplicationUserDCM.Description) },
};
# 视图代码:
<LightPointDialogForm LabelColSpan="24" WrapperColSpan="24" Layout="@FormLayout.Vertical" TModel="ApplicationUserDCM" @ref="LightPointFormRef" FormItemConfigs="FormItemConfigs" OnSubmit="SubmitForm">
<FormItemSlot TModel="ApplicationUserDCM" Name="Roles">
@{
if(AllRoles != null)
{
foreach(var role in AllRoles)
{
<Checkbox Label="@role.Label" OnChange="(val)=> HandleCheckBoxChange(context.Model!, role, val)" Checked="@(BindedRoles.Any(x=>x.Value == role.Value))" />
}
}
}
</FormItemSlot>
</LightPointDialogForm>
FormItemSlot的实现:
@{
if (SlotModel?.ColumnInfo?.Prop == Name)
{
@ChildContent
}
}
@typeparam TModel where TModel : class, new()
@code {
[Parameter]
public string? Name { get; set; }
[Parameter]
public RenderFragment? ChildContent { get; set; }
[CascadingParameter]
public SlotModel<TModel>? SlotModel { get; set; }
}

浙公网安备 33010602011771号