I Bowyer
I Bowyer

Reputation: 833

Creating dynamic forms with .net.core

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?

Upvotes: 8

Views: 19854

Answers (3)

You can use JJMasterData, it can create dynamic forms from your tables at runtime or compile time. Supports both .NET 6 and .NET Framework 4.8.

  1. After setting up the package, access /en-us/DataDictionary in your browser
  2. Create a Data Dictionary adding your table name
  3. Click on More, Get Scripts, Execute Stored Procedures and then click on Preview and check it out
  4. To use your CRUD at runtime, go to en-us/MasterData/Form/Render/{YOUR_DICTIONARY}
  5. To use your CRUD at a specific page or customize at compile time, follow the example below:

At your Controller:

    public IActionResult Index(string dictionaryName)
    {
        var form = new JJFormView("YourDataDictionary");
        
        form.FormElement.Title = "Example of compile time customization"
        
        var runtimeField = new FormElementField();
        runtimeField.Label = "Field Label";
        runtimeField.Name = "FieldName";
        runtimeField.DataType = FieldType.Text;
        runtimeField.VisibleExpression = "exp:{pagestate}='INSERT'";
        runtimeField.Component = FormComponent.Text;
        runtimeField.DataBehavior = FieldBehavior.Virtual; //Virtual means the field does not exist in the database.
        runtimeField.CssClass = "col-sm-4";

        form.FormElement.Fields.Add(runtimeField);

        return View(form);
    }

At your View:

@using JJMasterData.Web.Extensions
@model JJFormView

@using (Html.BeginForm())
{
    @Model.GetHtmlString()
}

Upvotes: 1

Kevin Kohler
Kevin Kohler

Reputation: 373

I'm going to put my solution here since I found this searching 'how to create a dynamic form in mvc core.' I did not want to use a 3rd party library.

Model:

public class IndexViewModel
{
    public Dictionary<int, DetailTemplateItem> FormBody { get; set; }
    public string EmailAddress { get; set; }
    public string templateName { get; set; }
}

cshtml

<form asp-action="ProcessResultsDetails" asp-controller="home" method="post">
    <div class="form-group">
        <label [email protected] class="control-label"></label>
        <input [email protected] class="form-control" />
    </div>
    @foreach (var key in Model.FormBody.Keys)
    {
        <div class="form-group">

            <label asp-for="@Model.FormBody[key].Name" class="control-label">@Model.FormBody[key].Name</label>
            <input asp-for="@Model.FormBody[key].Value" class="form-control" value="@Model.FormBody[key].Value"/>
            <input type="hidden" asp-for="@Model.FormBody[key].Name"/>
        </div>
    }
    <input type="hidden" asp-for="templateName" />
    <div class="form-group">
        <input type="submit" value="Save" class="btn btn-primary" />
    </div>
</form>

Upvotes: 3

mcintyre321
mcintyre321

Reputation: 13306

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

Upvotes: 7

Related Questions