This is an translation of my article from 2nd April of 2012.
From the first version of ASP.NET MVC couple of years ago I found that there is no common solution how to display drop down lists properly. Probably every of you asked yourself: “How to pass the selection list to a drop down list?” And so I was asking the same question to myself, I could not even sleep;)
Introduction
For example we have an form to create a film, and we need to select film's genre from a drop down list. I'm not going to show any "newbuy" solutions as, for instance, getting the genres in the view file.
"Head on" solution
The programmers bumping that problem try to solve it by brute force: they create additional property "Genres" of type SelectList at the model class, and they fill it at the controller's action. Like this:
The model
public class Movie {
public int GenreId { get; set; }
public SelectList Genres { get; set; }
//...
}
The controller
public class MoviesController {
[HttpGet] public ActionResult Create() {
var model = new Movie() { Genres = GetAllGenresFromDatabase(); }
return View(model);
}
[HttpPost] public ActionResult Create(Movie form) {
// do something with movie.
}
[HttpGet] public ActionResult Edit(int id) {
var model = GetMovieFromDatabase();
model.Genres = GetAllGenresFromDatabase();
return View(model);
}
[HttpPost] public ActionResult Edit(EditMovie form) {
// do something with movie.
}
//...
}
This works so far, but the solution has some major problems
- Model has redundant fields
- Need to create a model for a creation form (when there is no entity in DB yet)
- Code duplication for select list options retrieval logic. This problem grows if you need to select form the same list in many places.
- Not possible to use
Html.EditorForModel()
to display all you form at once with auto-generated markup, as we do not have access to neighbour fields when ASP.NET renders the model
Solution using ViewBag / ViewData
The solution is similar to the previous one with one simple change: selection list is passing through ViewBag / ViewData
The model
public class Movie {
[UIHint("Genres")]
public int GenreId { get; set; }
//...
}
The controller
public class MoviesController {
[HttpGet] public ActionResult Create() {
ViewBag.Genres = GetAllGenresFromDatabase();
return View();
}
[HttpPost] public ActionResult Create(Movie form) {
// do something with movie.
}
[HttpGet] public ActionResult Edit(int id) {
var model = GetMovieFromDatabase();
ViewBag.Genres = GetAllGenresFromDatabase();
return View(model);
}
[HttpPost] public ActionResult Edit(EditMovie form) {
// do something with movie.
}
//...
}
Pros over the previous solution
- The model does not have redundant fields
- Not needed to create a model for a creation form (when there is no entity in DB yet)
- Possible to use
Html.EditorForModel()
with own Editor Template for every drop-down list field
Cons
- Using of the dynamics or magic-strings*, which complicates refactoring and code modification
- Code duplication for select list options retrieval logic
- Custom editor template per field
Improved solution with ViewBag / ViewData
To improve the solution and reduce code duplication of retrieving of selection options logic we move the code from the controller action to an action filter
The model
The same as in the previous example
ActionFilter
public class PopulateGenresAttribute: ActionFilterAttribute {
public override void OnActionExecuted(ActionExecutedContext filterContext) {
filterContext.Controller.ViewData["Genres"] = GetAllGenresFromDatabase();
}
//...
}
The controller
public class MoviesController {
[HttpGet, PopulateGenres] public ActionResult Create() {
return View();
}
[HttpPost] public ActionResult Create(Movie form) {
// do something with movie.
}
[HttpGet, PopulateGenres] public ActionResult Edit(int id) {
var model = GetMovieFromDatabase();
return View(model);
}
[HttpPost] public ActionResult Edit(EditMovie form) {
// do something with movie.
}
//...
}
Pros over the previous solution
- No code duplication for select list options retrieval logic
Cons
- Using of the dynamics or magic-strings*, which complicates refactoring and code modification
- Custom editor template per field
- Application specific logic in the filter, which smells
Imporved solution with ViewBag / ViewData + MvcExtnsions
MvcExtensions have beautiful methdos to work with drop down lists: AsDropDownList / AsListBox (the first one for drop down list and the second one for multi-select lists). These are extension methods for model metadata builder. These methods set the template and allow to pass the ViewData's field name which stores the selection list to the View. So we are solving the need of having an template per field.
The model
public class Movie {
public int GenreId { get; set; }
}
The metadata
public class MovieMetadata : ModelMetadataConfiguration {
public MovieMetadata {
Configure(movie => movie.GenreId).AsDropDownList("Genres"/*шаблон*/);
}
}
The controller
The same as in the previous example
Pros over the previous solution
- Using of two generic templates (DropDownList / ListBox) for all lists, or specific one if needed
Cons
- Using of the dynamics or magic-strings*, which complicates refactoring and code modification
The solution with ChildAction
This is a good idea to put logic of the selection list to a separate action. This will lead to better separation of concern and give you some benifits like caching. But yf you'll try to use just a child action, then it'll not work at all: you will not have client-side validation from the box, you will not be able to use nested forms, etc., because the field names will be broken. To make it work properly you need to set view data's model metadata to the same as in parent action.
The model
public class Movie {
[UIHint("Genres")]
public int GenreId { get; set; }
}
The controllers
public class MoviesController {
[HttpGet] public ActionResult Create() {
return View();
}
[HttpPost] public ActionResult Create(Movie form) {
// do something with movie.
}
[HttpGet] public ActionResult Edit(int id) {
var model = GetMovieFromDatabase();
return View(model);
}
[HttpPost] public ActionResult Edit(EditMovie form) {
// do something with movie.
}
}
public class GenresController {
public ActionResult List() {
var selectedGenreId = this.ControllerContext.ParentActionViewContext.ViewData.Model as int?;
var genres = GetGenresFormDatabase();
var model = new SelectList(genres, "Id", "DisplayName", selectedGenreId);
this.ViewData.Model = model;
this.ViewData.ModelMetadata = this.ControllerContext.ParentActionViewContext.ViewData.ModelMetadata;
return View("DropDown");
}
}
Pros over the previous solution
- No using of dynamics and magic-strings
- No code duplication of select list options retrieval logic
- No application specific logic in ActionFilter
Cons
- Duplication of the boilerplate code
- Custom template per select list
- The Post-Redirect-Get scenario is not supported
Solution using ChildAction + MvcExtensions
I decided to imporve the previouse solution and apply the ActionFilter experience, and now, from the version 2.5.0-rc8000 MvcExtension do support child-action based drop down lists out of the box. I've added extension methods, which allows to use RenderAction to render the model fields. Also SelectListActionAttribute
attibute were added, which serves the action providing selection list data. Also this solution supports Post-Redirect-Get scenario.
The model
public class Movie {
public int GenreId { get; set; }
}
The metadata
public class MovieMetadata : ModelMetadataConfiguration {
public MovieMetadata {
Configure(movie => movie.GenreId).RenderAction("List", "Genres");
}
}
The controllers
public class MoviesController {
[HttpGet] public ActionResult Create() {
return View();
}
[HttpPost] public ActionResult Create(Movie form) {
// do something with movie.
}
[HttpGet] public ActionResult Edit(int id) {
var model = GetMovieFromDatabase();
return View(model);
}
[HttpPost] public ActionResult Edit(EditMovie form) {
// do something with movie.
}
}
public class GenresController {
[ChildActionOnly, SelectListAction] public ActionResult List(int selected) {
var model = GetGenresFormDatabase(selected);
return View("DropDown", model);
}
}
Pros over the previous solution
- No boilerplate code duplication
- Use an generic template
- MultiSelect "out of the box"
- Post-Redirect-Get is supported
Ending
For me, as a developer of MvcExtensions the methdos using the library is preferrable.
The sample of code with ViewBag / ViewData + MvcExtensions is here: http://github.com/MvcExtensions/Core/tree/master/samples
The sample of code with ChildAction + MvcExtensions is here: http://github.com/hazzik/DropDowns
*magic-strings can be easily solved by using of constans, and so because of that, I prefere using string over dynamic properties.
PS: The MvcExtensions abilities to extend ASP.NET MVC are limitless