Pull to refresh

Пять способов показать выпадающий список в Asp.Net MVC, с достоинствами и недостатками

Reading time 7 min
Views 45K
В большинстве интродукций к Asp.Net MVC рассказывается о том, как красиво и просто организовать привязку модели к простым полям ввода, таким, как текстовое или чекбокс. Если ты, бесстрашный кодер, осилил этот этап, и хочешь разобраться, как показывать выпадающие списки, списки чекбоксов или радиобаттонов, этот пост для тебя.

Постановка задачи


У нас есть база фильмов, про которых мы знаем название и жанр. Хочется показать редактор информации о кино, у которого жанры выбираются из списка. Проблема в том, что наш View должен иметь данные как относящиеся к собственно фильму, так и к списку жанров. Причем, данные о фильме относятся к тому, что мы показываем, а список жанров — к тому, как мы редактируем нашу информацию.

Способ первый. The Ugly.


Мы можем передавать данные о фильме через контроллер, а список жанров наш вью будет извлекать сам. Понятно, что это нарушение всех мыслимых принципов, но чисто теоретически такая возможность имеется.

Имеем такие классы моделей:
	public class MovieModel {
		public string Title { get; set; }
		public int GenreId { get; set; }
	}

	public class GenreModel {
		public int Id { get; set; }
		public string Name { get; set; }
	}


Метод контроллера, как в руководствах для начинающих:
        public ActionResult TheUgly(){
        	var model = Data.GetMovie();
        	return View(model);
        }


Здесь Data — это просто статический класс, который нам выдает данные. Он придуман исключительно для простоты обсуждения, и я бы не советовал использовать что-либо подобное в реальной жизни:
	public static class Data {
		public static MovieModel GetMovie() {
			return new MovieModel {Title = "Santa Barbara", GenreId = 1};
		}
	}


Приведем теперь наш ужасный View, точнее, ту его часть, которая касается списка жанров. По сути, наш код должен вытащить все жанры и преобразовать их в элементы типа SelectListItem.
 <%
var selectList = from genre in Data.GetGenres() 
      select new SelectListItem {Text = genre.Name, Value = genre.Id.ToString()};
            %>
                <%:Html.DropDownListFor(model => model.GenreId, selectList, "choose") %>


Что же здесь ужасного? Дело в том, что главное достоинство Asp.Net MVC, на мой взгляд, состоит в том, что у нас есть четкое разделение обязанностей (separation of concerns, или SoC). В частности, именно контроллер отвечает за передачу данных во вью. Разумеется, это не догма, а просто хорошее правило. Нарушая его, Вы рискуете наворотить кучу ненужного кода в Ваших представлениях, и разобраться через год, что к чему, будет очень непросто.

Плюс: простой контроллер.
Минус: в представление попадает код, свойственный контроллеру; грубое нарушение принципов модели MVC.
Когда использовать: если надо быстро набросать демку.

Способ второй. The Bad.


Как и прежде, модель передаем стандартным образом через контроллер. Все дополнительные данные передаем через ViewData. Метод контроллера у нас вот:
		public ActionResult TheBad() {
        		var model = Data.GetMovie();
			ViewData["AllGenres"] = from genre in Data.GetGenres() 
                               select new SelectListItem {Text = genre.Name, Value = genre.Id.ToString()};
			return View(model);
		}


Понятно, что в общем случае мы можем нагромоздить во ViewData все, что угодно. Дальше все это дело мы используем во View:
<%:Html.DropDownListFor(model => model.GenreId, 
                                        (IEnumerable<SelectListItem>) ViewData["AllGenres"], 
                                        "choose")%>


Плюс: данные для «что» и «как» четко разделены: первые хранятся в модели, вторые — во ViewData.
Минусы: данные-то разделены, а вот метод контроллера «перегружен»: он занимается двумя (а в перспективе — многими) вещами сразу; кроме того, у меня почему-то инстинктивное отношение к ViewData как к «хакерскому» средству решения проблем. Хотя, сторонники динамических языков, возможно, с удовольствием пользуются ViewData.
Когда использовать: в небольших формах с одним-двумя списками.

Способ третий. The Good.


Мы используем модель, которая содержит все необходимые данные. Прямо как в книжке.
	public class ViewModel {
		public MovieModel Movie { get; set; }
		public IEnumerable<SelectListItem> Genres { get; set; }
	}


Теперь задача контроллера — изготовить эту модель из имеющихся данных:
public ActionResult TheGood() {
	var model = new ViewModel();
	model.Movie = Data.GetMovie();
	model.Genres = from genre in Data.GetGenres() 
		select new SelectListItem {Text = genre.Name, Value = genre.Id.ToString()};
	return View(model);
}


Плюс: каноническая реализация паттерна MVC (это хорошо не потому, что хорошо, а потому, что другим разработчикам будет проще врубиться в тему).
Минусы: как и в прошлом примере, метод контроллера перегружен: он озабочен «что» и «как»; кроме того, эти же «что» и «как» соединены в одном классе ViewModel.
Когда использовать: в небольших и средних формах с одним-тремя списками и другими нестандартными элементами ввода.

Способ четвертый. The Tricky.


Есть еще одна «задняя дверь», через которую можно доставить данные во View — это метод RenderAction. Многие брезгливо морщатся при упоминании об этом методе, поскольку, по классике, View не должен знать о контроллере. Лично для меня это (да простят меня боги) хороший аналог UserControl-ов из WebForms. А именно, возможность создать некий элемент, практически (если не считать параметров вызова этого метода) независимый от всей остальной страницы.

Итак, в качестве модели мы используем MovieModel, и метод контроллера такой же, как и TheUgly. Но у нас теперь появится новый контроллер, и метод для отрисовки дропдауна в нем, а также partial с этим дропдауном. Этот partial мы сделаем макимально гибким, чтобы им пользоваться и в других случаях, назовем Dropdown.ascx, и поместим его в папку Views\Shared:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IEnumerable<SelectListItem>>" %>
<% =Html.DropDownList(ViewData.ModelMetadata.PropertyName, Model, ViewData.ModelMetadata.NullDisplayText)%>


Что касается метода, который рендерит этот вью, то тут есть пара хитростей:
	public class GenreController : Controller{
		public ActionResult GetGenresDropdown(int selectedId) {
			ViewData.Model = from genre in Data.GetGenres() 
				select new SelectListItem 
					{ Text = genre.Name, 
					Value = genre.Id.ToString(), 
					Selected = (genre.Id == selectedId) };
			ViewData.ModelMetadata = 
				new ModelMetadata(
					ModelMetadataProviders.Current, 
					null, 
					null, 
					typeof (int), 
					"GenreId")
				{NullDisplayText = "choose"};
			return View("Dropdown");
		}
	}


Во-первых, мы теряем тут автоматический выбор нужного значения, поэтому нам нужно вручную устанавливать свойство Selected у SelectListItem. Во-вторых, если мы передаем какие-то нетривиальные метаданные в наш View, то мы должны сначала установить модель, а потом уже метаданные. В противном случае метаданные автоматически создадутся на основе модели. По той же причине мы не должны писать return View(model). Ну и собственно метаданные нужны для того, чтобы определить название свойства и текст по умолчанию (NullDisplayText). Без последнего, кстати, можно обойтись.

Наконец, метод контроллера вызывается из главного View:
<% Html.RenderAction("GetGenresDropdown", "Genre", new {selectedId = Model.GenreId}); %>


Плюсы: разделение ответственности на уровне контроллера: MovieController отвечает за данные о фильме, GenreController — за жанры. На уровне View у нас тоже полная победа: главный вью существенно упростился, а детали реализации выбора жанра отправились во вспомогательный. Здесь, кстати, есть некая аналогия с упрощением длинного метода и выносом части кода во вспомогательный метод.
Минусы: больше кода, сложнее структура.
Когда использовать: когда главный View становится достаточно большим, и дропдауны появляются у нескольких полей, либо когда выбор жанра необходимо использовать на нескольких страницах.

Способ пятый. The Smart.


Когда вся нетривиальная часть по организации ввода (или выбора) нужного значения отделена от главного View, возникает желание как-то резко это главный View упростить. И очевидное тут решение — использовать Html.EditorForModel(). Теперь за выбор способа отображения того или иного поля отвечают метаданные класса модели. Единственная проблема — встроенными средствами мы можем лишь заставить движок вызвать в нужном месте RenderPartial(), но не RenderAction(). Поэтому придется создать Partial View, который не несет никакой нагрузки, кроме как вызвать соответствующий RenderAction. (Правда, если нам нужно будет кастомизировать редактор поля, то мы будем изменять именно этот вью, а DropDown.ascx оставим нейтральным.)

Итак, в папке \Views\Movie\EditorTemplates создаем Partial View под названием GenreEditor.ascx. Модель у него будет того же типа, что и свойство GenreId, которое мы редактируем, т.е., int. Сам вью будет содержать только вызов RenderAction:
<% Html.RenderAction("GetGenresDropdown", "Genre", new {selectedId = Model}); %>


Чтобы наш вью использовать, надо в модель добавить нужный атрибут к свойству GenreId:
[UIHint("GenreEditor")]


Плюсы: те же, что и в предыдущем примере, но при этом мы существенно упростили наш главный View.
Минусы: нам пришлось изготовить лишний View, который (пока) не несет никакой осмысленной нагрузки (но, возможно, позволит кастомизировать редактирование поля, например, добавить подсказку). Еще один, более важный, минус — труднее кастомизировать общую структуру формы (например, одно поле показать в левой части, а остальные — в правой). Если требуется глобальная кастомизация (например, везде использовать таблицы вместо дивов), можно изготовить свой шаблон для Object.
Когда использовать: когда полей много, показываются они более-менее страндартным образом, и часто вносятся изменения в список полей.

Теперь, если у нас появляются дропдауны с категориями, рейтингом и т.д., мы сможем по-прежнему использовать Dropdown.aspx (как и для других дропдаунов), но нам придется писать аналогичные методы контроллеров и partials, аналогичные нашему GenreEditor. Можно ли это как-то оптимизировать? Во-первых, можно изготовить базовый Generic Controller, и отправить наш метод туда. Во-вторых, можно каким-нибудь образом передать в наш partial название связанного класса («Genre») (например, через атрибут DataType), и сконструировать вызов RenderAction соответственным образом. Теперь вместо GenreEditor мы будем иметь универсальный редактор для выбора из выпадающего списка. В итоге мы получим, что добавление новых справочников никак не увеличит количество необходимого кода — надо лишь соответствующим образом проставить атрибуты у модели. В примерах этого нет, но читатель лекго это реализует сам.

А как еще можно?


Единственный по-настоящему отличающийся от приведенных здесь способ, который пришел мне в голову — сделать наполнение дропдауна через AJAX. Плюс здесь очевиден: данные передаются независимо от html и могут быть использованы в других местах другим способом. Я слышал, что такая штука очень просто реализуется в Spark, но у меня руки еще не дошли попробовать.

А зачем, вообще, с этим париться?


С примерами всегда одна беда: они должны быть достаточно простыми, чтобы было понятно, о чем речь, но тогда непонятно, зачем такое сложное решение. Если Вас по-прежнему волнует этот вопрос, перечитайте пункты «когда использовать».

Разумеется, прежде, чем пользоваться одним из этих решений, лучше прикинуть отношение сигнал/шум: небольшие проекты поддерживать проще, когда там меньше кода, классов, файлов и т.д., в то время, как в больших проще иметь дело с большим количеством элементов, каждый из которых четко сфокусирован на решении своей маленькой задачи. К счастью, все это довольно неплохо рефакторится: мы можем начать с одного из первых решений, и, по мере усложнения нашей формы, переходить к более продвинутым вариантам.

Что еще? Ах да, исходники можно взять здесь. Наслаждайтесь!
Tags:
Hubs:
+22
Comments 18
Comments Comments 18

Articles