Blazor动态插槽实现

背景

.Net MVC一开始就让我觉得别扭,跑去写了好几年的Vue之后,Blazor终于来了,不用再来回切换vs和vs code了,马上试用,发现有一些地方还是让人不爽(反应迟钝的红色波浪)

特别是我个人是很喜欢偷懒的,所以CRUD业务很喜欢封装成组件来使用,当我用Vue的思维来编写偷懒组件的时候,发现官方没有实现动态插槽填充。。。

这是VueElement 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视图中填充ListRenderFragment,那就只能曲线救国了:

<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>

只需要将LightPointTdSlotName属性对应上TableColumnInfo中的Prop属性即可,是不是就跟vuetemplate很类似了?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中愉快的使用类似于Vuetemplate #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; }
}
posted @ 2024-03-28 15:27  LazYu  阅读(154)  评论(0)    收藏  举报