Blazor Form Generator

Blazor Form Generator

I have a need for a form generator that creates a MudBlazor based edit form, at runtime, using nothing more than a POCO model reference. I looked around and found a few open source projects, but none of them met my specific needs. So, I spent some time and created my own.

Here is how I created a completely dynamic Blazor and MudBlazor based form generator.

The source code for the NUGET package is available, for free, from HERE.

The NUGET package itself is available, for free, from HERE.


NOTE: since writing this article, I have gone back and redesigned the internals of the library. Good news is, those changes don’t affect the use of the library (other than I moved the MudBlazor attributes into a separate assembly). Don’t worry though, the article is still pertinent, and I’ll be writing about the new design in future articles


I started with a razor component that looks like this:

@namespace CG.Blazor.Forms
@typeparam T

<div class="dynamic-form-wrapper">
     @if (OnSubmit.HasDelegate)
    {
        <EditForm Model="@Model" OnSubmit="@OnSubmit">
            @GenerateFormBody()
            <MudButton ButtonType="ButtonType.Submit">Submit</MudButton>
        </EditForm>
    }
    else if (!OnValidSubmit.HasDelegate && OnInvalidSubmit.HasDelegate)
    {
        <EditForm Model="@Model" OnInvalidSubmit="@OnInvalidSubmit">
            @GenerateFormBody()
            <MudButton ButtonType="ButtonType.Submit">Submit</MudButton>
        </EditForm>
    }
    else if (OnValidSubmit.HasDelegate && !OnInvalidSubmit.HasDelegate)
    {
        <EditForm Model="@Model" OnValidSubmit="@OnValidSubmit">
            @GenerateFormBody()
            <MudButton ButtonType="ButtonType.Submit">Submit</MudButton>
        </EditForm>
    }
    else if (OnValidSubmit.HasDelegate && OnInvalidSubmit.HasDelegate)
    {
        <EditForm Model="@Model" OnInvalidSubmit="@OnInvalidSubmit" OnValidSubmit="@OnValidSubmit">
            @GenerateFormBody()
            <MudButton ButtonType="ButtonType.Submit">Submit</MudButton>
        </EditForm>
    }
    else
    {
        <EditForm Model="@Model">
            @GenerateFormBody()
            <MudButton ButtonType="ButtonType.Submit">Submit</MudButton>
        </EditForm>
    }
</div>

As we can see from the code snippet, the component contains a single EditForm, with callbacks wired up to various combinations of OnSubmit, OnValidSubmit, and OnInvalidSubmit handlers. This may seem like overkill, but, I figure this way, I can decide which handlers to wire up and which ones not to, as my project needs change.

Here is the code-behind for the component:

public partial class DynamicForm<T>
{

    [Parameter]
    public T Model { get; set; }

    [Parameter]
    public EventCallback<EditContext> OnValidSubmit { get; set; }

    [Parameter]
    public EventCallback<EditContext> OnSubmit { get; set; }

    [Parameter]
    public EventCallback<EditContext> OnInvalidSubmit { get; set; }

    [Inject]
    private IFormGenerator FormGenerator { get; set; }

    private RenderFragment GenerateFormBody() => builder => FormGenerator.Generate(
        builder,
        (IHandleEvent)this,
        Model
        );
}

As shown, the component has parameters for the event callbacks. It also has a parameter for the model reference.

There is also an IFormGenerator object injected into the project, by the Blazor DI container.

If we look at the razor code, we see that there is a reference to the GenerateFormBody method. That method is implemented here, in the code-behind. Looking at the code, we see that its actually a delegate that ultimately calls the IFormGenerator.Generate method. That method is defined on the IFormGenerator interface. We’ll look at that shortly.

Using the DynamicForm component is pretty easy. Here’s a quick demo:

@page "/"

<DynamicForm Model="@Model" OnValidSubmit="OnValidSubmit"/>

@code {
   MyModel Model {get; set; } 
   
   void OnValidSubmit(EditContext context)
   {
      // TODO : process the now populated model.
   }
}

This example uses a model of type: MyModel. That type is hypothetical, but we could define it like so:

public class MyModel
{
    [RenderMudTextField]
    public string FirstName { get; set; }

    [RenderMudTextField]
    public string LastName { get; set; }

    [RenderMudDatePicker]
    public DateTime? DateOfBirth { get; set; }
}

All of that preparation creates a form, at runtime, that looks similar to this:

The form is automatically bound to the properties on the MyModel model. The OnValidSubmit handler is also bound, and will be called whenever someone clicks the Submit button.

Looking back at the code for MyModel, we see that the properties are all decorated with custom attributes, namely, RenderMudTextField and RenderMudDatePicker attributes. Those attributes tell the form generator how to render fields for the properties.

Any non-object properties that aren’t decorated with an attribute are ignored. So, for instance, on the MyModel code snippet, if we removed the attribute from the FirstName property, the form would no longer contain a field for that property. Make sense?

The DynamicForm component, along with the various attributes and your POCO type, are the only things you’ll probably ever have to interact with, in order to generate forms. That’s deliberate, on my part, because I wanted the entire process to be as simple as I could make it.

Everything beyond this point is internal to the library and probably only of interest to someone wanting to create their own form generator, or someone wanting to understand how the current form generator works.

Earlier, I said I would cover the IFormGenerator type. Let’s do that next. Here is the definition for that interface:

public interface IFormGenerator
{
    void Generate(
        RenderTreeBuilder builder,
        IHandleEvent eventTarget,
        object model
        );
}

We see a single method: Generate, that accepts a RenderTreeBuilder object, an IHandleEvent reference, and a model reference. This is how the body of the edit form is generated, at runtime.

For my purposes, I really only need MudBlazor based forms. However, I can see the possibility of adding code to generate forms for other UI packages, such as Infragistics, or Telerik, or even Syncfusion. Towards that end, I put all of my generation logic inside a class I named MudBlazorFormGenerator. Later, if I need to, I can add generators for other types and not have to mess with my MudBlazor generator.

The code for my MudBlazorFormGenerator class is shown here:

internal class MudBlazorFormGenerator : IFormGenerator
{
    public virtual void Generate(
        RenderTreeBuilder builder,
        IHandleEvent eventTarget,
        object model
        )
    {
        // Validate the parameters before attempting to use them.
        Guard.Instance().ThrowIfNull(builder, nameof(builder))
            .ThrowIfNull(eventTarget, nameof(eventTarget))
            .ThrowIfNull(model, nameof(model));

        var index = 0;
        try
        {
            // Get the type of the model.
            var modelType = model?.GetType();

            // Should we render a data validator?
            var validatorAttr = modelType?.GetCustomAttribute<DataAnnotationsValidatorAttribute>();
            if (null != validatorAttr)
            {
                // Render the tag in the form.
                builder.RenderDataAnnotationsValidator(index++);
            }

            // Render the model.
            index = RenderProperties(
                builder,
                index,
                eventTarget,
                model,
                null // <-- null to render the model itself.
                );
        }
        catch (Exception ex)
        {
            // Give the error more context.
            throw new FormGenerationException(
                message: $"Failed to render the '{model?.GetType().Name}' " +
                    $"model instance. See inner exception(s) for more detail.",
                innerException: ex
                );
        }
    }

    private int RenderProperties(
        RenderTreeBuilder builder,
        int index,
        IHandleEvent eventTarget,
        object model,
        PropertyInfo modelProp
        ) 
    {
        // Get the child properties on the model.
        var childProps = (null == modelProp)
            ? model?.GetType().GetProperties().Where(x => x.CanWrite && x.CanRead)
            : modelProp.PropertyType.GetProperties().Where(x => x.CanWrite && x.CanRead);

        // Did we find any properties?
        if (null != childProps)
        {
            // Loop through the child properties.
            foreach (var childProp in childProps)
            {
                // TODO : deal with collection types, at some point.

                // Is the property a 'primitive' type?
                if (childProp.PropertyType.IsPrimitive ||
                    childProp.PropertyType == typeof(string) ||
                    childProp.PropertyType == typeof(decimal) ||
                    childProp.PropertyType == typeof(DateTime) ||
                    childProp.PropertyType == typeof(Nullable<DateTime>) ||
                    childProp.PropertyType == typeof(TimeSpan) ||
                    childProp.PropertyType == typeof(Nullable<TimeSpan>) ||
                    childProp.PropertyType == typeof(MudColor))
                {
                    // Render the property as a 'primitive'.
                    index = RenderPrimitives(
                        builder,
                        index++,
                        eventTarget,
                        model,
                        childProp
                        );
                }

                // Is the property an object type?
                else if (childProp.PropertyType.IsClass)
                {
                    // Get the value of the property - we'll need
                    //  this value in place of the model reference,
                    //  when we start rendering any child properties.
                    var propValue = childProp.GetValue(model);

                    // Anything to render?
                    if (null != propValue)
                    {
                        // Render the properties on the object.
                        index = RenderProperties(
                            builder,
                            index++,
                            eventTarget,
                            propValue, // <-- in place of the model reference.
                            childProp
                            );
                    }
                }

                // Is the property an unknown type?
                else
                {
                    // Panic!!
                    throw new FormGenerationException(
                        message: $"Failed to render property: '{childProp.Name}', " +
                            $"of type: '{childProp.PropertyType.Name}'. The form " +
                            $"generator doesn't have logic to deal with this type."
                        );
                }
            }
        }

        // Return the index.
        return index;
    }

    private static int RenderPrimitives(
        RenderTreeBuilder builder,
        int index,
        IHandleEvent eventTarget,
        object model,
        PropertyInfo modelProp
        )
    {
        // Is the property of type string?
        if (modelProp.PropertyType == typeof(string))
        {
            // Render the string property.
            index = RenderString(
                builder,
                index,
                eventTarget,
                model,
                modelProp
                );
        }

        // Is the property of type numeric?
        else if (modelProp.PropertyType == typeof(int) ||
                    modelProp.PropertyType == typeof(long) ||
                    modelProp.PropertyType == typeof(decimal) ||
                    modelProp.PropertyType == typeof(float) ||
                    modelProp.PropertyType == typeof(double) ||
                    modelProp.PropertyType == typeof(byte))
        {
            // Render the numeric property.
            index = RenderNumeric(
                builder,
                index,
                eventTarget,
                model,
                modelProp
                );
        }

        // Is the property of type bool?
        else if (modelProp.PropertyType == typeof(bool))
        {
            // Render the boolean property.
            index = RenderBool(
                builder,
                index,
                eventTarget,
                model,
                modelProp
                );
        }

        // Is the property of type datetime?
        else if (modelProp.PropertyType == typeof(DateTime) ||
                    modelProp.PropertyType == typeof(Nullable<DateTime>))
        {
            // Render the datetime property.
            index = RenderDateTime(
                builder,
                index,
                eventTarget,
                model,
                modelProp
                );
        }

        // Is the property of type timespan?
        else if (modelProp.PropertyType == typeof(TimeSpan) ||
                    modelProp.PropertyType == typeof(Nullable<TimeSpan>))
        {
            // Render the timespan property.
            index = RenderTimeSpan(
                builder,
                index,
                eventTarget,
                model,
                modelProp
                );
        }

        // Is the property of type MudColor?
        else if (modelProp.PropertyType == typeof(MudColor))
        {
            // Render the mudcolor property.
            index = RenderColor(
                builder,
                index,
                eventTarget,
                model,
                modelProp
                );
        }

        // Return the index.
        return index;
    }

    private static int RenderBool(
        RenderTreeBuilder builder,
        int index,
        IHandleEvent eventTarget,
        object model,
        PropertyInfo modelProp
        )
    {
        // Should we render the property as a MudCheckBox?
        var mudCheckBoxAttr = modelProp.GetCustomAttribute<RenderMudCheckBoxAttribute>();
        if (null != mudCheckBoxAttr)
        {
            // Render the control with bindings to the property.
            index = builder.RenderMudCheckBox<bool>(
                index,
                mudCheckBoxAttr,
                model,
                modelProp,
                eventTarget
                );
        }

        // Should we render the property as a MudSwitch?
        var mudSwitchAttr = modelProp.GetCustomAttribute<RenderMudSwitchAttribute>();
        if (null != mudSwitchAttr)
        {
            // Render the control with bindings to the property.
            index = builder.RenderMudSwitch<bool>(
                index,
                mudSwitchAttr,
                model,
                modelProp,
                eventTarget
                );
        }

        // Return the index.
        return index;
    }

    private static int RenderNumeric(
        RenderTreeBuilder builder,
        int index,
        IHandleEvent eventTarget,
        object model,
        PropertyInfo modelProp
        )
    {
        // Should we render a numeric?
        var mudNumericFieldAttr = modelProp.GetCustomAttribute<RenderMudNumericFieldAttribute>();
        if (null != mudNumericFieldAttr)
        {
            // Is the property an integer?
            if (modelProp.PropertyType == typeof(int))
            {
                // Render the control with bindings to the property.
                index = builder.RenderMudNumericField<int>(
                    index,
                    mudNumericFieldAttr,
                    model,
                    modelProp,
                    eventTarget
                    );
            }

            // Is the property a long integer?
            else if (modelProp.PropertyType == typeof(long))
            {
                // Render the control with bindings to the property.
                index = builder.RenderMudNumericField<long>(
                    index,
                    mudNumericFieldAttr,
                    model,
                    modelProp,
                    eventTarget
                    );
            }

            // Is the property a decimal?
            else if (modelProp.PropertyType == typeof(decimal))
            {
                // Render the control with bindings to the property.
                index = builder.RenderMudNumericField<decimal>(
                    index,
                    mudNumericFieldAttr,
                    model,
                    modelProp,
                    eventTarget
                    );
            }

            // Is the property a float?
            else if (modelProp.PropertyType == typeof(float))
            {
                // Render the control with bindings to the property.
                index = builder.RenderMudNumericField<float>(
                    index,
                    mudNumericFieldAttr,
                    model,
                    modelProp,
                    eventTarget
                    );
            }

            // Is the property a double?
            else if (modelProp.PropertyType == typeof(double))
            {
                // Render the control with bindings to the property.
                index = builder.RenderMudNumericField<double>(
                    index,
                    mudNumericFieldAttr,
                    model,
                    modelProp,
                    eventTarget
                    );
            }

            // Is the property a byte?
            else if (modelProp.PropertyType == typeof(byte))
            {
                // Render the control with bindings to the property.
                index = builder.RenderMudNumericField<byte>(
                    index,
                    mudNumericFieldAttr,
                    model,
                    modelProp,
                    eventTarget
                    );
            }
        }

        // Return the index.
        return index;
    }

    private static int RenderString(
        RenderTreeBuilder builder,
        int index,
        IHandleEvent eventTarget,
        object model,
        PropertyInfo modelProp
        )
    {
        // Should we render the property inside a text field?
        var mudTextFieldAttr = modelProp.GetCustomAttribute<RenderMudTextFieldAttribute>();
        if (null != mudTextFieldAttr)
        {
            // Render the control with bindings to the property.
            index = builder.RenderMudTextField<string>(
                index,
                mudTextFieldAttr,
                model,
                modelProp,
                eventTarget
                );
        }

        // Should we render the property inside an auto complete field?
        var mudAutocompleteAttr = modelProp.GetCustomAttribute<
            RenderMudAutocompleteAttribute
            >();
        if (null != mudAutocompleteAttr)
        {
            // Render the control with bindings to the property.
            index = builder.RenderMudAutocomplete<string>(
                index,
                mudAutocompleteAttr,
                model,
                modelProp,
                eventTarget
                );
        }

        // Should we render the property inside a radio group?
        var mudRadioGroupAttr = modelProp.GetCustomAttribute<RenderMudRadioGroupAttribute>();
        if (null != mudRadioGroupAttr)
        {
            // Render the control with bindings to the property.
            index = builder.RenderMudRadioGroup<string>(
                index,
                mudRadioGroupAttr,
                model,
                modelProp,
                eventTarget
                );
        }
            
        // Should we render the property inside an alert?
        var mudAlertAttr = modelProp.GetCustomAttribute<RenderMudAlertAttribute>();
        if (null != mudAlertAttr)
        {
            // Render the control with bindings to the property.
            index = builder.RenderMudAlert(
                index,
                mudAlertAttr,
                model,
                modelProp,
                eventTarget
                );
        }
                        
        // Return the index.
        return index;
    }

    private static int RenderDateTime(
        RenderTreeBuilder builder,
        int index,
        IHandleEvent eventTarget,
        object model,
        PropertyInfo modelProp
        )
    {
        // Should we render the property inside a date picker?
        var mudDatePickerAttr = modelProp.GetCustomAttribute<RenderMudDatePickerAttribute>();
        if (null != mudDatePickerAttr)
        {
            // Render the control with bindings to the property.
            index = builder.RenderMudDatePicker(
                index,
                mudDatePickerAttr,
                model,
                modelProp,
                eventTarget
                );
        }

        // Return the index.
        return index;
    }

    private static int RenderTimeSpan(
        RenderTreeBuilder builder,
        int index,
        IHandleEvent eventTarget,
        object model,
        PropertyInfo modelProp
        )
    {
        // Should we render the property inside a time picker?
        var mudTimePickerAttr = modelProp.GetCustomAttribute<RenderMudTimePickerAttribute>();
        if (null != mudTimePickerAttr)
        {
            // Render the control with bindings to the property.
            index = builder.RenderMudTimePicker(
                index,
                mudTimePickerAttr,
                model,
                modelProp,
                eventTarget
                );
        }

        // Return the index.
        return index;
    }

    private static int RenderColor(
        RenderTreeBuilder builder,
        int index,
        IHandleEvent eventTarget,
        object model,
        PropertyInfo modelProp
        )
    {
        // Should we render the property inside a color picker?
        var mudColorPickerAttr = modelProp.GetCustomAttribute<RenderMudColorPickerAttribute>();
        if (null != mudColorPickerAttr)
        {
            // Render the control with bindings to the property.
            index = builder.RenderMudColorPicker(
                index,
                mudColorPickerAttr,
                model,
                modelProp,
                eventTarget
                );
        }

        // Return the index.
        return index;
    }
}

Starting at the top of the code listing, with the Generate method, the first thing we do is get the model’s type, so we can check to see if the model’s class was decorated with a DataAnnotationsValidator attribute. If so, we then render a <DataAnnotationsValidator /> tag inside the edit form. That way, the form can validate itself, as part of the submission process. Next thing the method does is call the RenderProperties method, passing in the model reference we received as a parameter to the Generate method.

The RenderProperties method first checks for a collection of child properties on whatever object was passed as the model parameter. If any properties are found, we then iterate through them and perform some type checking to decide how best to render each property. Assuming the property type is primitive, we then call RenderPrimitives. Otherwise, assuming the property type is an object, we call RenderProperties again. That little bit of recursion allows us to walk down a complicated model type, rendering all the decorated properties we find, no matter how many levels the object tree goes.

The RenderPrimitives method does some further type checking and classifies the possible property types into ‘string’, ‘numeric’, ‘bool’, ‘datetime’, ‘timespan’ and ‘color’ types. That, in turn, causes us to call one of the methods reserved for that class of property: RenderString, RenderNumeric, RenderBool, RenderDateTime, RenderTimeSpan, and RenderColor.

Each of those methods renders a class of property using any of the controls that make sense for that type. For instance, RenderBool is capable of rendering a bool property as either a check box or a switch. On the other hand, RenderNumeric is capable of rendering any numeric type as a numeric text field. RenderDateTime currently only renders DateTime properties as a date picker field. Make sense?

I won’t cover all of these low-level methods, since they are mostly the same with the exception of the type of field they generate, and little details like the specific events they wire up. I will cover one of them though, and then we’ll walk down into the code that actually produces a MudBlazor field, for the property.

The RenderDateTime method first checks the incoming property to see if it’s decorated with a RenderMudDatePicker attribute. Assuming it is, it then calls an extension method on the RenderTreeBuilder, called RenderMudDatePicker, passing in the attribute used to decorate the property with, along with all the other arguments we got from RenderDateTime.

The code inside RenderMudDatePicker is solely responsible for rendering a MudDatePicker field into the specified RenderTreeBuilder object. The code for that method looks like this:

public static int RenderMudDatePicker(
    this RenderTreeBuilder builder,
    int index,
    RenderMudDatePickerAttribute attribute,
    object model,
    PropertyInfo prop,
    IHandleEvent eventTarget
    ) 
{
    // Validate the parameters before attempting to use them.
    Guard.Instance().ThrowIfNull(builder, nameof(builder))
        .ThrowIfLessThanZero(index, nameof(index))
        .ThrowIfNull(attribute, nameof(attribute))
        .ThrowIfNull(model, nameof(model))
        .ThrowIfNull(prop, nameof(prop))
        .ThrowIfNull(eventTarget, nameof(eventTarget));

    // Get any non-default attribute values (overrides).
    var attributes = attribute.ToAttributes();

    // Did we not override the label?
    if (false == attributes.ContainsKey("Label"))
    {
        // Ensure we have a label.
        attributes["Label"] = prop.Name;
    }

    // Are we binding to a datetime?
    if (prop.PropertyType == typeof(DateTime) ||
        prop.PropertyType == typeof(Nullable<DateTime>))
    {
        // Ensure the property value is set.
        attributes["Date"] = (DateTime?)prop.GetValue(model);

        // Ensure the property is bound, both ways.
        attributes["DateChanged"] = RuntimeHelpers.TypeCheck<EventCallback<DateTime?>>(
            EventCallback.Factory.Create<DateTime?>(
                eventTarget,
                EventCallback.Factory.CreateInferred<DateTime?>(
                    eventTarget,
                    x => prop.SetValue(model, x),
                    (DateTime?)prop.GetValue(model)
                    )
                )
            );
    }

    // Is the type unknown?
    else
    {
        // Panic!!
        throw new FormGenerationException(
            message: $"Failed to bind a MudTimePicker to property: " +
                $"'{prop.Name}', of type '{prop.PropertyType.Name}'"
            );
    }

    // Render the property as a MudDatePicker control.
    index = builder.RenderUIComponent<MudDatePicker>(
        index++,
        attributes: attributes
        );

    // Return the index.
    return index;
}

I created this extension method, and the others like it, to isolate the actual field rendering logic from the code in the MudBlazorFormGenerator class, that simply walks through a model instance, using reflection, and decides what properties need to be rendered, at runtime. This way, my generator class can focus on what it does best, and I can keep all the rendering logic out of the way. Before I did that, my MudBlazorFormGenerator class was scary complicated, when it didn’t need to be. It’s much cleaner now.

Back to the RenderMudDatePicker method. The first thing the method does is call ToAttributes on the attribute we passed in, from the form generator. I’ll cover the ToAttributes method in detail when I cover the internals of the attribute types. For now, just know that this is how we’ll know what attributes to add to the field, when we render it.

Most attributes for MudBlazor fields are completely optional. A few though, really are necessary. For instance, for the MudDatePicker field, the Label attribute is required, one way or the other. If the Label was specified at the attribute level then we leave it alone and pass it on to the rendering code. On the other hand, if it wasn’t specified then we need to come up with something in its place. For that purpose, we use the property name as a fallback label.

The only other thing to do, in this method, is to handle the property binding and, of course, render the field itself. Property binding happens by pulling the value of the property from the model, then plugging that value into the Date property, on the MudDatePicker field. The DateChanged attribute requires us to generate a callback that will, at runtime, sync the model and the field together whenever anything changes. For that, we use the RuntimeHelpers class, and some rather involved looking code that generates a callback, which we then plugin into the DateChanged attribute.

In case anyone is interested, I figured out how to generate the callback for the DateChanged event by placing a MudDatePicker field on a WebAssembly razor page, binding the field to a property on the code-behind, compiling the project, then looking at the compiled assembly using a dotnet decompiler. Yup, otherwise I’d still be trying to figure that out … Sheesh!

Finally, to render the MudDatePicker field itself, I call the RenderUIComponent extension method. The code for that method is shown here:

public static int RenderUIComponent<T>(
    this RenderTreeBuilder builder,
    int index,
    Action<RenderTreeBuilder> contentDelegate = null,
    IDictionary<string, object> attributes = null
    ) where T : class, IComponent
{
    // Validate the parameters before attempting to use them.
    Guard.Instance().ThrowIfNull(builder, nameof(builder))
        .ThrowIfLessThanZero(index, nameof(index));

    // Open the HTML tag.
    builder.OpenComponent<T>(index++);

    // Are any attributes specified?
    if (null != attributes)
    {
        // Loop through the properties.
        foreach (var prop in attributes)
        {
            // Add the standard attribute.
            builder.AddAttribute(
                index++,
                prop.Key,
                prop.Value
                );
        }
    }

    // Should we render child content?
    if (null != contentDelegate)
    {
        // Render the child content
        builder.AddAttribute(
            index++,
            "ChildContent",
            new RenderFragment(contentBuilder =>
                contentDelegate(contentBuilder)
                )
            );
    }

    // Close the HTML tag.
    builder.CloseComponent();

    // Make the HTML purdy.
    builder.AddMarkupContent(index++, "\r\n    ");

    // Return the index.
    return index;
}

By now, we’ve moved down into the guts of Blazor rendering logic. This code, in the RenderUIComponent method, is ugly, but necessary. It’s code that Blazor uses to generate HTML and razor components, at runtime. In fact, this is exactly the same code that Blazor compiles to, when you create a normal Blazor page, in Visual Studio, and then compile it. Blazor sort of hides all this complexity from you, but, it’s still there.

In our case, we called RenderUIComponent using MudDatePicker as a type argument, since we want to create a date picker field. The method starts by calling OpenComponent on the RenderTreeBuilder object, passing in our MudDatePicker type as T. That directs Blazor to start the opening tag for our field. Next, we check to see if there are any attributes we need to add to the fields opening tag. If so, we loop and and those now.

If the control has child content, we’ll deal with that by adding a ChildContent attribute, and passing in the delegate that produces that content. In our case, for this example, our date picker has no such content, so we don’t mess with that.

Finally, we close the date picker field’s tag and add a control feed, carriage return, to keep the HTML looking all pretty, and stuff.

At that point, we’re done rendering the date picker field for the DateTime property on the model.

The only thing I haven’t yet covered are the attribute types that we use to decorate our model properties with. Let’s start that by looking at the base type I created, for those kind of attributes:

public abstract class FormGeneratorAttribute : Attribute
{
    public virtual IDictionary<string, object> ToAttributes()
    {
        return new Dictionary<string, object>();
    }
}

The FormGeneratorAttribute type derives from Attribute, so it is an attribute type that we can use to decorate C# code with. For my purpose, I added a public virtual method called ToAttributes . That method is important so I’ll cover it now.

Remember when we were inside the RenderDateTimePicker extension method, and the first thing it did was call ToAttributes on the attribute? Well, that’s this ToAttributes method. By itself, we can see that it really does nothing more than return an empty dictionary. That’s because most razor components don’t require any attributes so we start by assuming that they’ll require none. Of course, in situations where that’s not a valid assumption, we can add attribute values to the dictionary. We do that by deriving from the FormGeneratorAttribute type, like this:

[AttributeUsage(AttributeTargets.Property)]
public class RenderMudDatePickerAttribute : FormGeneratorAttribute
{
    public Adornment Adornment { get; set; }
    public Color AdornmentColor { get; set; }
    public string AdornmentIcon { get; set; }
    public bool AllowKeyboardInput { get; set; }
    public bool AutoClose { get; set; }
    public string Class { get; set; }
    public string ClassActions { get; set; }
    public int ClosingDelay { get; set; }
    public Color Color { get; set; }
    public string DateFormat { get; set; }
    public bool Disabled { get; set; }
    public bool DisableToolbar { get; set; }
    public int DisplayMonths { get; set; }
    public bool Editable { get; set; }
    public int Elevation { get; set; }
    public DayOfWeek? FirstDayOfWeek { get; set; }
    public Size IconSize { get; set; }
    public string Label { get; set; }
    public Margin Margin { get; set; }
    public DateTime? MaxDate { get; set; }
    public int? MaxMonthColumns { get; set; }
    public DateTime? MinDate { get; set; }
    public OpenTo OpenTo { get; set; }
    public Orientation Orientation { get; set; }
    public PickerVariant PickerVariant { get; set; }
    public bool ReadOnly { get; set; }
    public bool Required { get; set; }
    public bool Rounded { get; set; }
    public bool ShowWeekNumbers { get; set; }
    public bool Square { get; set; }
    public DateTime? StartMonth { get; set; }
    public string Style { get; set; }
    public object Tag { get; set; }
    public string TitleDateFormat { get; set; }
    public string ToolBarClass { get; set; }
    public IDictionary<string, object> UserAttributes { get; set; }
    public Variant Variant { get; set; }

    public RenderMudDatePickerAttribute()
    {
        // Set default values.
        Adornment = Adornment.End;
        AdornmentColor = Color.Default;
        AdornmentIcon = string.Empty;
        AllowKeyboardInput = false;
        AutoClose = false;
        Class = string.Empty;
        ClassActions = string.Empty;
        ClosingDelay = 100;
        Color = Color.Primary;
        DateFormat = string.Empty;
        Disabled = false;
        DisableToolbar = false;
        DisplayMonths = 1;
        Editable = false;
        Elevation = 0;
        FirstDayOfWeek = null;
        IconSize = Size.Medium;
        Label = string.Empty;
        Margin = Margin.None;
        MaxDate = null;
        MaxMonthColumns = null;
        MinDate = null;
        OpenTo = OpenTo.Date;
        Orientation = Orientation.Portrait;
        PickerVariant = PickerVariant.Inline;
        ReadOnly = false;
        Required = false;
        Rounded = false;
        ShowWeekNumbers = false;
        Square = false;
        StartMonth = null;
        Style = string.Empty;
        Tag = null;
        TitleDateFormat = string.Empty;
        ToolBarClass = string.Empty;
        UserAttributes = null;
        Variant = Variant.Text;
    }

    public override IDictionary<string, object> ToAttributes()
    {
        var attr = new Dictionary<string, object>();

        if (Adornment.End != Adornment)
        {
            attr[nameof(Adornment)] = Adornment;
        }

        if (Color.Default != AdornmentColor)
        {
            attr[nameof(AdornmentColor)] = AdornmentColor;
        }

        if (false != string.IsNullOrEmpty(AdornmentIcon))
        {
            attr[nameof(AdornmentIcon)] = AdornmentIcon;
        }

        if (false != AllowKeyboardInput)
        {
            attr[nameof(AllowKeyboardInput)] = AllowKeyboardInput;
        }

        if (false != AutoClose)
        {
            attr[nameof(AutoClose)] = AutoClose;
        }

        if (false != string.IsNullOrEmpty(Class))
        {
            attr[nameof(Class)] = Class;
        }

        if (false != string.IsNullOrEmpty(ClassActions))
        {
            attr[nameof(ClassActions)] = ClassActions;
        }

        if (100 != ClosingDelay)
        {
            attr[nameof(ClosingDelay)] = ClosingDelay;
        }

        if (Color.Primary != Color)
        {
            attr[nameof(Color)] = Color;
        }

        if (false != string.IsNullOrEmpty(DateFormat))
        {
            attr[nameof(DateFormat)] = DateFormat;
        }

        if (false != Disabled)
        {
            attr[nameof(Disabled)] = Disabled;
        }

        if (false != DisableToolbar)
        {
            attr[nameof(DisableToolbar)] = DisableToolbar;
        }

        if (1 != DisplayMonths)
        {
            attr[nameof(DisplayMonths)] = DisplayMonths;
        }

        if (false != Editable)
        {
            attr[nameof(Editable)] = Editable;
        }

        if (0 != Elevation)
        {
            attr[nameof(Elevation)] = Elevation;
        }

        if (null != FirstDayOfWeek)
        {
            attr[nameof(FirstDayOfWeek)] = FirstDayOfWeek.Value;
        }

        if (Size.Medium != IconSize)
        {
            attr[nameof(IconSize)] = IconSize;
        }

        if (false == string.IsNullOrEmpty(Label))
        {
            attr[nameof(Label)] = Label;
        }

        if (Margin.None != Margin)
        {
            attr[nameof(Margin)] = Margin;
        }

        if (null != MaxDate)
        {
            attr[nameof(MaxDate)] = MaxDate;
        }

        if (null != MaxMonthColumns)
        {
            attr[nameof(MaxMonthColumns)] = MaxMonthColumns;
        }

        if (null != MinDate)
        {
            attr[nameof(MinDate)] = MinDate;
        }

        if (OpenTo.Date != OpenTo)
        {
            attr[nameof(OpenTo)] = OpenTo;
        }

        if (Orientation.Portrait != Orientation)
        {
            attr[nameof(Orientation)] = Orientation;
        }

        if (PickerVariant.Inline != PickerVariant)
        {
            attr[nameof(PickerVariant)] = PickerVariant;
        }

        if (false != ReadOnly)
        {
            attr[nameof(ReadOnly)] = ReadOnly;
        }

        if (false != Required)
        {
            attr[nameof(Required)] = Required;
        }

        if (false != Rounded)
        {
            attr[nameof(Rounded)] = Rounded;
        }

        if (false != ShowWeekNumbers)
        {
            attr[nameof(ShowWeekNumbers)] = ShowWeekNumbers;
        }

        if (false != Square)
        {
            attr[nameof(Square)] = Square;
        }

        if (null != StartMonth)
        {
            attr[nameof(StartMonth)] = StartMonth;
        }

        if (false != string.IsNullOrEmpty(Style))
        {
            attr[nameof(Style)] = Style;
        }

        if (null != Tag)
        {
            attr[nameof(Tag)] = Tag;
        }

        if (false == string.IsNullOrEmpty(TitleDateFormat))
        {
            attr[nameof(TitleDateFormat)] = TitleDateFormat;
        }

        if (false == string.IsNullOrEmpty(ToolBarClass))
        {
            attr[nameof(ToolBarClass)] = ToolBarClass;
        }

        if (null != UserAttributes)
        {
            attr[nameof(UserAttributes)] = UserAttributes;
        }

        if (Variant.Text != Variant)
        {
            attr[nameof(Variant)] = Variant;
        }

        return attr;
    }
}

As we can see, the MudBlazor MudDatePicker field has a rather large number of optional attributes. In our case though, we can’t set any of those attribute directly on the generated field, because, that field doesn’t exist at compile time! That means our attribute type, RenderMudDatePickerAttribute, has to be our stand in. Here is where we’ll supply any additional information the form generator needs, to generate the date picker control exactly how we want it.

To do that, we override the ToAttributes method, in the RenderMudDatePickerAttribute class, and we use it to add the name and value of any attributes that were set by the developer. That way, we only add additional noise to the resulting HTML when it’s absolutely needed.

So, let’s look at our MyModel example again, but this time we’ll add some additional attributes and see how it changes the look of the rendered form.

public class MyModel
{
    [RenderMudTextField(Lines = 4)]
    public string FirstName { get; set; }

    [RenderMudTextField(Placeholder = "enter something")]
    public string LastName { get; set; }

    [RenderMudDatePicker(Color = MudBlazor.Color.Secondary)]
    public DateTime? DateOfBirth { get; set; }
}

Now, when we run the form generator, we see this form:

We can see the FirstName field is 4 lines. We can also see the LastName field has a placeholder. Finally, we can see that the DateOfBirth field has a color associated with it. All those changes happened because we changed the properties on the attributes. That, in turn, changed the fields that were generated by passing additional attributes to the rendering logic, through the ToAttributes method of our custom attribute type.

Every attribute in the library has optional properties with the exception of the DataAnnotationsValidator attribute. That’s because the data annotations validator doesn’t accept any attributes, so, there’s nothing to copy.

Well that’s about it, for my little form generator. Look for many changes as the code is still super new. I have a variety of additional features I want to add to the library, as well. The MudBlazor library has a ridiculous number of controls that might be useful, one way or another, on an edit form. I’ll go through each one and decide whether, and how to include support in my form generator. Also, there’s still the possibility of integrating my form generator with another UI package.

In the meantime, thanks for reading and I hope you have fun with the library.

Photo by Alexander Schimmeck on Unsplash