Creating dynamic forms with .net.core

I Bowyer picture I Bowyer · Jun 28, 2017 · Viewed 11.5k times · Source

I have a requirement to have different forms for different clients which can all be configured in the background (in the end in a database)

My initial idea is to create an object for "Form" which has a "Dictionary of FormItem" to describe the form fields.

I can then new up a dynamic form by doing the following (this would come from the database / service):

   private Form GetFormData()
    {
        var dict = new Dictionary<string, FormItem>();
        dict.Add("FirstName", new FormItem()
        {
            FieldType = Core.Web.FieldType.TextBox,
            FieldName = "FirstName",
            Label = "FieldFirstNameLabel",
            Value = "FName"
        });
        dict.Add("LastName", new FormItem()
        {
            FieldType = Core.Web.FieldType.TextBox,
            FieldName = "LastName",
            Label = "FieldLastNameLabel",
            Value = "LName"
        });
        dict.Add("Submit", new FormItem()
        {
            FieldType = Core.Web.FieldType.Submit,
            FieldName = "Submit",
            Label = null,
            Value = "Submit"
        });

        var form = new Form()
        {
            Method = "Post",
            Action = "Index",
            FormItems = dict
        };

        return form;
    }

Inside my Controller I can get the form data and pass that into the view

        public IActionResult Index()
    {
        var formSetup = GetFormData(); // This will call into the service and get the form and the values

        return View(formSetup);
    }

Inside the view I call out to a HtmlHelper for each of the FormItems

@model Form
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

@using FormsSpike.Core.Web
@{
    ViewData["Title"] = "Home Page";
}

@using (Html.BeginForm(Model.Action, "Home", FormMethod.Post))
{
    foreach (var item in Model.FormItems)
    {
        @Html.FieldFor(item);
    }
}

Then when posting back I have to loop through the form variables and match them up again. This feels very old school I would expect would be done in a model binder of some sort.

   [HttpPost]
    public IActionResult Index(IFormCollection form)
    {
        var formSetup = GetFormData();

        foreach (var formitem in form)
        {
            var submittedformItem = formitem;

            if (formSetup.FormItems.Any(w => w.Key == submittedformItem.Key))
            {
                FormItem formItemTemp = formSetup.FormItems.Single(w => w.Key == submittedformItem.Key).Value;
                formItemTemp.Value = submittedformItem.Value;
            }
        }
        return View("Index", formSetup);
    }

This I can then run through some mapping which would update the database in the background.

My problem is that this just feels wrong :o{

Also I have used a very simple HtmlHelper but I can't really use the standard htmlHelpers (such as LabelFor) to create the forms as there is no model to bind to..

 public static HtmlString FieldFor(this IHtmlHelper html, KeyValuePair<string, FormItem> item)
    {
        string stringformat = "";
        switch (item.Value.FieldType)
        {
            case FieldType.TextBox:
                stringformat = $"<div class='formItem'><label for='item.Key'>{item.Value.Label}</label><input type='text' id='{item.Key}' name='{item.Key}' value='{item.Value.Value}' /></ div >";
                break;
            case FieldType.Number:
                stringformat = $"<div class='formItem'><label for='item.Key'>{item.Value.Label}</label><input type='number' id='{item.Key}' name='{item.Key}' value='{item.Value.Value}' /></ div >";
                break;
            case FieldType.Submit:
                stringformat = $"<input type='submit' name='{item.Key}' value='{item.Value.Value}'>";
                break;
            default:
                break;
        }

        return new HtmlString(stringformat);
    }

Also the validation will not work as the attributes (for example RequiredAttribute for RegExAttribute) are not there.

Am I having the wrong approach to this or is there a more defined way to complete forms like this?

Is there a way to create a dynamic ViewModel which could be created from the origional setup and still keep all the MVC richness?

Answer

mcintyre321 picture mcintyre321 · Feb 8, 2018

You can do this using my FormFactory library.

By default it reflects against a view model to produce a PropertyVm[] array:

```

var vm = new MyFormViewModel
{
    OperatingSystem = "IOS",
    OperatingSystem_choices = new[]{"IOS", "Android",};
};
Html.PropertiesFor(vm).Render(Html);

```

but you can also create the properties programatically, so you could load settings from a database then create PropertyVm.

This is a snippet from a Linqpad script.

```

//import-package FormFactory
//import-package FormFactory.RazorGenerator


void Main()
{
    var properties = new[]{
        new PropertyVm(typeof(string), "username"){
            DisplayName = "Username",
            NotOptional = true,
        },
        new PropertyVm(typeof(string), "password"){
            DisplayName = "Password",
            NotOptional = true,
            GetCustomAttributes = () => new object[]{ new DataTypeAttribute(DataType.Password) }
        }
    };
    var html = FormFactory.RazorEngine.PropertyRenderExtension.Render(properties, new FormFactory.RazorEngine.RazorTemplateHtmlHelper());   

    Util.RawHtml(html.ToEncodedString()).Dump(); //Renders html for a username and password field.
}

```

Theres a demo site with examples of the various features you can set up (e.g. nested collections, autocomplete, datepickers etc.)