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

    В большинстве интродукций к 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, но у меня руки еще не дошли попробовать.

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


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

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

    Что еще? Ах да, исходники можно взять здесь. Наслаждайтесь!
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 18
    • +1
      аа спасибо )))
      • 0
        Я в шоке.
        Я боюсь спросить: а существует ли способ в Asp.NET MVC вывести список кастомным html-шаблоном, при этом написав менее 100 строк кода?
        • +5
          Коллега, список выводится одной строчкой:
          <%:Html.DropDownListFor(model => model.GenreId, selectList, «choose») %>

          Этот пост про то, как подцепить _данные_ к этому списку. Конечно, никто не запрещает прямо из View обратиться к базе с SQL запросом. Наверное, при этом и строк будет поменьше.
        • +2
          Вы вынесли мне мозг:(
          Что мешает доработать способ 3, сделав умную модель для вью, которая достает все нужные списки в конструкторе? И код не будет перегружен лишними конструкциями, и извращаться никак не надо.
          • +1
            Отлично, это шестой способ!

            Здесь один тонкий момент. Обязательно найдутся разработчики, которые назовут извращением именно Ваш способ. Например, согласно Догме 1, модель пассивна, а все действия совершает контроллер. А согласно Догме 2, модель не имеет права «знать» о доступе к данным.

            Но суть не в том, чтобы не извращаться, а в том, чтобы накидать побольше способов, и чтобы каждый обрадовался хоть одному из них.
            • 0
              Если так хочется следовать всем догмам, можно сделать метод CreateViewModel(...) который будет в одном месте конструировать пассивную модель.
              • 0
                Седьмой способ :) А где этот метод будет сидеть? Не в контроллере, случайно?
                • +1
                  Контроллер. Догма 3 в том, что контроллер не должен заниматься прямым доступом к данным и их обработкой. А вот подготовкой данных к отображению — его прямое дело.
                  • 0
                    Именно так. Контроллер из domain-модели (из сервисов приложения) формирует View-модели, и, получая ViewModel, передать данные обратно в сервисы приложения. Больше он ничего не делает.
              • +1
                «накидать побольше способов, и чтобы каждый обрадовался хоть одному из них»

                На мой взгляд, как раз это один из концептуальных недостатков и самого ASP.NET MVC. Он, в отличие от Rails, не задает какого-то определенного, «правильного» способа, что заставляет каждого изобретать велосипеды по ходу работы.
                Ему не помешало бы быть более «Opinionated».

                А что за догмы?
                • 0
                  Догмы — это, как раз, «правильный» способ. Заглянем к Фаулеру и сделаем, как у него написано.

                  Но ведь у нас разные ситуации. Где-то больше подходит DDD или там CQRS всякое. Где-то DLINQ — и готово! Все вокруг, как правило, борются за «как надо», а я вот решил с пеной у рта доказывать всем, что можно по-разному. Тут все взрослые, каждый сам выберет себе, как ему (и всем на свете) надо.
                  • 0
                    С этим не поспоришь. Я немного другое имел в виду, но тут спорить не о чем.

                    Просто все же есть какой-то наиболее «правильный способ» и для меня это Thunderdome Principle. По сути, как раз некие догмы. Никто не ограничивает воображение, но если есть хороший (или даже очевидно лучший) способ сделать что-то — на мой взгляд стоит того, чтобы внести во фреймворк как способ по-умолчанию, и чтобы упростить его использование, всюду следовать принципу Convention over Configuration.

                    Вы ведь и сами понимаете, что такие способы как The Bad and The Ugly — это скорее «технические возможности», и что-то мне подсказывает, что сами вы их применять не будете даже для «простой формы с одним списком» :) А используете вариант SuperSmart, да еще и в нем, чтоб избавиться от Magic Strings, используете T4MVC, чтоб совсем хорошо стало. Вот и получается, что хороший способ, может и не самый простой, и, хотя и не исключает вариантов, все же, достаточно определенный.
                    • 0
                      Я думаю, это не хороший способ, а просто у нас просто одинаковые вкусы:) Мне кажется, тут все как с музыкой. Вот слушаешь [...], и кажется, ну вот — настоящий шедевр, и только идиоты с испорченным вкусом этого не понимают. А уж мы-то, настоящие ценители, знаем толк!

                      Я бы назвал «лучшим» способ, который дает максимальную производительность и максимальное наслаждение (или, хотя бы, чувство удовлетворения) вот этому конкретному разработчику в этой конкретной ситуации.

                      Например, меня все больше напрягает официально признанный convention о том, что все View должны лежать в одноименной папке.
                      • 0
                        Я тут, пожалуй, соглашусь. ASP.NET MVC по определению не может быть направленным в определенное русло (opinionated), каждый строит на нем небольшой over-framework под себя.

                        А Convention насчет папок View — это как раз пример стандартного «opinion»а :) Это вполне можно поменять, реализовав свой ViewEngine (можно даже унаследовать тот же RazorViewEngine или WebFormsViewEngine и поменять способы инициализации convention-свойств). Впрочем, как по мне — вот это как раз неплохое соглашение. Чем оно не устраивает?

                        Меня вот, кстати, больше напрягает, что по-умолчанию используется ActionInvoker, не дающий возможности возвращать ViewModel из Action'ов контроллеров. Ну и, само собой, то, что многое завязано на HttpContext (включая HtmlHelper'ы), что усложняет тестирование.
                        • 0
                          По поводу тестирования всяких завязанных на HttpContext штук я химичу уже давно: sm-art.biz/Ivonna.aspx
                • 0
                  А можно еще поподробнее про «согласно Догме 1, модель пассивна, а все действия совершает контроллер. А согласно Догме 2, модель не имеет права «знать» о доступе к данным.»?

                  Тут есть какое-то противоречие?
                  • +1
                    Противоречие с тем, что предлагает Kefir в своем первом комменте — модель сама себя обустраивает, сама лезет за данными куда надо. Мой внутренний jeremymiller, прочтя это, скукожился и обмяк.
                    • 0
                      А, это точно. Мой jeremymiller тоже себя очень неуютно чувствует при таких раскладах :)

                      Мне просто показалось, будто между этими двумя догмами само по себе есть какое-то противоречие.

                      ViewModel не должна выполнять задачи контроллера, ActionInvoker'а или ActionResult'а, это совершенно точно.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.