-1

I have been trying to find a solution to a situation that I'm busy designing, however I have not managed to get to it.

Imagine having the following model

public enum InputType
{
  TextInput,
  LookupInput
}
public struct AdditionalProperty
{
  public string Key {get;set;}
  public string Value {get;set;}
  public InputType Type {get;set;}
}
public class Person
{
  public string FirstName {get;set;}

  public List<AdditionalProperty> AdditionalProperties {get;set;}
}

Then, having the following controller

public class HomeController
{
  public ActionResult Index()
  {
    var model = new Person { FirstName = "MyName" };
    model.AdditionalProperties = new List<AdditionalProperty>();

    var listItem = new AdditionalProperty
    {
      Key = "Surname",
      Value = "MySurname"
    };
    model.AdditionalProperties.Add(listItem);
    return View(model)
  }
}

What I'm looking for is the Razor view code on how to "dynamically" create the properties with the correct input type, bound to something in order for me to be able to still use the model when the form gets posted back to the controller for a Save function.

So the property that is known, would be something like this:

<div class="form-group">
    <div class="form-row">
        <div class="col-md-6">
            @Html.LabelFor(model => model.FirstName, new { @class = "control-label" })
            <div>
                @Html.TextBoxFor(model => model.FirstName, new { @class = "form-control", placeholder = "Enter Group Name", type = "text" })
                @Html.ValidationMessageFor(model => model.FirstName, "", new { @class = "text-danger" })
            </div>
        </div>
    </div>
</div>

The Idea would then be to have the following. Obviously the below isn't sufficient, and this is where I need the help. I would like to show the additional properties, one below the other, each on a separate line (using bootstrap row) based on the property.InputType

@foreach (var property in Model.Properties)
{
  @Html.LabelFor(model => property.Key, new { @class = "control-label" })
  <div>
    @if (property.InputType == TextInput)
    {
      @Html.TextBoxFor(model => property.Value, new { @class = "form-control", placeholder = "Enter Group Name", type = "text" })
    }
    @Html.ValidationMessageFor(model => property.Key, "", new { @class = "text-danger" })
  </div>
}

Thus, I would like to see my view as:

                 | <label>     | <input>
Known Property   | FirstName   | MyFirstName
Unknown Property | Surname     | MySurname
Gawie Schneider
  • 1,078
  • 1
  • 9
  • 14
  • 1
    Yo cannot use a `foreach` loop to generate form controls for a collection (refer [Post an HTML Table to ADO.NET DataTable](https://stackoverflow.com/questions/30094047/post-an-html-table-to-ado-net-datatable/30094943#30094943). And your code to generate the ` –  Aug 15 '18 at 23:53
  • Stephen, so is it possible or not because you didn’t answer my question – Gawie Schneider Aug 16 '18 at 07:35
  • Yes, its possible (but using a `for` loop or custom `EditorTemplate` so the `name` attributes of the form controls are correctly generated - see the link in my first comment) –  Aug 16 '18 at 07:37
  • I am aware that I am supposed to use a for when working with the index. The concept I'm struggling with is to populate the correct InputType (Textbox, Combobox etc) for a certain item – Gawie Schneider Aug 16 '18 at 08:30
  • In order to make use of different editortemplates, I have to make use of different models for each property based om my type that I mentioned. – Gawie Schneider Aug 16 '18 at 08:40
  • No, An `EditorTemplate` in your case would be named `AdditionalProperty.cshtml` with `@model AdditionalProperty` and your `if/else` blocks (and in the main view you would call `@Html.EditorFor(m => m.AdditionalProperties)` to render that template for each item in the collection –  Aug 16 '18 at 08:42
  • I know I'm asking a lot, but would it be possible for you, should you have the time, to supply me with an example for the editor template and the view? – Gawie Schneider Aug 16 '18 at 10:36
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/178126/discussion-between-stephen-muecke-and-gawie-greef). –  Aug 16 '18 at 11:11

1 Answers1

0

In terms of completeness, I am posting the following answer.

I am going to post the Model, View (Index & EditorTemplates) & Controller to show the complete working solution that I used to test the answer that was given to me.

My Model class for this test
Person.cs

public class Person
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    public List<AdditionalProperty> AdditionalProperties { get; set; }
}

AdditionalProperty.cs

public struct AdditionalProperty
{
    public string Key { get; set; }

    public object Value { get; set; }

    public DateTime? DateValue
    {
        get
        {
            DateTime dateValue;
            if (DateTime.TryParse(Value?.ToString(), out dateValue))
            {
                return dateValue;
            }
            return null;
        }
        set => Value = value;
    }

    public InputType InputType { get; set; }

    public List<SelectListItem> ValueLookupItems { get; set; }
}

The reason I have a separate DateValue property here is to assist the browser when doing DateTime binding otherwise the DateTimePicker doesn't show.

I used an enum to determine what type of input type this specific property should make use of.
InputType.cs

public enum InputType
{
    TextBox,

    DropdownBox,

    TextArea,

    DateSelection,
}

In order to keep the views as simple as possible, Stephen provided me with a sample for the Index View as well as an EditorTemplate for the AdditionalProperty object. The EditorTemplate is used for separation of concerns and to ensure that all the logic behind what input type is being used is in one place.

I have found that the DateTime property doesn't work well, so an additional EditorTemplate was required. I got this from this post.

DateTime.cshtml
Note: Location of template -> /Views/Shared/EditorTemplates

@model DateTime
@{
    IDictionary<string, object> htmlAttributes;
    object objAttributes;
    if (ViewData.TryGetValue("htmlAttributes", out objAttributes))
    {
        htmlAttributes = objAttributes as IDictionary<string, object> ?? HtmlHelper.AnonymousObjectToHtmlAttributes(objAttributes);
    }
    else
    {
        htmlAttributes = new RouteValueDictionary();
    }
    htmlAttributes.Add("type", "date");
    String format = (Request.UserAgent != null && Request.UserAgent.Contains("Chrome")) ? "{0:yyyy-MM-dd}" : "{0:d}";
    @Html.TextBox("", Model, format, htmlAttributes)
}

AdditionalProperty.cshtml
Note: Location of template -> /Views/Shared/EditorTemplates
Note: The location of my AdditionalProperty formed part of the DynamicViewExample.Models namespace

@model DynamicViewExample.Models.AdditionalProperty
<div>
    @Html.HiddenFor(m => m.Key)
    @Html.LabelFor(m => m.Key, Model.Key, new {@class = "control-label"})
    @if (Model.InputType == DynamicViewExample.Models.InputType.TextBox)
    {
        @Html.TextBoxFor(m => m.Value, new {@class = "form-control"})
    }
    else if (Model.InputType == DynamicViewExample.Models.InputType.TextArea) 
    {
        @Html.TextAreaFor(m => m.Value, new {@class = "form-control"})
    }
    else if (Model.InputType == DynamicViewExample.Models.InputType.DropdownBox)
    {
        @Html.DropDownListFor(m => m.Value, Model.ValueLookupItems, new {@class = "form-control"})
    }
    else if (Model.InputType == DynamicViewExample.Models.InputType.DateSelection)
    {
        @Html.EditorFor(m => m.DateValue, new {@class = "form-control"})
    }
    else
    {
        @Html.HiddenFor(m => m.Value) // we need this just in case
    }
</div

This would be how the Index.cshtml file would look

@model DynamicViewExample.Models.Person

@{
    ViewBag.Title = "Home Page";
}

@using (Html.BeginForm())
{
    <div class="row">
        @Html.LabelFor(model => model.FirstName, new { @class = "control-label" })
        @Html.TextBoxFor(model => model.FirstName, new { @class = "form-control", placeholder = "Enter Group Name", type = "text" })
        @Html.ValidationMessageFor(model => model.FirstName, "", new { @class = "text-danger" })
    </div>
    <div class="row">
        @Html.LabelFor(model => model.LastName, new { @class = "control-label" })
        @Html.TextBoxFor(model => model.LastName, new { @class = "form-control", placeholder = "Enter Group Name", type = "text" })
        @Html.ValidationMessageFor(model => model.LastName, "", new { @class = "text-danger" })
    </div>
    <div class="row">
        @Html.EditorFor(m => m.AdditionalProperties, new { htmlAttributes = new { @class = "form-control"}})
    </div>

    <input type="submit" class="btn btn-primary" />
}

And then finally, the HomeController.cs file contains a Get and Post that allows the ability to manipulate the data as you please. What is missing here is the "dynamic" way of populating the model, but that will naturally happen once a DB has been introduced into the mix.

    [HttpGet]
    public ActionResult Index()
    {
        var model = new Person
        {
            FirstName = "Gawie",
            LastName = "Schneider",
            AdditionalProperties = new List<AdditionalProperty>
            {
                new AdditionalProperty {Key = "Identification Number", Value = "1234567890123456", InputType = InputType.TextBox},
                new AdditionalProperty {Key = "Date Of Birth", Value = DateTime.Today, InputType = InputType.DateSelection},
                new AdditionalProperty {Key = "Age", Value = "31", InputType = InputType.TextBox},
                new AdditionalProperty {Key = "Gender", Value = "Male", InputType = InputType.DropdownBox,
                    ValueLookupItems = new List<SelectListItem>
                    {
                        new SelectListItem{Text = "Male", Value = "Male"},
                        new SelectListItem{Text = "Female", Value = "Female"}
                    }},
            }
        };

        return View(model);
    }

    [HttpPost]
    public ActionResult Index(Person model)
    {
        //Do some stuff here with the model like writing it to a DB perhaps
        return RedirectToAction("Index");
    }

So if I would have to sum up what I was trying to do here.
The goal I wanted to achieve was to be able to make use of Strongly Typed / Known Properties in conjunction with Dynamic / Unknown Properties to create a system that would allow the user to create new inputs on the fly without the need for a developer to be involved.

I honestly hope that this might help someone else as well some day.

Enjoy the coding experience
Gawie

Gawie Schneider
  • 1,078
  • 1
  • 9
  • 14