Sunday 7 September 2014

Two ways to pass a list or array from view to action in ASP.NET MVC4 : by javascript or by custom Modelbinder

In this post, I will talk about two tricks to pass a list from the View to the Action in asp.net MVC4.

Let's start with a asp.net mvc 4 basic project inside which I have created the view below :
 
 

The view /Test/Index seems very simple :
  • a table having 4 columns
  • on each row , the remove button deletes the current rows, remove it totally from the grid.
  • the Save button posts the current content of the table to the /Test/Action method on the server side.

The TestController contains the two actions :
  • Index : displays the table
  • Save1 and Save2 : manages the save process ( Save1 is used for the common way, Save2 for the use of a custom modelbinder)
public class TestController : Controller
{
public ActionResult Index()
{
List<TestModel> model = new List<TestModel>();
model.Add(new TestModel() { FirstName="Charly",LastName="Park.", Age=75 });
model.Add(new TestModel() { FirstName = "Stan", LastName = "Get.", Age = 80 });
model.Add(new TestModel() { FirstName = "Mick", LastName = "Bran.",Age=90 });
model.Add(new TestModel() { FirstName = "Georg", LastName = "Bras.",Age=65 });
return View(model);
}
[HttpPost]
public ActionResult Save1(List<TestModel> model)
{
return null;
}

[HttpPost]
public ActionResult Save2([ModelBinder(typeof(TestSaveModelBinder))] List<TestModel> model)
{
return null;
}
}

The model used is TestModel
namespace Devenva.Demo.Solution.ModelBinder.Models
{
public class TestModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
}
}

The Index.cshtml view :

CASE 1 : the Javascript version

@model List<Devenva.Demo.Solution.ModelBinder.Models.TestModel>
<h2>Index</h2>
<script type="text/javascript" src="~/Scripts/jquery-1.8.2.js"></script>
<form id="frm_Test" method="post" action="/Test/Save1">
<table>
<thead>
<th>FirstName</th>
<th>LastName</th>
<th>Age</th>
<th></th>
</thead>
<tbody>
@{ int i=0;}
@foreach (var item in Model)
{
<tr>
<td><input type="text" name="Model[@i].FirstName" value="@item.FirstName" /></td>
<td><input type="text" name="Model[@i].LastName" value="@item.LastName" /></td>
<td><input type="button" class="remove" value="remove" /></td>
</tr>
i++;
}
</tbody>
</table>
<input type="submit" value="Save"/>
</form>
<script type="text/javascript">
$(function () {
$('.remove').click(function () {
$(this).parent().parent().remove();
});
});

</script>

If we do some research over the web, this is the common way to implement the table. In the tbody markup, we loop through the List of TestModel and we have to give a different name for each input to make it a unique element when the page is posted back to the server. This is the classical way to tell the mvc engine how data will processed.
Try to remove a row (for example the 2nd row) and save the form, the asp.net mvc is no longer able to parse our datas ( in the Save1 action, model is null). Why?
When removing a row, there was a gap in the index order ( here i=2 is missing ) so this case is not handled by the default modelbinder ( In few words, model binder is the component that manages the conversion of a html form data to a C# object, if the conversion goes well, the excpected object is implicitly mapped, otherwise an error is thrown or object is set to null ).
To prevent this behavoir, the view have to manage the list of index. This list should be updated each time a row is added or removed. This can be done easily by a javascript function. I will not focus on that function in this post.

CASE 2 : Using a custom modelbinder

@model List<Devenva.Demo.Solution.ModelBinder.Models.TestModel>
<h2>Index</h2>
<script type="text/javascript" src="~/Scripts/jquery-1.8.2.js"></script>
<form id="frm_Test" method="post" action="/Test/Save2">
<table>
<thead>
<th>FirstName</th>
<th>LastName</th>
<th>Age</th>
<th></th>
</thead>
<tbody>
@foreach (var item in Model)
{
<tr>
<td><input type="text" name="FirstName" value="@item.FirstName" /></td>
<td><input type="text" name="LastName" value="@item.LastName" /></td>
<td><input type="text" name="Age" value="@item.Age" /></td>
<td><input type="button" class="remove" value="remove" /></td>
</tr>
}
</tbody>
</table>
<input type="submit" value="Save"/>
</form>
<script type="text/javascript">
$(function () {
$('.remove').click(function () {
$(this).parent().parent().remove();
});
});

</script>

We have to notice that in the second version we don't take care of the uniqueness of the name of each input element.
The posted data looks like

We keep the view as simple as possible, the tricky part of the job is on the server side.

In the current case, the action public ActionResult Save(List<TestModel> model)
is not able to convert those data to a List<TestModel>
We need to build a custom model binder that will manage this specific stuff.
I have create the class TestSaveModelBinder :

namespace Devenva.Demo.Solution.ModelBinder.ModelBinders
{
public class TestSaveModelBinder : IModelBinder
{
static private string[] fieldSeparator =new string[]{ "&" };
static private string[] keyValueSeparator = new string[] { "=" };
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
HttpRequestBase request = controllerContext.HttpContext.Request;
var formStr = request.Form.ToString();
var listObject = ParseToModel(formStr);
List<TestModel> model = new List<TestModel>();
for (int i = 0; i < listObject.Length; i++)
{
model.Add(new TestModel() {
FirstName = listObject[i].Item1,
LastName = listObject[i].Item2,
Age = listObject[i].Item3
});
}
return model;
}
static private Tuple<string, string, int>[] ParseToModel(string input)
{
var fieldValues = input.Split(fieldSeparator, StringSplitOptions.None);
object[] properties = new object[fieldValues.Length];
for (int i = 0; i < fieldValues.Length; i++)
{
var value = fieldValues[i].Split(keyValueSeparator,StringSplitOptions.None);
properties[i] = value[1].ToString();
}
int dataNumber = properties.Length / 3;
var result = new Tuple<string, string, int>[dataNumber];

for (int i = 0; i < dataNumber ; i++)
{
var data = new Tuple<string, string, int>(properties[i].ToString(), properties[i + dataNumber * 1].ToString(),int.Parse( properties[i + dataNumber * 2].ToString()));
result[i] =data;
}
return result;
}
}
}

Working with modelbinder always starts from the posted data (controllerContext.HttpContext.Request.Form.ToString()).
The BindModel method implements the logic to parse the data to the type of the input parameter of the action ( List<TestModel> ).
Once the modelbinder was setup, we should tell the mvc engine to use it only that specific action:
[HttpPost]
public ActionResult Save2([ModelBinder(typeof(TestSaveModelBinder))] List<TestModel> model)
{
return null;
}

By putting a breakpoint on that method, we can see that model contains all the datas that we have posted.

No comments:

Post a Comment