asp.net-coreblazor

Blazor dynamic form has field name issue


Consider the following code which is just a demo to show the problem I'm facing:

@page "/login"
@using System.ComponentModel.DataAnnotations

<EditForm Model="MyModel" FormName="LoginForm" OnValidSubmit="LoginUser">
    <DataAnnotationsValidator />

    <div>
        <InputText type="text" @bind-Value="MyModel.Username" />
        <ValidationMessage For="() => MyModel.Username" />
    </div>

    <div>
        <InputText type="password" @bind-Value="MyModel.Password" />
        <ValidationMessage For="() => MyModel.Password" />
    </div>

    <button type="submit">
        Login
    </button>

</EditForm>

@code {
    [SupplyParameterFromForm]
    public required LoginModel MyModel { get; set; }

    protected override void OnInitialized() => MyModel ??= new();

    void LoginUser() { }

    public class LoginModel
    {
        [Required]
        public string? Username { get; set; }

        [Required]
        public string? Password { get; set; }
    }
}

When I submit the form the form field names start with MyModel:

default form submission

Now I want to do the same with reflection so I can create a form builder component (for static SSR). Using the same demo code, consider this:

@page "/login2"

@using System.ComponentModel.DataAnnotations
@using System.Linq.Expressions
@using System.Reflection

<EditForm Model="MyModel" FormName="LoginForm" OnValidSubmit="LoginUser">
    <DataAnnotationsValidator />

    @foreach (var prop in typeof(LoginModel).GetProperties())
    {
        var inputType = prop.Name == "Username" ? "text" : "password";

        <div>
            <InputText type="@inputType"
                       Value="@(GetPropertyValue<string>(prop))"
                       ValueChanged="val => SetPropertyValue(prop, val)"
                       ValueExpression="GetPropertyExpression<string>(prop)" />

            <ValidationMessage For="GetPropertyExpression<string>(prop)" />
        </div>
    }

    <button type="submit">
        Login
    </button>

</EditForm>

@code {
    [SupplyParameterFromForm]
    public required LoginModel MyModel { get; set; }

    protected override void OnInitialized() => MyModel ??= new();

    void LoginUser() { }

    public class LoginModel
    {
        [Required]
        public string? Username { get; set; }

        [Required]
        public string? Password { get; set; }
    }


    T? GetPropertyValue<T>(PropertyInfo prop) => (T?)prop.GetValue(MyModel, null);

    void SetPropertyValue(PropertyInfo prop, object? value) => prop.SetValue(MyModel, value);

    Expression<Func<T>> GetPropertyExpression<T>(PropertyInfo prop)
    {
        var model = Expression.Constant(MyModel);
        var selector = Expression.Property(model, prop.Name);

        return Expression.Lambda<Func<T>>(selector, []);
    }
}

Now the form field names do not start with the MyModel and the framework populates the ValidationMessage with the default error of Required.

reflection form submission

How can I fix this naming issue?


Solution

  • Now the form field names do not start with the MyModel and the framework populates the ValidationMessage with the default error of Required.

    The issue relates the GetPropertyExpression<T>(PropertyInfo prop) method, it lacks a MemberExpression referring explicitly to MyModel. Try to modify it as below:

    Expression<Func<T>> GetPropertyExpression<T>(PropertyInfo prop)
    {
        var model = Expression.Constant(this);
        var property = Expression.Property(Expression.Property(model, nameof(MyModel)), prop.Name);
    
        return Expression.Lambda<Func<T>>(property);
    }
    

    After rendering, we can see the name already start with MyModel and the validation also work well:

    result

    The complete code is as below:

    @page "/login2"
    
    @using System.ComponentModel.DataAnnotations
    @using System.Linq.Expressions
    @using System.Reflection
    
    <EditForm Model="MyModel" FormName="LoginForm" OnValidSubmit="LoginUser">
        <DataAnnotationsValidator />
    
        @foreach (var prop in typeof(LoginModel).GetProperties())
        {
            var inputType = prop.Name == "Username" ? "text" : "password";
    
            <div>
                <InputText type="@inputType"
                           Value="@(GetPropertyValue<string>(prop))"
                           ValueChanged="val => SetPropertyValue(prop, val)"
                           ValueExpression="GetPropertyExpression<string>(prop)" />
    
                <ValidationMessage For="GetPropertyExpression<string>(prop)" />
            </div>
        }
    
        <button type="submit">
            Login
        </button>
    
    </EditForm>
    
    @code {
        [SupplyParameterFromForm]
        public required LoginModel MyModel { get; set; }
    
        protected override void OnInitialized() => MyModel ??= new();
    
        void LoginUser()
        {
            var newitem = MyModel;
        }
        public class LoginModel
        {
            [Required]
            public string? Username { get; set; }
    
            [Required]
            public string? Password { get; set; }
        }
    
    
        T? GetPropertyValue<T>(PropertyInfo prop) => (T?)prop.GetValue(MyModel, null);
    
        void SetPropertyValue(PropertyInfo prop, object? value) => prop.SetValue(MyModel, value);
    
        Expression<Func<T>> GetPropertyExpression<T>(PropertyInfo prop)
        {
            var model = Expression.Constant(this);
            var property = Expression.Property(Expression.Property(model, nameof(MyModel)), prop.Name);
    
            return Expression.Lambda<Func<T>>(property);
        }
    }