ASP.NET MVC: Post a list or collection of complex types with non-sequential indices

In ASP.Net MVC when you post your form then DefaultModelBinder bind your form’s submitted value. Now what if your model is list / collection of complex type which you want to post and the case is become even worse when the sequence or let say index is not maintained. Okay let’s see it in detail.

In one of my project developed in MVC 4, I had a requirement to bind list of complex type model to view. Furthermore, from this view user can add or remove items using Jquery / JavaScript before submitting it. Let’s create this situation. Let say, I want to add more than one students in “ClassRoom” table in single submit. So, I have model named – “ClassRoomViewModel” which contain property “Students” of type List.

public class ClassRoomViewModel
{
	public List<Student> Students { get; set; }
}

public class Student
{
	public string StudentName { get; set; }
	public int Age { get; set; }
}

As this is student entry form. So it opens as an empty form using below Action method that renders a view.

public ActionResult Index()
{
	var classRoom = new ClassRoomViewModel()
	{
		Students = new List<Student>()
	};
	
	return View(classRoom);
}

Now let’s create a view which call another partial view. Here is the code snippet.

@model PostListWithBrokenSequence.Models.ClassRoomViewModel

@{
	ViewBag.Title = "Home Page";
}
<div class="jumbotron">
<h1>Enroll Students</h1>
</div>
<div class="row">
	@using (Html.BeginForm())
	{
		<div class="col-md-4">
			<h2>Add Student</h2>
			@Html.Partial("_PartialAddStudent", 0)

			@Html.ActionLink("Add Student", "LoadBlankFormView", null, new {@id = "lnkAddStudent", @class = "btn btn-primary"})
			<button class="btn btn-default" type="submit"> Save </button>
		</div>
	}
</div>

And for partial view named – “_PartialAddStudent.cshtml”. here is code snippet.

@model int

<div class="form-horizontal" id="dv_@Model" data-rownum="@Model">
    <!-- <input type="hidden" name="Students.Index" value="@Model" /> -->
    <div class="form-group">
        @Html.Label("Name", new { @class = "control-label" })
        <div>
            @Html.TextBox("Students[" + Model + "].StudentName", "", new { @class = "form-control" })
        </div>
    </div>

    <div class="form-group">
        @Html.Label("Age", new {@class = "control-label"})
        <div>
            @Html.TextBox("Students[" + Model + "].Age", "", new {@class = "form-control"})
        </div>
    </div>
    <div class="form-group">
        <a href="#" class="deleteStudent btn btn-primary">Remove</a>
    </div>
    <hr/>
</div>

When you run the application it looks like this in browser.

AddStudentForm

To bind complex objects like list etc., we need to provide an index for each row as shown in “_PartialAddStudent” view. Yes, Index which start with 0 and incremented by 1. That’s why I render “_PartialAddStudent” view with 0 index in “Index” view using HTML.Partial() method. As you see in the above code that I have manually build names and ids of all control in the partialview. Well you can use templated helper by creating strongly typed partialview in “~/Views/Shared/ EditorTemplates” and achieve the same result with less code by using the built-in Html.EditorFor() or Html.EditorForModel() helpers, but in our case it would be difficult to use as we add and remove rows dynamically from the DOM.

All-right coming to the point, Index must be in sequence when posting the form with list of complex object, otherwise MVC DefaultModelBinder can’t able to bind it to our complex object (In our case its List<Student>). Okay let’s see it practically. As you see in above image, “Add Student” button which dynamically add our partialview (row) to DOM using jquery. Below is the code snippet.

$('#lnkAddStudent').click(function() {
	var index = parseInt($('.form-horizontal:last').data('rownum')) + 1;

	$.ajax({
		url: this.href + '/' + index,
		type: "GET",
		cache: false,
		success: function(html) {
			$('.form-horizontal:last').after(html);
		}
	});
	return false;
});

When you click on “Add button”, it makes AJAX request and call “LoadBlankFormView” action method which append partialviews’ html to DOM.

public ActionResult LoadBlankFormView(int id)
{
	return PartialView("_PartialAddStudent", id);
}

Now run the application and click on “Add Student” button 2 times and then fill the form and submit the form by click on “Save” button.

StudentFormWithData

As there is no broken index so DefaultModelBinder binds our complex object correctly when you debug the application, you can see all records posted back correctly as show in below image.

FormPostWithProperIndex

Now let’s remove any of the row which break the index and makes the index as 0, 2, ..etc and our HTML in DOM looks like this –

<form action="/" method="post">        
<div class="col-md-4">
    <h2>Add Student</h2>
	<div>
		<div class="form-group">
			Name
			<div class="col-md-10">
				<input class="form-control" name="Students[0].StudentName" type="text" value="John Miller" />
			</div>
		</div>
		<div class="form-group">
			Age
			<div class="col-md-10">
				<input class="form-control" name="Students[0].Age" type="text" value="65" />
			</div>
		</div>
		<hr />
	</div>
	<div>
		<div class="form-group">
			Name
			<div class="col-md-10">
				<input class="form-control" name="Students[2].StudentName" type="text" value="Krishn" />
			</div>
		</div>
		<div class="form-group">
			Age
			<div class="col-md-10">
				<input class="form-control" name="Students[2].Age" type="text" value="90" />
			</div>
		</div>
	</div>
</div>
</form>

See the index in above code. Here is the “Remove” button code which removes row from the DOM.

$(document.body).on("click", "a.deleteStudent", function() {
	$(this).parents('div.form-horizontal:first').remove();
	return false;
});

Now when you post the above form with broken indices, you’ll lost the data from where the sequence get break. See the below image.

FormPostWithBreakIndex

Don’t worry, to overcome this problem we just need an extra hidden input field with the name – “list/collectionName.Index” for each item we need to bind to the list. In our case hidden input field name should be “Students.Index”.

<input name="Students.Index" type="hidden" value="@Model" />

The name of each of these hidden inputs must be the same with unique values. So after adding it to our “_PratialAddStudent” view, run application again and add 2 new records, remove any middle one and all. After doing all of these test cases at the end, look at rendered html code with broken sequence now looks like this –

ViewSourceOfBrokenIndices

Now click on “Save” button and post that form. Even with broken sequence DefaultModelBinder bind your form’s value correctly!!. You can download the sample demo project here. If you have any query then mail me or leave your comments.

That’s it guys for now. Hope you enjoy this post. Thank you.

Advertisements

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