Creating dynamic forms with .net.core
Asked Answered
F

3

7

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?

Fishnet answered 28/6, 2017 at 13:56 Comment(6)
Did you ever find a solution to this?Supervene
@Bonner웃 If you look below there is a reply to this from mcintyre321. however I have not looked further into this problem as I have moved projects. Looking at the demo at aspdatatables.azurewebsites.net it seems to do something like I was wanting but my example was much more complex with perhaps 100s of fields.Fishnet
I didn't like the FormFactory solution as a model is being passed into a parser and it all feels very limited. I prefer the WebForms API over the FormFactory library, which is why I asked.Supervene
I have come across this issue as well. I know you have moved on from this project but just trying my luck here, have you or @BrownCow found a different solution?Yahiya
@Yahiya No I have moved on :)Fishnet
@BrownCow thanks anywaysYahiya
M
7

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

Macerate answered 8/2, 2018 at 9:46 Comment(0)
B
2

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>
Bluecollar answered 3/6, 2021 at 18:41 Comment(2)
It’s an old post of mine, does this work with validation included?Fishnet
I don't see why it wouldn't. I am not sure the razor tag. The code I got from a friend had validation with old mvc code.Bluecollar
L
1

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()
}
Luis answered 16/9, 2022 at 0:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.