J P 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

<form action="/Employees/Create" method="post">
<p>
    Name
    <input type="text" name="Name" />
</p>
<p>
    Phone Type
    <select name="PrimaryPhone.Type"><option value="Business">Business</option><option value="Cell">Cell</option><option value="Home">Home</option></select>
    Phone Number
    <input type="text" name="PrimaryPhone.Number" />
</p>
<p>
    <input type="submit" name="btnSubmit" value="Create new employee" />
</p>
</form>

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 
{ 
    public string Name { get; set; } 
    public List<Phone> Phones { get { return _phones; } }
    private List<Phone> _phones = new List<Phone>(); 
}

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

@model EmployeeDemo.Models.EmployeeViewModel

<form action="/Employees/Create" method="post">
<p>
    Name
    <input type="text" name="Name" />
</p>
<p>
    Phone Type
    <select name="Phones[0].Type"><option value="Business">Business</option><option value="Cell">Cell</option><option value="Home">Home</option></select>
    Phone Number
    <input type="text" name="Phones[0].Number" />
</p>
<p>
    Phone Type
    <select name="Phones[1].Type"><option value="Business">Business</option><option value="Cell">Cell</option><option value="Home">Home</option></select>
    Phone Number
    <input type="text" name="Phones[1].Number" />
</p>
<p>
    <input type="submit" name="btnSubmit" value="Create new employee" />
</p>
</form>

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

<form action="/Employees/Create" method="post">
    <p>
        @Html.LabelFor(model => model.Name)
        @Html.TextBoxFor(model => model.Name)
    </p>

    <div id="phoneList">
        @if (Model.Phones.Count > 0)
        {
            <span>Phone Type Phone Number</span>
        }

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

    <p>
        <input type="submit" name="btnSubmit" value="Create new employee" />
    </p>

</form>

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

<script type="text/javascript" src="../../Scripts/jquery-1.10.2.min.js"></script>
<script type="text/javascript">

$(document).ready(function () {

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

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

    $("#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');
        });
    })
});

</script>

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

Advertisements

6 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


RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Create a free website or blog at WordPress.com.

%d bloggers like this: