You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Blazor GenericEditForm运行时数据绑定问题求助

Blazor通用编辑表单动态数据绑定解决方案

问题背景

需要实现可复用的GenericEditForm组件,通过@typeparam TModel适配不同模型,结合JSON渲染定义动态生成表单,但由于模型属性仅在运行时可知,无法直接使用@bind-Value实现双向绑定。

核心解决方案

通过反射获取属性信息+手动实现Blazor组件绑定逻辑替代编译时的@bind-Value,同时优化类型转换与组件状态更新逻辑。

1. 完善属性值的类型转换与赋值

修改SetValue方法,添加类型安全转换、默认值处理,并触发UI更新:

public void SetValue(object inputValue, PropertyInfo propertyInfo)
{
    if (inputValue == null)
    {
        propertyInfo.SetValue(CreateModel, GetDefaultValue(propertyInfo.PropertyType));
        StateHasChanged();
        return;
    }

    // 处理可空类型
    Type targetType = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType;
    object convertedValue;

    try
    {
        convertedValue = Convert.ChangeType(inputValue, targetType);
    }
    catch
    {
        // 转换失败时设置对应类型默认值
        convertedValue = GetDefaultValue(targetType);
    }

    propertyInfo.SetValue(CreateModel, convertedValue);
    // 通知Blazor更新UI
    StateHasChanged();
}

private object GetDefaultValue(Type type)
{
    return type.IsValueType ? Activator.CreateInstance(type) : null;
}

2. 用Blazor内置组件替代原生HTML控件

Blazor内置表单组件(InputText/InputSelect等)自带状态管理,通过ValueChanged和ValueExpression手动模拟双向绑定:

InputText示例

case "InputText":
<EditFormControlWrapper FieldName="@field.DisplayName" IsRequired="@field.Rules.Mandatory">
    <div class="col-xs-17 col-sm-12 col-md-10 col-lg-11">
        <InputText class="form-control" 
                   Value="@GetPropertyValue(propertyInfo)"
                   ValueChanged="@(value => SetValue(value, propertyInfo))"
                   ValueExpression="@(() => GetPropertyValue(propertyInfo))" />
    </div>
</EditFormControlWrapper>
break;

InputSelect示例

case "InputSelect":
if (field.DisplayName == "Status") break;
<EditFormControlWrapper FieldName="@field.DisplayName" IsRequired="@field.Rules.Mandatory" isSelect="true">
    <div class="col-xs-17 col-sm-12 col-md-10 col-lg-11 select">
        <InputSelect class="form-control" 
                     Value="@GetPropertyValue(propertyInfo)"
                     ValueChanged="@(value => SetValue(value, propertyInfo))"
                     ValueExpression="@(() => GetPropertyValue(propertyInfo))">
            <option value="">--请选择--</option>
            @foreach (var item in field.Items)
            {
                <option value="@item">@item</option>
            }
        </InputSelect>
    </div>
</EditFormControlWrapper>
break;

3. 优化模型初始化逻辑

支持传入现有模型进行编辑,避免总是创建新实例:

protected override async Task OnParametersSetAsync()
{
    try
    {
        if (EditObject is TItem existingModel)
        {
            CreateModel = existingModel;
        }
        else
        {
            CreateModel = Activator.CreateInstance<TItem>();
        }

        hasStatusField = CreateModel.GetType().GetProperty("Status") != null;
    }
    catch (Exception ex)
    {
        Console.WriteLine($"初始化模型失败: {ex.Message}");
    }
}

4. 集成表单验证

在组件外层包裹EditForm并添加验证组件,结合模型数据注解或JSON配置规则实现验证:

<EditForm Model="@CreateModel">
    <DataAnnotationsValidator />
    <ValidationSummary />
    
    @* 原有字段渲染循环 *@
    @foreach (var field in FormControlConfig.Fields)
    {
        // ... 字段渲染逻辑
    }
</EditForm>

原始问题相关代码

JSON渲染配置

{
  "CreateTitle": "Create new committee",
  "Fields": [
    {
      "ModelPropName": "Department",
      "DisplayName": "Department",
      "FormControlType": "InputSelect",
      "Items": [
        "Afdeling A",
        "Afdeling B",
        "Afdeling C"
      ],
      "Rules": {
        "Mandatory": true,
        "Regex": "[a-zA-Z0-9.,_ åæøÅÆØ-]{0,100}"
      }
    },
    {
      "ModelPropName": "CommitteeName",
      "DisplayName": "Committee name",
      "FormControlType": "InputText",
      "Rules": {
        "Mandatory": true,
        "regex": "[a-zA-Z0-9.,_ åæøÅÆØ-]{0,100}"
      }
    },
    {
      "ModelPropName": "Status",
      "DisplayName": "Status",
      "FormControlType": "InputSelect",
      "Items": [
        "Status 1",
        "Status 2"
      ],
      "Rules": {
        "Mandatory": true,
        "regex": ""
      }
    }
  ]
}

原GenericEditForm组件代码

@foreach (var field in FormControlConfig.Fields)
                        {
                            var propertyName = field.ModelPropName;
                            var propertyInfo = typeof(TItem).GetProperty(propertyName);
                            var propertyValue = GetPropertyValue(propertyInfo);
                            switch (field.FormControlType)
                            {
                                case "InputText":
                                    <EditFormControlWrapper FieldName="@field.DisplayName" IsRequired="@field.Rules.Mandatory">
                                        <div class="col-xs-17 col-sm-12 col-md-10 col-lg-11">
                                            <input type="text"
                                   spellcheck="true"
                                   class="form-control"
                                   id="@propertyName"
                                   value="@propertyValue"
                                   @oninput="e => SetValue(e.Value, propertyInfo)">
                                        </div>
                                    </EditFormControlWrapper>
                                    break;
                                case "InputSelect":
                                    @if (field.DisplayName == "Status")
                                    {
                                        break;
                                    }
                                    <EditFormControlWrapper FieldName="@field.DisplayName" IsRequired="@field.Rules.Mandatory" isSelect="true">
                                        <div class="col-xs-17 col-sm-12 col-md-10 col-lg-11 select">
                                            <select class="form-control"
                                    id="@propertyName"
                                    value="@propertyValue"
                                    @oninput="e => SetValue(e.Value, propertyInfo)">
                                                <option value="" />
                                                @foreach (var item in field.Items)
                                                {
                                                    <option value="@item">@item</option>
                                                }
                                            </select>
                                        </div>
                                    </EditFormControlWrapper>
                                    break;
                                case "InputTextArea":
                                    <EditFormControlWrapper FieldName="@field.DisplayName" IsRequired="@field.Rules.Mandatory">
                                        <div class="col-xs-17 col-sm-12 col-md-10 col-lg-11">
                                            <textarea cols="20"
                                      rows="4"
                                      type="text"
                                      spellcheck="true"
                                      class="form-control"
                                      id="@propertyName"
                                      value="@propertyValue"
                                      @oninput="e => SetValue(e.Value, propertyInfo)" />
                                        </div>
                                    </EditFormControlWrapper>
                                    break;
                                case "InputDate":
                                    <EditFormControlWrapper FieldName="@field.DisplayName" IsRequired="@field.Rules.Mandatory">
                                        <div class="col-xs-17 col-sm-7 col-md-10 col-lg-3">
                                            <input type="date"
                                   class="form-control datepicker hasDatepicker"
                                   value="@Convert.ToString(propertyValue)"
                                   @oninput="e => SetValue(DateTime.Parse(e.Value.ToString()), propertyInfo)" />
                                        </div>
                                    </EditFormControlWrapper>
                                    break;
                                case "InputTime":
                                    var startName = "MeetingTimeStart";
                                    var endName = "MeetingTimeEnd";
                                    var propertyInfoStart = typeof(TItem).GetProperty(startName);
                                    var propertyInfoEnd = typeof(TItem).GetProperty(endName);
                                    var startValue = GetPropertyValue(propertyInfoStart);
                                    var endValue = GetPropertyValue(propertyInfoEnd);

                                    <EditFormControlWrapper FieldName="@field.DisplayName" IsRequired="@field.Rules.Mandatory">
                                        <div class="col-xs-17 col-sm-7 col-md-10 col-lg-2 select">
                                            <select class="form-control"
                                    min="09:00" max="18:00"
                                    size="1"
                                    id="@startName"
                                    value="@Convert.ToString(startValue)"
                                    @oninput="e => SetValue(TimeSpan.Parse(e.Value.ToString()), propertyInfoStart)">
                                                @for (int hour = 9; hour <= 18; hour++)
                                                {
                                                    @for (int minute = 0; minute < 60; minute += 15)
                                                    {
                                                        var timeSpan = new TimeSpan(hour, minute, 0);
                                                        var timeString = timeSpan.ToString(@"hh\:mm");
                                                        <option value="@timeSpan">@timeString</option>
                                                    }
                                                }
                                            </select>
                                        </div>
                                        <div class="col-lg-0">
                                            <label class="control-label">
                                                :
                                            </label>
                                        </div>
                                        <div class="col-xs-17 col-sm-7 col-md-10 col-lg-2 select">
                                            <select class="form-control"
                                    min="09:00" max="18:00"
                                    size="1"
                                    id="@endName"
                                    value="@Convert.ToString(endValue)"
                                    @oninput="e => SetValue(TimeSpan.Parse(e.Value.ToString()), propertyInfoEnd)">
                                                @for (int hour = 9; hour <= 18; hour++)
                                                {
                                                    @for (int minute = 0; minute < 60; minute += 15)
                                                    {
                                                        var timeSpan = new TimeSpan(hour, minute, 0);
                                                        var timeString = timeSpan.ToString(@"hh\:mm");
                                                        <option value="@timeSpan">@timeString</option>
                                                    }
                                                }
                                            </select>
                                        </div>
                                    </EditFormControlWrapper>
                                    break;
                            }
                        }
    
@code {
 [Parameter]
    public ProfileFormControlConfigModel FormControlConfig { get; set; }

    [Parameter]
    public object? EditObject { get; set; }

    public TItem CreateModel;

    public bool hasStatusField = false;

    public bool isActive = false;

    public Field field;

    protected override async Task OnParametersSetAsync()
    {
        try
        {
            CreateModel = Activator.CreateInstance<TItem>();

            var result = CreateModel.GetType().GetProperty("Status");

        if (result != null) { hasStatusField = true; }
        }
        catch (ArgumentNullException ex)
        {
            Console.WriteLine("Error: Property name is null.");
        }
        catch (AmbiguousMatchException ex)
        {
            Console.WriteLine("Error: More than one property with the specified name found.");
        }
        catch (Exception ex)
        {
            Console.WriteLine("Error: " + ex.Message);
        }
 public void SetValue(object value, PropertyInfo propertyInfo)
    {
        Type propType = propertyInfo.PropertyType;
        propertyInfo.SetValue(CreateModel, value);
    }

    private object GetPropertyValue(PropertyInfo propertyInfo)
    {
        Type propType = propertyInfo.PropertyType;

        object propertyValue;

        propertyValue = propertyInfo.GetValue(CreateModel);
        
        return propertyValue;
    }
}

内容的提问来源于stack exchange,提问作者FoodCoderMan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 00:17:35