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.