Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Blazor how to create dynamic form

I'm trying to create a dynamic form in Blazor but I'm stuck. I have a FormFactory component that takes two input parameters. Model for my data binding and a dictionary that defines which elements should be created. The idea is to pass a model object

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

and a dictionary

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

I want to create a form with two input fields for Name and Phone bound to the properties in my model.

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

But this doesn't work. I get an error saying

"Cannot convert lambda expression to intended delegate type because some of the return types in the block are not implicitly convertible to the delegate return type" and "The left hnad side of an assignment must be a variable, property or indexer"

EDIT: I've found that Blazor generates a ValueChanged attribute that causes error

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()))));

That won't work so I tried creating a RenderFragment and changed my component as following

<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();
    };
}

This compiles but I get a runtime error

System.ArgumentException: The provided expression contains a MethodCallExpression1 which is not supported. FieldIdentifier only supports > simple member accessors (fields, properties) of an object.

Any help would be appreciated

like image 271
partyelite Avatar asked Aug 09 '19 08:08

partyelite


People also ask

How do I redirect to another page in Blazor?

You can redirect to a page in Blazor using the Navigation Manager's NavigateTo method. In the following code snippet, it will redirect to the home page when this page gets loaded. Similarly, you can call NavigateTo() method from NavigationManager class anywhere to redirect to another page.

How do you get URI in Blazor?

Inject NavigationManager in razor. Use Uri from NavigationManager to get the current URL.

How do I submit a form to Blazor?

You can submit a Blazor form programmatically by using EditContent validation. In the following example, a keypress event function triggers when you press the Enter key. It validates the form editContent. Validate() and submits the form programmatically.


1 Answers

See BlazorFiddle for working sample.

Your problem was with ValueExpression, that required it to be an expression to get a property (or similar).
I create this in my sample:

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

For Value and ValueChanged I use reflection to get and set the property values.

// 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)))));

I'm quite new working with Linq's Expression classes so there may be better ways to do this.
Also the code for ValueChanged is based on the generated code for a static InputText. There may be better ways for this as well.

Full code: 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";
}

Edit: I just saw that you had the "blazor-clientside" tag on the question. I have just tested this with server-side Blazor.

like image 193
Björn Avatar answered Sep 28 '22 00:09

Björn