С самого выхода еще первой версии ASP.NET MVC три года назад я столкнулся с проблемой выпадающих списков. Наверное каждый из вас задавал себе вопрос: “Как корректно передавать данные для отображения в выпадающие списки?” Вот и меня до недавнего времени этот вопрос волновал и очень существенно. Я буквально не мог спать;)
Допустим у нас есть форма, для создания фильма. И нам нужно из DropDown выбрать жанр фильма. Откровенно “профанские” решения, такие как получение возможных значений прямо на View я рассматривать не буду.
Решение “в лоб”
Программисты, сталкивающиеся с этой проблемой очень часто идут решать ее в лоб: в модели создается дополнительное свойство Genres типа SelectList и оно заполняется в методе контроллера.
Модель:
public class Movie {
public int GenreId { get; set; }
public SelectList Genres { get; set; }
//...
}
Контроллер:
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.
}
//...
}
Но у этого решения для меня есть огромные недостатки:
- Лишнее поле в модели
- Необходимо создавать модель при отображении формы создания
- Дублирование кода заполнения возможных значений. Эта проблема становится особенно актуальное, если у вас в системе можно во многих местах выбирать значения из одного и того же справочника.
- Нет возможности использовать
Html.EditorForModel()
- ASP.NET отображает общую разметку для всех полей модели, при этом при отображении какого-либо поля модели нет доступа к другим полям.
Решение с использованием ViewBag / ViewData
Это решение по большей части аналогично предыдущему решению, за тем лишь исключением, что возможные значение передаются через ViewBag:
Модель:
public class Movie {
[UIHint("Genres")]
public int GenreId { get; set; }
//...
}
Контроллер:
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.
}
//...
}
Плюсы, по сравнению с предыдущим решением.
- Нет лишних полей в модели
- Нет необходимости создавать модель при отображении формы создания
- Можно использовать
Html.EditorForModel()
в связке с шаблоном (EditorTemplate). При этом для каждого справочника необходим свой шаблон
Минусы
- Используется dynamic или magic-strings*, что не всегда положительно сказывается на возможности рефакторинга
- Дублирование кода заполнения возможных значений
- Необходимо иметь по шаблону на каждый тип справочника
Недостатки
Улучшенное решение с использованием ViewBag / ViewData
Для устранения дублировани кода получения возможных значений вынесем этот код в отдельный ActionFilter.
Модель:
та же, что в предыдущем примере
ActionFilter:
public class PopulateGenresAttribute: ActionFilterAttribute {
public override void OnActionExecuted(ActionExecutedContext filterContext) {
filterContext.Controller.ViewData["Genres"] = GetAllGenresFromDatabase();
}
//...
}
Контроллер:
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.
}
//...
}
Плюсы, по сравнению с предыдущим решением.
- Устранено дублирование кода заполнения возможных значений
Минусы
- Используется dynamic или magic-strings*, что не всегда положительно сказывается на возможности рефакторинга
- Необходимо иметь по шаблону на каждый тип справочника
Улучшенное решение с использованием ViewBag / ViewData + MvcExtnsions
В MvcExensions есть замечательные методы для работы с drop-down list: AsDropDownList / AsListBox (первый для выпадающего списка, второй для множественного выбора). Это методы-расширения для конструктора метаданных. Данные методы устанавливают шаблон и позволяют передать в шаблон название поля ViewBag, которое хранит данные с возможными значениями. Таким образом решается проблема с необходимостю иметь по шаблону на каждый справочник.
Модель:
public class Movie {
public int GenreId { get; set; }
}
Метаданные:
public class MovieMetadata : ModelMetadataConfiguration {
public MovieMetadata {
Configure(movie => movie.GenreId).AsDropDownList("Genres"/*шаблон*/);
}
}
Контроллер:
как в предыдущем примере.
Плюсы, по сравнению с предыдущим решением:
- Используется два универсальных шаблона (DropDownList / ListBox) для всех списков (есть возможность указать свой шаблон, если это необходимо)
Минусы:
- Используется dynamic или magic-strings*, что не всегда положительно сказывается на возможности рефакторинга.
Решение с использованием ChildAction
Если попытаться использовать child action “в лоб”, то это решение просто-напросто не будет работать: не будет работать клиенская валидация, не будут работать сценарии в случае сложных вложенных форм и т.д. В статье (часть 2) неизвестного автора (быстрый поиск выдал только профиль на хабре) решены эти проблемы, и по-этому я буду рассматривать окончательное решение автора.
Модель
public class Movie {
[UIHint("Genres")]
public int GenreId { get; set; }
}
Контроллеры:
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() {
int? 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");
}
}
Плюсы, по сравнению, с решениями с ViewBag / ViewData
- Не используется dynamic или magic-strings
- Устранено дублирование кода заполнения возможных значений
Минусы
- Дублирование обслуживающего кода
- Необходимо иметь по шаблону на каждый тип справочника
- Не поддерживается сценарий Post-Redirect-Get
Решение с использованием ChildAction + MvcExtensions
Я решил, усовершенствовать последнее решение и применить опыт использования ActionFilter, и теперь, с версии 2.5.0-rc8000 в MvcExtensions поддерживаются выпадающие списки “из коробки”. Были добавлены методы расширения, позволяющие указывать, что для отображения данного поля модели необходимо вызвать ChildAction. Также был добавлен SelectListActionAttribute
, который занимается обслуживанием метода, предоставлюящего возможнные значения для выпадающего списка. Поддерживается Post-Redirect-Get
Модель:
public class Movie {
public int GenreId { get; set; }
}
Метаданные:
public class MovieMetadata : ModelMetadataConfiguration {
public MovieMetadata {
Configure(movie => movie.GenreId).RenderAction("List", "Genres");
}
}
Контроллеры:
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);
}
}
Плюсы, по сравнению с предыдущими решениями
- Устранено дублирование обслужвающего кода
- Используется единый шаблон
- MultiSelect “из коробки”
- Поддерживается сценарий Post-Redirect-Get
Вместо заключения.
Для меня, как одного из разработчиков MvcExtensions варианты с использованием этой библиотеки предпочтительнее.
Пример кода для варианта с ViewBag / ViewData + MvcExtensions здесь: http://github.com/MvcExtensions/Core/tree/master/samples
Пример кода для варианта с ChildAction + MvcExtensions здесь: http://github.com/hazzik/DropDowns
*magic-strings легко побеждаются, использованием констант, и по-этому для меня в данном контексте предпочтительней, чем dynamic
PS: Возможности MvcExtensions для расширения старого доброго ASP.NET MVC просто безграничны.