6

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?

I Bowyer
  • 691
  • 1
  • 9
  • 21
  • Did you ever find a solution to this? – Professor of programming Mar 09 '18 at 16:15
  • @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 http://aspdatatables.azurewebsites.net/ it seems to do something like I was wanting but my example was much more complex with perhaps 100s of fields. – I Bowyer Mar 14 '18 at 08:24
  • 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. – Professor of programming Mar 14 '18 at 15:30
  • 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? – learner Jun 18 '18 at 16:25
  • @learner No I have moved on :) – I Bowyer Jul 04 '18 at 09:32
  • @BrownCow thanks anyways – learner Jul 04 '18 at 09:34

3 Answers3

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

mcintyre321
  • 12,996
  • 8
  • 66
  • 103
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 asp-for=@Model.EmailAddress class="control-label"></label>
        <input asp-for=@Model.EmailAddress 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>
Kevin Kohler
  • 364
  • 4
  • 21
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()
}