实现类似Angular FormBuilder的动态表单功能:ExpandoObject绑定MudBlazor表单控件报错问题
我之前也遇到过类似的需求——想模仿Angular FormBuilder实现动态控件和验证,用ExpandoObject的时候踩过一模一样的坑。咱们来拆解下错误原因,再给出可行的解决方案:
为什么两种ExpandoObject方式都会报错?
1. 直接赋值动态属性的编译错误
Cannot use a lambda expression as an argument to a dynamically dispatched operation without first casting it to a delegate or expression tree type.
Blazor的@bind-Value语法本质上会生成一个lambda表达式(比如x => x.FirstName)来处理双向绑定,但因为FormModel是dynamic类型,编译器无法推断这个lambda的委托类型,这是C#动态类型的底层限制,没法绕过。
2. 字典方式的运行时转换错误
Conversion to type System.Object not implemented
当MudBlazor尝试更新控件值时,它需要把用户输入的内容(比如字符串类型的年龄)转换成对应属性的目标类型(int)。但ExpandoObject的字典存储的是object类型,MudBlazor的绑定机制无法自动处理这种动态类型的转换,导致运行时抛出错误。
可行的解决方案
方案1:用Dictionary<string, object>+指定控件泛型类型
这是最直接的替代方案,既保留动态性,又能解决类型转换问题:
protected Dictionary<string, object> FormModel = new(); private EditContext? _editContext; private ValidationMessageStore? _validationStore; protected override void OnInitialized() { // 初始化动态字段 FormModel["FirstName"] = string.Empty; FormModel["LastName"] = string.Empty; FormModel["Age"] = 0; // 初始化验证上下文 _editContext = new EditContext(FormModel); _validationStore = new ValidationMessageStore(_editContext); // 添加动态验证规则 _editContext.OnFieldChanged += (sender, e) => { _validationStore?.Clear(e.FieldIdentifier); // 验证FirstName必填 if (e.FieldIdentifier.FieldName == "FirstName") { if (string.IsNullOrWhiteSpace(FormModel["FirstName"]?.ToString())) { _validationStore?.Add(e.FieldIdentifier, "First name is required"); } } // 验证Age必须大于等于18 if (e.FieldIdentifier.FieldName == "Age") { if ((int)FormModel["Age"] < 18) { _validationStore?.Add(e.FieldIdentifier, "Age must be at least 18"); } } _editContext?.NotifyValidationStateChanged(); }; }
对应的MudBlazor页面代码:
<EditForm EditContext="_editContext"> <DataAnnotationsValidator /> <MudTextField T="string" Label="First name" @bind-Value="FormModel["FirstName"]" Margin="Margin.Dense" /> <ValidationMessage For="@(() => FormModel["FirstName"])" /> <MudTextField T="int" Label="Age" @bind-Value="FormModel["Age"]" Margin="Margin.Dense" /> <ValidationMessage For="@(() => FormModel["Age"])" /> <MudButton ButtonType="ButtonType.Submit" Variant="Variant.Filled" Color="Color.Primary" Class="mt-4">Submit</MudButton> </EditForm>
关键是给每个MudTextField指定T类型,让控件明确知道要处理的目标类型,避免转换错误。
方案2:自定义DynamicObject控制类型转换
如果更倾向于用动态属性语法(FormModel.FirstName),可以自定义一个DynamicObject子类,手动处理属性的获取和设置逻辑:
public class DynamicFormModel : DynamicObject { private readonly Dictionary<string, object> _properties = new(); public override bool TryGetMember(GetMemberBinder binder, out object? result) { return _properties.TryGetValue(binder.Name, out result); } public override bool TrySetMember(SetMemberBinder binder, object? value) { // 这里可以添加自定义类型转换逻辑 switch (binder.Name) { case "Age": if (value is string strVal && int.TryParse(strVal, out int intVal)) { _properties[binder.Name] = intVal; return true; } break; default: _properties[binder.Name] = value ?? string.Empty; return true; } return false; } }
使用这个自定义动态模型:
protected dynamic FormModel = new DynamicFormModel(); protected override void OnInitialized() { FormModel.FirstName = string.Empty; FormModel.LastName = string.Empty; FormModel.Age = 0; }
页面绑定代码:
<MudTextField T="string" Label="First name" @bind-Value="FormModel.FirstName" Margin="Margin.Dense" /> <MudTextField T="int" Label="Age" @bind-Value="FormModel.Age" Margin="Margin.Dense" />
这种方式既保留了动态属性的语法,又能通过重写TrySetMember控制类型转换,避免运行时错误。
内容的提问来源于stack exchange,提问作者Mohiyo Deen

