Patric Johansson On Applied .NET

May 25, 2015

ASP.NET MVC: Proper model binding with dynamic form

Filed under: ASP.NET MVC,C#,JQuery — Patric Johansson @ 10:04 PM
Tags: , ,

ASP.NET MVC automatically maps values entered in a form by a user to properties of a ViewModel object. This is done by matching property names to field names and is commonly known as model binding. Correctly done it will aid in decoupling your controller from a view.

We will use an employee database as our domain. The goal is to allow us to create employees with a name and phone numbers. Let’s start with an EmployeeViewModel class with a Name and a PhoneNumber property.

public class EmployeeViewModel
{
    public string Name { get; set; }
    public string PhoneNumber { get; set; }
}

Next we need a form defined in view Create.cshtml with appropriately named fields to automatically create an EmployeeViewModel object.

@model EmployeeDemo.Models.EmployeeViewModel

<form action="/Employees/Create" method="post">
    <p>
        Name
        <input type="text" name="Name">
    </p>
    <p>
        Phone Number
        <input type="text" name="PhoneNumber" />
    </p>
    <p>
         <input type="submit" name="btnSubmit" value="Create new employee" />
</p>
</form>

Notice that the names of the HTML fields matches the names of the viewmodel properties. In the EmployeesController we would have a Create post method which takes the viewmodel as argument.

// POST: Employees/Create
[HttpPost]
public ActionResult Create(EmployeeViewModel employeeViewModel)
{
    if (!ModelState.IsValid)
    {
        return View("Create", employeeViewModel);
    }

    // Save the created employee object

    return RedirectToAction("Index");
}

Model binding can also handle nested objects. Let’s introduce a Phone class to demonstrate.

public class EmployeeViewModel
{
    public string Name { get; set; }
    public Phone PrimaryPhone { get; set; }
} 

public class Phone
{
    public enum Types
    {
        Business,
        Cell, 
        Home
     }
     public Types Type { get; set; }
     public string Number { get; set; }
}

We need to modify our form to allow users to input phone type. We will use a dropdown list (select field) to enter phone type. Notice the names of the phone fields specifying how they are nested.

@model EmployeeDemo.Models.EmployeeViewModel
Name
Phone Type Business
Cell
Home
Phone Number

No changes will need to be done to our controller. The controller is not tightly coupled to the view.

A straightforward requirement for an Employee class would be multiple phone numbers. Let us start with allowing users to enter two phones for an employee. Phone class is defined as above. We add a collection property of Phone objects to EmployeeViewModel class.

public class EmployeeViewModel
{
&amp;nbsp;&amp;nbsp;&amp;nbsp; public string Name { get; set; }
&amp;nbsp;&amp;nbsp;&amp;nbsp; public List&amp;lt;Phone&amp;gt; Phones { get { return _phones; } }
&amp;nbsp;&amp;nbsp;&amp;nbsp; private List&amp;lt;Phone&amp;gt; _phones = new List&amp;lt;Phone&amp;gt;();
}

Our form needs now two sets of fields for defining phone numbers.

@model EmployeeDemo.Models.EmployeeViewModel
Name
Phone Type Business
Cell
Home
Phone Number
Phone Type Business
Cell
Home
Phone Number

The model binding will create a List with two phones. It is important that names have consecutive indexes or model binding will not work properly. Allowing only a fixed number of phones in our form is not a practical solution, we need to allow user to add any number of phones, meaning zero or more. For this we will use jQuery to allow client-side logic for adding and removing fields in our form depending on number of phones for a given employee.

We will need two jQuery functions, one for adding rows and one for removing rows. We will add links for user to add and remove rows. A row will consists of fields for entering one phone with a phone type and number.

Let’s see an image of our form in action. In the image we can see that the user wants to create an employee with three phones.
EmployeeForm

The add function will append a new row to bottom of the list of phones. It will get index to use for field names by counting existing rows.

The remove function will remove the row with the clicked Remove link as well as rename all remaining field names using consecutive indexes. As mentioned before, names of fields need to have consecutive indexes.

The view for the create action uses HtmlHelper methods for HTML generation. The div with id phoneList is the list of phones. It contains zero or more divs of class phoneRow which contains the dropdown list and input field.

@using EmployeeDemo.Models;
@model EmployeeDemo.Models.EmployeeViewModel

&amp;lt;form action="/Employees/Create" method="post"&amp;gt;
    &amp;lt;p&amp;gt;
        @Html.LabelFor(model =&amp;gt; model.Name)
        @Html.TextBoxFor(model =&amp;gt; model.Name)
    &amp;lt;/p&amp;gt;

    &amp;lt;div id="phoneList"&amp;gt;
        @if (Model.Phones.Count &amp;gt; 0)
        {
            &amp;lt;span&amp;gt;Phone Type Phone Number&amp;lt;/span&amp;gt;
        }

        @for (int i = 0; i &amp;lt; Model.Phones.Count; i++)
        {
            &amp;lt;div class="phoneRow"&amp;gt;
                @Html.DropDownList("Phones[" + i + "].Type", Model.Phones[i].PhoneTypeSelectListItems, new { @class = "phoneType" })
                @Html.TextBoxFor(model =&amp;gt; Model.Phones[i].Number, new { @class = "phoneNumber" })
                &amp;lt;a href="javascript:void(0);" class="remRow"&amp;gt;Remove&amp;lt;/a&amp;gt;
            &amp;lt;/div&amp;gt;
        }
    &amp;lt;/div&amp;gt;

    &amp;lt;p&amp;gt;
        &amp;lt;a href="javascript:void(0);" class="addRow"&amp;gt;Add row&amp;lt;/a&amp;gt;
    &amp;lt;/p&amp;gt;

    &amp;lt;p&amp;gt;
        &amp;lt;input type="submit" name="btnSubmit" value="Create new employee" /&amp;gt;
    &amp;lt;/p&amp;gt;

&amp;lt;/form&amp;gt;

PhoneTypeSelectListItems is a new property on our Phone class which returns the enum for phone types as a list of SelectListItems. We also add some data validations attributes to the properties of the class.

public class Phone
{
    public enum Types
    {
        Business,
        Cell,
        Home
    }

    [Required]
    public Types Type { get; set; }

    [Required]
    [Phone]
    public string Number { get; set; }

    public IEnumerable&amp;lt;SelectListItem&amp;gt; PhoneTypeSelectListItems
    {
        get
        {
            foreach(Types type in Enum.GetValues(typeof(Types)))
            {
                SelectListItem selectListItem = new SelectListItem();
                selectListItem.Text = type.ToString();
                selectListItem.Value = type.ToString();
                selectListItem.Selected = Type == type;
                yield return selectListItem;
            }
        }
    }
}

When the Create view is first opened, Model.Phones.Count is zero. Model.Phones.Count will only be non-zero after user hit submit with an invalid model state, for example empty name or invalid phone number. Let’s take a look at our jQuery functions for adding and removing rows in the form.

&amp;lt;script type="text/javascript" src="../../Scripts/jquery-1.10.2.min.js"&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;script type="text/javascript"&amp;gt;

$(document).ready(function () {

    $(".addRow").click(function () {

        var rowCount = $('.phoneRow').length;
        $("#phoneList").append('&amp;lt;div class="phoneRow"&amp;gt;&amp;lt;select name="Phones[' + rowCount + '].Type" class="phoneType"&amp;gt;@foreach (SelectListItem item in new Phone().PhoneTypeSelectListItems){&amp;lt;option value="@item.Value"&amp;gt;@item.Text&amp;lt;/option&amp;gt;}&amp;lt;/select&amp;gt;&amp;lt;input type="text" name="Phones[' + rowCount + '].Number" class="phoneNumber" /&amp;gt;&amp;lt;a href="javascript:void(0);" class="remRow"&amp;gt;Remove&amp;lt;/a&amp;gt;&amp;lt;/div&amp;gt;');
    });

    $("#phoneList").on('click', '.remRow', function () {

        $(this).closest('.phoneRow').remove();

        $('.phoneType').each(function (index) {
            $(this).attr('name', 'Phones[' + index + '].Type');
        });

        $('.phoneNumber').each(function (index) {
            $(this).attr('name', 'Phones[' + index + '].Number');
        });
    })
});

&amp;lt;/script&amp;gt;

The addRow function appends some HTML for a new phoneRow div to the phoneList div. This HTML is the same as generated by our HtmlHelper methods used in the form. The new fields will have names with an index equal to existing number of phoneRow divs so we have consecutively named fields. The remRow function removes the phoneRow div it belongs to. Next it iterates over all fields and renames them so we get consecutively named fields. It finds them using class names phoneType and phoneNumber.

Usually when working with dynamic forms and lists, a FormCollection object is used to get field values in the controller method. The Create method in the EmployeesController would look something like this:

// POST: Employees/Create
[HttpPost]
public ActionResult Create(FormCollection formData)
{
    string name = formData["Name"];
    string phones0Type = formData["Phones[0].Type"];
    string phones1Number = formData["Phones[0].Number"];
    . . .

The drawback is of course that we now need to construct our EmployeeViewModel from a string collection and cast it to correct type. Any changes to EmployeeViewModel or form will break the controller method, like changing Name property to first and last name properties. Instead, with just some minor work in the form making sure that all fields have proper names and relying on ASP.NET MVC model binding, we get a fully constructed object and a controller which is not tightly coupled to its view.

On the web there are many examples how to do a dynamic list in a form, but all I seen rely on using an HTML table instead of nested divs. Tables are for tabular data and not for layout. A list of fields is not tabular data so we use nested divs instead.

CSS styling of any kind is intentionally left out, in order to focus on model binding using dynamic forms.

The attached Visual Studio 2013 C# solution contains the above code with some minor additions. Validation messages are displayed. The EmployeeViewModel class has an additional ID property. Beside the Create view we also have an Edit view for editing employees. The form is identical to the Create view, but added for to give demonstrate how edit is done with model binding. An Index view is the Home view for viewing employees and contains links to create and edit employees. There’s no true model for storing employees and no checks that a requested employee exists, employees are stored in a list and we assume employee ID is the index for that employee object in the list.

Download full VS2013 project

11 Comments »

  1. WOW!!! Patric, “thank you”.

    Comment by name — December 6, 2015 @ 10:08 PM | Reply

  2. Excellent post Patric. Good demonstration/explanation.

    Comment by Chris — August 15, 2016 @ 12:00 PM | Reply

  3. Absolutely loved this.

    Comment by portspanfa1601 — November 26, 2016 @ 5:09 PM | Reply

  4. Thank u u save me. I spent 6 hours to found this example

    Comment by Abdou MANAI — January 30, 2017 @ 8:34 AM | Reply

  5. lit

    Comment by dank memes — February 21, 2017 @ 10:06 PM | Reply

  6. Thanks, I found it very helpful

    Comment by Abdulrahman Sulaiman — April 16, 2017 @ 2:37 AM | Reply

  7. Very nice Article, there is a nice workaround, that makes it easier to deal with the consecutive thing.

    You can add a hidden field for every row like so:

    @Html.Hidden(“Phones.Index”, rowCount, new { id = “” })

    If you add this line to every row, the numbers dont have to be consecutive anymore. Means less work to do in Javascript / Jquery (especially the delete part).

    Comment by Benjamin Schneuwly — October 11, 2017 @ 4:19 AM | Reply

  8. I have been searching for some explanation of how all of this works and a complete example there of. Microsoft does a horrible job of going to the next level of depth in their guides. They should pay you for this. Problem is it took me almost a week to find it… Thank you for posting this… Just out of curiosity do you know if there is a framework that manages this basic UI operation more elegantly? Even with all of this wonderful MVC. MVVM, the fact is it seems a little stone-age having to hand code out a common CRUD action even for slightly more complicated models. Thanks again

    Comment by Chris Berberich — September 10, 2018 @ 10:56 AM | Reply

  9. I’m wondering if you have an update to this for Razor pages and instead of handing back a FormCollection, actually using a boundProperty ? TIA

    Comment by Chris Berberich — September 11, 2018 @ 12:40 PM | Reply

  10. Take a look at this https://youtu.be/3o92v9DNfz0

    Comment by Mohammad Mustafa — November 23, 2018 @ 3:29 PM | Reply

  11. if i delete some row which is dynamically added using Jquery then the index serial is going to break and the post method dont post all the rows to the controller. whats the solution ?

    Comment by shariaretanvir akash — April 18, 2020 @ 3:19 PM | Reply


RSS feed for comments on this post. TrackBack URI

Leave a comment

Create a free website or blog at WordPress.com.