【问题标题】:Blazor how to create dynamic formBlazor 如何创建动态表单
【发布时间】:2025-12-02 06:55:02
【问题描述】:

我正在尝试在 Blazor 中创建一个动态表单,但我被卡住了。我有一个带有两个输入参数的 FormFactory 组件。我的数据绑定模型和定义应创建哪些元素的字典。 这个想法是传递一个模型对象

public class Data 
{
    public string Name { get; set; }
    public string Phone { get; set; }
    public string Address { get; set; }
}

还有一本字典

{ "Name": "string" },
{ "Phone": "string" }

我想创建一个表单,其中包含绑定到模型中属性的姓名和电话两个输入字段。

<h3>Dynamic form</h3>
<EditForm Model="@DataContext">
    @foreach (var field in FieldIdentifiers)
    {
        if (field.Value == "string")
        {
            <InputText @bind-Value="DataContext.GetType().GetProperty(field.Key).GetValue(DataContext, null).ToString()"></InputText>
        }
    }    
</EditForm>    
@code {    
    [Parameter] public object DataContext { get; set; }
    [Parameter] public Dictionary<string, string> FieldIdentifiers { get; set; }
}

但这不起作用。我收到一个错误提示

"无法将 lambda 表达式转换为预期的委托类型,因为 块中的某些返回类型不可隐式转换 到委托返回类型”和“分配的左侧 必须是变量、属性或索引器"

编辑: 我发现 Blazor 会生成导致错误的 ValueChanged 属性

builder2.AddAttribute(8, "ValueChanged", Microsoft.AspNetCore.Components.RuntimeHelpers.TypeCheck<Microsoft.AspNetCore.Components.EventCallback<System.String>>(Microsoft.AspNetCore.Components.EventCallback.Factory.Create<System.String>(this, Microsoft.AspNetCore.Components.EventCallback.Factory.CreateInferred(this, __value => DataContext.GetType().GetProperty(field.Key).GetValue(DataContext, null).ToString() = __value, DataContext.GetType().GetProperty(field.Key).GetValue(DataContext, null).ToString()))));

那行不通,所以我尝试创建一个 RenderFragment 并按以下方式更改了我的组件

<h3>Dynamic form</h3>
@MyForm()


@code {

    [Parameter] public object DataContext { get; set; }

    [Parameter] public Dictionary<string, string> FieldIdentifiers { get; set; }

    RenderFragment MyForm() => builder =>
    {
        builder.AddMarkupContent(0, "<h3>Dynamic form</h3>\r\n");
        builder.OpenComponent<Microsoft.AspNetCore.Components.Forms.EditForm>(1);
        builder.AddAttribute(2, "Model", Microsoft.AspNetCore.Components.RuntimeHelpers.TypeCheck<System.Object>(DataContext));
        builder.AddAttribute(3, "ChildContent", (Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.Forms.EditContext>)((context) => builder2 =>
        {
            builder2.AddMarkupContent(4, "\r\n");
            foreach (var field in FieldIdentifiers)
            {
                if (field.Value == "string")
                {
                    builder2.AddContent(5, "            ");
                    builder2.OpenComponent<Microsoft.AspNetCore.Components.Forms.InputText>(6);
                    builder2.AddAttribute(7, "Value", Microsoft.AspNetCore.Components.RuntimeHelpers.TypeCheck<System.String>(Microsoft.AspNetCore.Components.BindMethods.GetValue(DataContext.GetType().GetProperty(field.Key).GetValue(DataContext, null).ToString())));
                    builder2.AddAttribute(8, "ValueChanged", Microsoft.AspNetCore.Components.RuntimeHelpers.TypeCheck<Microsoft.AspNetCore.Components.EventCallback<System.String>>(Microsoft.AspNetCore.Components.EventCallback.Factory.Create<System.String>(this, Microsoft.AspNetCore.Components.EventCallback.Factory.CreateInferred(this, __value => DataContext.GetType().GetProperty(field.Key).SetValue(DataContext, __value), DataContext.GetType().GetProperty(field.Key).GetValue(DataContext, null).ToString()))));
                    builder2.AddAttribute(9, "ValueExpression", Microsoft.AspNetCore.Components.RuntimeHelpers.TypeCheck<System.Linq.Expressions.Expression<System.Func<System.String>>>(() => Convert.ToString(DataContext.GetType().GetProperty(field.Key).GetValue(DataContext, null))));
                    builder2.CloseComponent();
                    builder2.AddMarkupContent(10, "\r\n");
                }
            }

            builder2.AddMarkupContent(11, "\r\n");
        }));
        builder.CloseComponent();
    };
}

这可以编译,但出现运行时错误

System.ArgumentException:提供的表达式包含一个 不受支持的 MethodCallExpression1。 FieldIdentifier 仅支持 > 对象的简单成员访问器(字段、属性)。

任何帮助将不胜感激

【问题讨论】:

    标签: blazor blazor-client-side


    【解决方案1】:

    有关工作示例,请参阅 BlazorFiddle

    您的问题在于ValueExpression,它要求它是一个表达式才能获取属性(或类似属性)。
    我在我的示例中创建了这个:

    var constant = System.Linq.Expressions.Expression.Constant(DataContext, typeof(DataX));
    var exp = System.Linq.Expressions.MemberExpression.Property(constant, fld);
    var lamb = System.Linq.Expressions.Expression.Lambda<Func<string>>(exp);
    builder.AddAttribute(4, "ValueExpression", lamb);
    

    对于ValueValueChanged,我使用反射来获取和设置属性值。

    // Get the initial property value
    var propInfoValue = typeof(DataX).GetProperty(fld);
    var s = propInfoValue.GetValue(DataContext);
    builder.AddAttribute(1, "Value", s);
    
    // Create the handler for ValueChanged. I use reflection to the value.
    builder.AddAttribute(3, "ValueChanged", Microsoft.AspNetCore.Components.RuntimeHelpers.TypeCheck<Microsoft.AspNetCore.Components.EventCallback<System.String>>(Microsoft.AspNetCore.Components.EventCallback.Factory.Create<System.String>(this, Microsoft.AspNetCore.Components.EventCallback.Factory.CreateInferred(this, __value => propInfoValue.SetValue(DataContext, __value), (string)propInfoValue.GetValue(DataContext)))));
    

    我对 Linq 的 Expression 类很陌生,所以可能有更好的方法来做到这一点。
    ValueChanged 的代码也是基于为静态 InputText 生成的代码。也可能有更好的方法。

    完整代码: Index.razor

    @page "/"
    
    <h3>Dynamic form</h3>
    <EditForm Model="@DataContext">
        @foreach (var field in FieldIdentifiers)
        {
            if (field.Value == "string")
            {
                @field.Key
                @CreateComponent(field.Key);
                <br />
            }
        }
    
        <h3>Statically declared</h3>
        <InputText @bind-Value="@DataContext.Name"></InputText> <br>
        Name: @DataContext.Name
    </EditForm>
    
    @code {
        [Parameter] public DataX DataContext { get; set; } = new DataX();
        [Parameter] public Dictionary<string, string> FieldIdentifiers { get; set; } = new Dictionary<string, string> { { "Name", "string" }, { "Phone", "string" } };
    
        public RenderFragment CreateComponent(string fld) => builder =>
        {
            builder.OpenComponent(0, typeof(InputText));
    
            // Get the initial property value
            var propInfoValue = typeof(DataX).GetProperty(fld);
            var s = propInfoValue.GetValue(DataContext);
            builder.AddAttribute(1, "Value", s);
    
            // Create the handler for ValueChanged. I use reflection to the value.
            builder.AddAttribute(3, "ValueChanged", Microsoft.AspNetCore.Components.RuntimeHelpers.TypeCheck<Microsoft.AspNetCore.Components.EventCallback<System.String>>(Microsoft.AspNetCore.Components.EventCallback.Factory.Create<System.String>(this, Microsoft.AspNetCore.Components.EventCallback.Factory.CreateInferred(this, __value => propInfoValue.SetValue(DataContext, __value), (string)propInfoValue.GetValue(DataContext)))));
    
            // Create an expression to set the ValueExpression-attribute.
            var constant = System.Linq.Expressions.Expression.Constant(DataContext, typeof(DataX));
            var exp = System.Linq.Expressions.MemberExpression.Property(constant, fld);
            var lamb = System.Linq.Expressions.Expression.Lambda<Func<string>>(exp);
            builder.AddAttribute(4, "ValueExpression", lamb);
    
            builder.CloseComponent();
        };
    }
    

    DataX.cs

    public class DataX
    {
        public string Name { get; set; } = "Jane Doe";
        public string Phone { get; set; } = "123456789";
        public string Address { get; set; } = "The Street";
    }
    

    编辑:我刚刚看到您的问题上有“blazor-clientside”标签。我刚刚使用服务器端 Blazor 对此进行了测试。

    【讨论】:

    • 是的,你是对的。这里需要通常由编译器设置的ValueExpression属性,告诉编译器如何产生值...非常感谢您的回答。
    • 我已经在客户端 blazor 上对其进行了测试,它按预期工作。谢谢!!
    • Fiddle 环境也已更新。所以原来的源代码已经不行了
    【解决方案2】:

    在研究时遇到了这个问题,但我最终只是跳过了@bind-value,而是直接使用valueonchanged 来通过反射设置属性:

        @foreach (var p in Datacontext.GetType().GetProperties())
        {
            <input type="text" 
                   placeholder="@p.Name" 
                   value="@p.GetValue(Datacontext)"
                   @onchange="(e) => p.SetValue(Datacontext, e.Value)">
        }
    

    这段代码不会对属性进行任何类型转换,只是为了说明使用 onchange 处理程序对属性对象使用闭包

    也许有人会觉得它有用:)

    【讨论】:

      【解决方案3】:

      尝试遍历模型对象(DataContext)的属性,并将其评估应为“DataContext.Phone”形式的表达式分配给@bind-Value指令

      public class Data
          {
              public string Name { get; set; }
              public string Phone { get; set; }
              public string Address { get; set; }
      
          }
      
      <h3>Dynamic form</h3>
      <EditForm Model="@DataContext">
        for (int i = 0; i < DataContext.GetType().GetProperties().Count(); i++ )
      {
          var name = "DataContext.";
          name += DataContext.GetType().GetProperties().ElementAt(i).Name;
         <InputText @bind-Value="@name"></InputText>
      }
      
      </EditForm>    
      @code {    
          [Parameter] public Data DataContext { get; set; }
      
      }
      

      希望这行得通...

      【讨论】:

      • 不,它不起作用。它与我尝试的解决方案有相同的问题。为 ValueChanged 生成的 Blazor 代码看起来像 __value => "DataContext"。 + DataContext.GetType().GetProperties().ElementAt(i).Name = __value 并导致错误
      • 好的,我明白了...表达式未计算为所需的结果。我已经更新了我的答案。让我们希望改变会奏效。如果使用“@name”不起作用,请尝试“@(@name)”祝你好运
      • 我们接近了.. 这段代码编译并运行,但输入的初始值为“DataContext.Text1”.. 我可以删除该文本并输入其他内容(如“No name”)但是当输入失去焦点时,我输入的文本被删除,值恢复为“DataContext.Text1”。变量名被评估为字符串“DataContext.Text1”,而不是对象DataContext的属性