NashTech Blog

Table of Contents

Modern enterprise Blazor apps often suffer from form sprawl: dozens of pages, nearly identical form logic, endless <MudTextField> and <InputText> clones…

  • In this blog, I will show a structure to help solving that problem:
  • A metadata-driven dynamic form engine in Blazor.
  • Forms defined by C# attributes + a single dynamic component.
  • Reusable, elegant, and production-ready.

Below is the full architecture with example code you can run today.


🎛️ Goal

For example, we want to define a model like this:

public class CustomerInfo
{
    [FormField("Full Name", InputType.Text, IsRequired = true)]
    public string FullName { get; set; }

    [FormField("Age", InputType.Number)]
    public int Age { get; set; }

    [FormField("Email Address", InputType.Email, IsRequired = true)]
    public string Email { get; set; }

    [FormField("Gender", InputType.Select, Options = new[] { "Male", "Female", "Other" })]
    public string Gender { get; set; }
}

…and have Blazor render the correct UI automatically.


Step 1 — The Custom Attribute

public enum InputType
{
    Text,
    Number,
    Email,
    Select
}

[AttributeUsage(AttributeTargets.Property)]
public class FormFieldAttribute : Attribute
{
    public string Label { get; }
    public InputType InputType { get; }

    public bool IsRequired { get; set; }
    public string[]? Options { get; set; }

    public FormFieldAttribute(string label, InputType type)
    {
        Label = label;
        InputType = type;
    }
}

Step 2 — A Dynamic Form Component

This component will:

  • read metadata via reflection
  • loop through properties
  • render the correct input
  • bind values into the model

DynamicForm.razor

@typeparam TModel

<EditForm Model="@Model">
    <DataAnnotationsValidator />
    <ValidationSummary />

    @foreach (var field in Fields)
    {
        <div class="mb-3">
            @RenderField(field)
        </div>
    }

    <button type="submit" class="btn btn-primary">Submit</button>
</EditForm>

@code {
    [Parameter] public TModel Model { get; set; }

    private List<FieldMetadata> Fields { get; set; } = new();

    protected override void OnInitialized()
    {
        Fields = typeof(TModel)
            .GetProperties()
            .Select(prop => new { 
                Prop = prop,
                Attr = prop.GetCustomAttribute<FormFieldAttribute>()
            })
            .Where(x => x.Attr != null)
            .Select(x => new FieldMetadata(x.Prop, x.Attr))
            .ToList();
    }

    private RenderFragment RenderField(FieldMetadata f) => builder =>
    {
        var seq = 0;
        var value = f.Property.GetValue(Model);

        builder.OpenElement(seq++, "label");
        builder.AddAttribute(seq++, "class", "form-label fw-bold");
        builder.AddContent(seq++, f.Attribute.Label);
        builder.CloseElement();

        switch (f.Attribute.InputType)
        {
            case InputType.Text:
            case InputType.Email:
                builder.OpenElement(seq++, "input");
                builder.AddAttribute(seq++, "class", "form-control");
                builder.AddAttribute(seq++, "type", f.Attribute.InputType.ToString().ToLower());
                builder.AddAttribute(seq++, "value", BindConverter.FormatValue(value));
                builder.AddAttribute(seq++, "onchange", EventCallback.Factory.CreateBinder(
                    this,
                    v => f.Property.SetValue(Model, v),
                    value));
                builder.CloseElement();
                break;

            case InputType.Number:
                builder.OpenElement(seq++, "input");
                builder.AddAttribute(seq++, "class", "form-control");
                builder.AddAttribute(seq++, "type", "number");
                builder.AddAttribute(seq++, "value", BindConverter.FormatValue(value));
                builder.AddAttribute(seq++, "onchange", EventCallback.Factory.CreateBinder(
                    this,
                    v => f.Property.SetValue(Model, Convert.ToInt32(v)),
                    value));
                builder.CloseElement();
                break;

            case InputType.Select:
                builder.OpenElement(seq++, "select");
                builder.AddAttribute(seq++, "class", "form-select");
                builder.AddAttribute(seq++, "value", BindConverter.FormatValue(value));
                builder.AddAttribute(seq++, "onchange", EventCallback.Factory.CreateBinder(
                    this,
                    v => f.Property.SetValue(Model, v),
                    value));

                foreach (var opt in f.Attribute.Options ?? Array.Empty<string>())
                {
                    builder.OpenElement(seq++, "option");
                    builder.AddAttribute(seq++, "value", opt);
                    builder.AddContent(seq++, opt);
                    builder.CloseElement();
                }

                builder.CloseElement();
                break;
        }
    };
}

FieldMetadata.cs

public class FieldMetadata
{
    public PropertyInfo Property { get; }
    public FormFieldAttribute Attribute { get; }

    public FieldMetadata(PropertyInfo prop, FormFieldAttribute attr)
    {
        Property = prop;
        Attribute = attr;
    }
}

Step 3 — Using the Dynamic Form

CustomerPage.razor

@page "/customer"

@code {
    private CustomerInfo Model = new();
}

<h3>Customer Registration</h3>

<DynamicForm TModel="CustomerInfo" Model="@Model" />

Boom. Entire form, no UI repetition, fully dynamic.



Bonus: MudBlazor Integration

Replace the internal switch with:

<MudTextField @bind-Value="@value" Label="@f.Attribute.Label" />

or:

<MudSelect T="string" @bind-Value="@value">
    @foreach (var opt in f.Attribute.Options)
    {
        <MudSelectItem T="string" Value="@opt">@opt</MudSelectItem>
    }
</MudSelect>

Suddenly you have a dynamic MudBlazor form generator.


Conclusion

This advanced blazor technique lets you:

  • define forms once
  • generate UI dynamically
  • eliminate repetitive Razor
  • create CMS-driven, metadata-driven, dynamic form engines

It’s a clean way to bring declarative UI magic into Blazor without sacrificing type-safety.

Picture of Hung Nguyen Minh

Hung Nguyen Minh

Leave a Comment

Your email address will not be published. Required fields are marked *

Suggested Article

Scroll to Top