I am attempting to write a page that retrieves a List<> from a SQL table and display it so users can make changes. I'm already able to display the information. I'm running into problems with submitting it. I am unable to figure out a way to take the data entered and send the changes to a List<> so I can manipulate it in the controller/model.
Currently I'm placing the new data into arrays upon submit. Since I'm using @model List<modelname>
in the view. Is it possible to replace the values or possibly put the data into a new list from the View?
You can absolutely do this, but people do initially tend to run into problems, so I'll provide a full example.
To start with, let's define a HomeController
which has an Index
action that returns a List<EmployeeViewModel>
:
public class HomeController : Controller
{
public ActionResult Index()
{
// Assume this would really come from your database.
var employees = new List<EmployeeViewModel>()
{
new EmployeeViewModel { Id = 1, Name = "Employee 1" },
new EmployeeViewModel { Id = 2, Name = "Employee 2" },
new EmployeeViewModel { Id = 3, Name = "Employee 3" },
};
return View(employees);
}
[HttpPost]
public ActionResult Index(List<EmployeeViewModel> employees)
{
// Rest of action
}
}
The typical way people first approach this problem is to do something like the following in index.cshtml
:
@model List<EmployeeViewModel>
@using (Html.BeginForm())
{
foreach (var item in Model)
{
<div class="row">
@Html.EditorFor(x => item.Id)
</div>
<div class="row">
@Html.EditorFor(x => item.Name)
</div>
}
<input type="submit" />
}
At first glance, this would look like it would work. However, if you put a breakpoint in the POST action and click the submit button, you'll notice that employees
is null
. The reason for that is because the foreach
loop is generating HTML like the following:
<input name="item.Id" type="number" value="1" />
<input name="item.Name" type="text" value="Employee 1" />
<input name="item.Id" type="number" value="2" />
<input name="item.Name" type="text" value="Employee 2" />
<input name="item.Id" type="number" value="3" />
<input name="item.Name" type="text" value="Employee 3" />
I've stripped out some irrelevant parts there, but notice how the name
attributes have the same values for each employee. When the default model binder tries to construct the list of data on the server, it has to be able to distinguish between the different employees, and, because it can't, it results in the list being null
.
So why is it generating the same values? It's because of this:
@Html.EditorFor(x => item.Id)
@Html.EditorFor(x => item.Name)
We're not passing an index value to the HtmlHelper
method calls, so that information doesn't make it to the generated HTML. We can fix this simply by making use of a for
loop instead:
for (int i = 0; i < Model.Count; i++)
{
@Html.EditorFor(x => Model[i].Id)
@Html.EditorFor(x => Model[i].Name)
}
As we're now supplying an index with each method call, the generated HTML does now contain an index for each employee:
<input name="[0].Id" type="number" value="1" />
<input name="[0].Name" type="text" value="Employee 1" />
<input name="[1].Id" type="number" value="2" />
<input name="[1].Name" type="text" value="Employee 2" />
<input name="[2].Id" type="number" value="3" />
<input name="[2].Name" type="text" value="Employee 3" />
This allows the default model binder to associate an Id
and a Name
with each EmployeeViewModel
, allowing it to correctly construct the type on the server.
At this point, your problem is solved, however, it's not recommended to use a for
loop if you can avoid it, which brings us to editor templates. Editor templates are HtmlHelper
methods that allow us to render a custom template (i.e. a view) for a given type. So let me show you an example of how to do that with the example code above.
To start with, you'll need to do the following:
EditorTemplates
folder inside of your ~/Views/Home/
folder (the EditorTemplates
name has special meaning in MVC, so it's important to spell it correctly).EmployeeViewModel.cshtml
view inside of that folder (again, the name is important here: it should match the name of the type you wish to create a custom template for).Once you've done that, it should look like this:
Now, open up EmployeeViewModel.cshtml
, and put your rendering code from Index.cs
inside of it:
@model EmployeeViewModel
<div class="row">
@Html.EditorFor(x => x.Id)
</div>
<div class="row">
@Html.EditorFor(x => x.Name)
</div>
Finally, open up index.cshtml
and change it to the following:
@model List<EmployeeViewModel>
@using (Html.BeginForm())
{
@Html.EditorFor(x => x)
<input type="submit" />
}
Html.EditorFor
, and Html.DisplayFor
, are both smart enough to recognise when they're being called for a collection, so they look for a custom template to render the collection's type (in this case, EditorFor
will look for an editor template for EmployeeViewModel
).
As we've provided an editor template, not only will it render it for each item in the collection, it will also generate the correct indices for each of those items, giving the model binder all the information it needs to reconstruct that collection on the server.
The end result is that model binding becomes simpler, and not only is the code for your views simpler, it is also split up based on the types involved, which makes your views easier to work with, as opposed to having a giant view that does everything.
I should also mention that as my example is directly using a collection, and nothing more, you can actually replace @Html.EditorFor(x => x)
with @Html.EditorForModel()
. I didn't do that initially as I didn't want to give the impression that templates are called just by using the latter.