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
相关产品推荐
相关产品推荐

