Pull to refresh

Comments 60

Несколько сложноваты имена для ViewModels. Достаточно было бы назвать так же (файлы), как и View. Для данного примера приведеный код, как мне кажется, избыточен. Неудачный Вы выбрали пример.

А последним хочется отметить: т.к. вы вначале написали про использование всеми подряж ViewData & ViewBag, то это Вас вынудило в конце использвать TempData, правильно? :)
Хотя, в случае с TempData я не прав, пожалуй :)
Отчасти правы, для случая с TempData[«Message»] я обычно использую обёртку из базового класса контроллера: SetTempMessage(..), а в самом базовом классе модели есть свойство TempMessage, которое используется в Layout, чтобы показать сообщение.

Но опять же, это история другого романа.
В контексте того, что все модели находтся в одном пространстве имён (NameSpaceProvider=False для их директорий), то имена различаются до основания. Можно убрать ViewModel и оставить Model — на вкус, суть не меняется.

Конечно для такой формы подход избыточен, но в жизни мало встречается формы из двух полей. О них и речь. Не буду же я здесь размещать килобайты разметки для повышения наглядности примера.
Но ведь если формы на сайте как правило одинаковы по стилю и html коду, почему бы не строить всю форму целиком из классов ViewModel, вместо расстановки однотипных Html.Label Html.TextBox. Хотелось бы увидеть в будущем статью об этом :-)
Унификация всех форм — это конечно утопия.
Но создание Html.InputFor(m=>m.Field), где сам label, разные звёздочки (обязательное поле) и инпут строится целиком по метаданным модели — это вполне практичный подход.

О плюсах/минусах такого подхода у себя в блоге как-то писал Jimmy Bogard. Впрочем, там много интересных заметок на тему MVC есть, среди прочего: How we do MVC
Если формы действительно стандартизированы, и для особых исключений, в виде аннотаций к ViewModel подписаны нужные UIHint-ы, и созданы EditorTemplates, то вполне. Это можно делать уже сейчас.
+1. Очень хороша книжка Стивена Сандерсона “Pro ASP.NET MVC 2 Framework”. Благодаря тому, что я начал знакомство с MVC именно с неё, мне удалось избежать многих ошибок, часть из которых вы здесь упомянули. После 4 месяцев «пост-релизной» поддержки проекта я ни разу не жалею, что сразу сделал всё по-уму :)
Не трогайте Request.IsAuthenticated в ваших View, для этого есть модель.

Покажите, пожалуйста, как вы предлагаете бороться с использованием IsAuthenticated во вьюшках, кроме банального пробрасывания этого поля и такого же использования как и то, с чем боремся.
Не делайте View слишком умными. Избегайте глобальных контекстов.
Я не настолько ортодоксален, как предлагают некоторые, чтобы избегать в них @if.
Но с точки зрения View, там где вы используете IsAuthtenticated, скорее всего должно быть свойства вроде [bool]Model.ShowUserControls и другие — смотрите на это со стороны View.

Однажды, вы захотите использовать этот же шаблон к примеру генерирующий ту же страницу в режиме для администратора: View this page from USER1 perspective. И вуаля — все ваши шаблоны это поддерживают. Это только 1 конкретный пример почему данные должны быть явными, а шаблоны глупыми.

Именно контроллер должен позаботиться о том, чтобы View была предоставленна необходимая (не больше) для него модель собранная из данных, контекста и т.п.

Я вполне понимаю что шаблоны должны быть глупыми и всю концепцию MVC. Но всегда есть мелкие проблемы с которыми неудобно бороться через похожее выпиливание лобзиком по вазелину.

Простой пример — надо показать залогиненому пользователю один текст, незалогиненому другой. Верстка — разная ( т.е. передать текст из контроллера не получится, передавать с версткой — глупости). Пилить 2 контрола на каждую простую вьюшку — ну тоже странновато.
Максимум что стоит делать для изолированности — пробрасывать значение через модель.

Проблемы с «однажды я захочу...» я бы предпочел отложить на то время, когда захочу — пока такой вопрос не стоит — я не решаю его (преждевременная оптимизация… ). К тому же в вашем примере проблема решается имперсонацией, а не запретом Request.IsAuthenticated.

Ну т.е. я искренне хотел бы увидеть практический пример как надо решать эту проблему и обоснование — почему надо решать именно так. А то получается догматизм какой-то — «нельзя» — «почему нельзя» — «нарушаешь атсральные заповеди»… Если у вас есть решение — приведите пожалуйста.
Всега стараюсь выступать с практических позиций.

Если вам в конкретном View удобно пробросить IsAuthenticated в модель — делайте это.
Если в конексте использования, вы понимаете, что IsAuthenticated не уместно, а должно быть (со стороны View) ShowUserControls — то пробрасывайте IsAuthenticated в виде ShowUserControls.
Только не заставляйте View _знать_ что IsAuthenticated всегда равно ShowUserControls. Потому что тогда вам самому придётся это тоже запомнить, а такие мелочи копятся.

Не надо ударсять в крайности — надо искать рациональное зерно, и применять только если понимаешь зачем.

И это не преждевременная оптимизация, это Prefactoring =)
«Только не заставляйте View _знать_ что IsAuthenticated всегда равно ShowUserControls. Потому что тогда вам самому придётся это тоже запомнить, а такие мелочи копятся.»

Это точно! Мы на практике рассматриваем эту вещь как часть AOP и используем для этого ViewBag, атрибуты контроллера, которые добавляют это во ViewBag и ViewHelper-ы для непосредственного использования.

Разумеется, если IsAuthenticated имеет _прямое и непосредственное_ отношение к текущему View — оно находится во View-модели (поскольку, больше не является аспектом, а является частью конкретной модели, которая отображается).
«Верстка — разная ( т.е. передать текст из контроллера не получится, передавать с версткой — глупости). Пилить 2 контрола на каждую простую вьюшку — ну тоже странновато.»

Можете чуть-чуть уточнить пример?
Для неаутентифицированного пользователя — описание общих сценариев использования
Для аутентифицированного — подробное описание каждого сценария с несколькими quick-start кнопками\ссылками.

Как-то так.
Я в таких случаях пользуюсь RenderAction — в соответствующем контроллере есть логика, которая проверяет IsAuth (значение берется из входного параметра с использованием соответствующего Binder-а), а затем контроллер выдает один из двух контролов. И ничего, что они простые, зато следуют SRP. Простота во вью еще важнее, чем в исполняемом коде.
Первое правило — View не должно зависеть от Request, как кстати, и контроллер.

Вы можете включить свойство IsUserAuthenticated в исходящую View-модель (для этого зачастую приходится создавать layer-supertype, то есть базовую ViewModelBase), или же использовать для этого ViewBag (который весьма разумно использовать для таких «побочных» вещей, но не рекомендуется для передачи основных данных — для этого есть View-модель).
А зачем прямо уж на каждую страницу создавать ViewModel? Я правда не вижу выгоды. У вас же есть домен, в нем есть User, которого с тем же успехом можно использовать во View. И в нем же можно прописать правила валидации. Особенно если используется CodeFirst модель.

То есть я хочу сказать, что упор надо делать на домен, а не на ViewModel, тогда и работы меньше придется делать и модель будет очевидней.
Первоначально да. Но сущетсвенная проблема заключается в том, что рано или поздно правила валидации выходят за рамки сущностей модели, иногда противоречит им, и вы начинаете размазывать валидацию по проекту, часто дублировать её, делать исключения то там то там.

Строго говоря — вы проверяете _форму_, а не объект, поэтому валидация формы — это проверка состояния формы, а не связанных сущностей, как вырожденный случай.

Кстати, самый удачный пример реализации валидации — на мой взгляд реализовано в Python FormEncode. Где валидатор — это объект со своей гибкой структурой, произвольной вложенность, и заодно ModelBinder (в понятиях MVC).
А как это делает FormEncode в Python?

Насчет ASP.NET MVC — мне очень НЕ нравится, что там на архитектурном уровне смешали валидацию и Model-binding, и то, что DefaultModelBinder всегда проводит валидацию, даже когда это не нужно. Но это пол-беды — ведь есть еще domain-level-валидация (построенная, например, на FluentValidation), и приходится серьезно поднапрячься, чтобы поддерживать ModelState в нормальном состоянии (чтобы не выводить лишних ошибок, и чтобы при этом были выведены все ошибки формы за раз).

На мой взгляд, это можно было сделать порядком проще, и чтобы не пришлось заморачиваться с ModelValidatorProvider'ами. Нужно было сделать DataAnnotations менее навязчивыми, что ли.
Это немного оффтопик. Но всё равно.

Для каждой формы создаётся схема. Эти схемы могут комбинироваться, в том числе в коллекции для форм и много чего интересного.

>>> class Registration(formencode.Schema):
... first_name = validators.String(not_empty=True)
... last_name = validators.String(not_empty=True)
... email = validators.Email(resolve_domain=True)
... username = formencode.All(validators.PlainText(),
... UniqueUsername())
... password = SecurePassword()
... password_confirm = validators.String()
... chained_validators = [validators.FieldsMatch(
... 'password', 'password_confirm')]


А дальше атрибутом (можно и в коде) у метода контроллера просто:
@validate(schema=Registration())

В общем случае создаётся набор базовых валидаторов — для различных случаев, и они комбинируются (учитывая к примеру множетсвенное наследоание в Python). Это даёт огромный плюс в переиспользовании и унификации форм.

В практике — очень практиченое решение. Кроме того, позволяет сделать валидацию для _любой_ формы с какой угодно вложенностью и хитростями, не прибегая к сделкам с совестью.

formencode.org/Validator.html

Да, и правда здорово. Вот в ASP.NET MVC надо было по-умолчанию сделать валидацию с помощью таких атрибутов, чтобы где надо их и указывать. Насчет гибкости валидатора — в .NET есть тот же FluentValidation (прекрасная вещь), но DefaultModelBinder «из коробки» юзает DataAnnotations.
Это в примерах, о которых автор говорил с самого начала так делается. И поначалу кажется очень простым. А потом, на практике, оказывается, что между Domain Model и Presentation Model более, чем прилично различий. Так что отнюдь не с тем же успехом.

Кроме того, поскольку User, как класс модели домена, может быть прикреплен к сессии ORM, это может позволить делать во View то, чего там делать не следует.

ViewModel, возможно, придется создавать не на каждую страницу, но на большинство. Самая засада — mapping из доменной модели во ViewModel, и обратно. Но и эта проблема решается с помощью AutoMapper'а.

Вообще, считайте ViewModel — это как контракт между контроллером и View.

P.S. Валидация — это вообще отдельная большая история. С учетом того, что существует множество видов валидации — структура сильно зависит от конкретной ситуации.
Не расскажете, что такое AutoMapper?
AutoMapper — отличная утилита, позволяющая на базе заранее заданных правил трансформировать одну модель в другую (например, создавать проекцию domain-модели во ViewModel — один из наиболее частых сценариев).

Позволяет избежать целой массы рутинного кода в контроллерах или сервисах. Однако, использовать нужно также с умом — каждый раз думать, как, например, все это дело сопрягается с Fetching-стратегиями вашей ORM (особенно, если у вас одна и та же View-модель используется в нескольких View).
Да, я понимаю что домен может разойтись с отображением. И тогда у вас будет 2 варианта: либо делать DTO, либо пересматривать домен. Мне кажется, что лучше изменить домен.
Ну нет, в корне не согласен. Domain не должен зависеть от View. В этом и смысл домена — это логика, которая не зависит от представления и может быть использована в разных приложениях (например, в MVC-App, в WCF-сервисе, и где-то еще). Как только вы свяжете домен с MVC-представлением, все остальные вещи отпадают.
Разумеется не должен, я этого и не говорил. Я сказал что если вы не можете смаппить домен на вью, то есть 2 варианта решения, пересмотреть домен, или делать костыли в виде ViewModel, которые по-сути раздувают домен со всех сторон.
Ну нифига себе, костыли :) ViewModel — это самый что ни на есть строгий контракт между контроллером и View, совершенно предсказуемый и тестируемый. А вот «пересмотреть домен из за View» — вот это самый-самый костыль.
Вы должно быть подумали, что я предлагаю добавлять в домен такие вещи как, например, IsUserLocationVisible, которые служат исключительно для того, чтобы определить показывать местоположение юзера на сайте или нет. Если добавить это поле в домен, то конечно же домен начинает зависить от View. И это будет костылем. Вы бы, я так полагаю, добавили это поле во ViewModel.

Я же предлагаю следующее, формализовать то бизнес-правило, на основании которого определяется показывать юзера на сайте или нет и внести его в домен. Допустим нам нужно показывать местоположение юзера, только если он дружелюбный, тогда в домене это будет поле IsFriendly с некой логикой внутри. Которое никаким образом не связано с UI. И на основании уже этого поля определять во View показывать его или нет.

В итоге, я хочу сказать, что вместо того чтобы добавлять поле IsUserLocationVisible во ViewModel лучше добавить в домен IsFriendly.
Это само собой, поскольку это так называемый «domain concern» — это действительно никак не связано с View. Ровно как не связано и с необходимостью наличия/отсутствия ViewModel-ей, поскольку требование того, что пользователю должен быть «Friendly» или «Non-Friendly» продиктовано не UI, а предметной областью, концепцией проекта.

Тут речь скорее о том, что от ViewModel-ей никуда не деться, если domain становится достаточно сложным (а чем он сложнее — тем больше различий между domain- и presentation-моделями).

P.S. В вашем примере, во ViewModel в любом случае не было бы никаких «IsUserLocationVisible» — определить «визибл» она или нет — это логика View, и не должна находиться в контроллере. Во ViewModel было бы то же самое свойство IsFriendly.
Тогда уж EmitMapper, который в несколько раз быстрее
А теперь вопрос- зачем? :) Зачем на каждый чих создавать ViewModel, даже для простых форм с несколькими полями? А для страниц с простыми сообщениями- тоже создавать? Ок, прикинем- стандартный проект, скажем 30 форм. Получается 30 ViewModel, куча мапперов, куча лишних файлов и т.д. Это отжирает кучу времени при нулевых бонусах. Архитектура ради архитектуры. Код ради кода. На самом деле в жизни нужно использовать комбинированный подход- где это имеет смысл, там использовать Presentation Model, где нет- не использовать. KISS.
P.S. Я в курсе про AutoMapper, но сути это не меняет.
От части я с вами согласна, но вот беда, когда это все тестируешь и рефакторишь, ту пусть будет больше файлов, чем отсутствие типизации.
На самом деле, и да, и нет.

Я одно время сам был сторонником исключительно строгих контрактов. Однако, в контексте того же ASP.NET MVC оказалось очень предпочтительно использовать ViewBag для передачи дополнительных данных во View (что-то вроде AOP). С одной стороны — loose-contract, а с другой — он не имеет отношения к основному View, не засоряет его ViewModel, и не мешает всей strong-type-овости всех основных View.

Для основных View, все же, на практике оказалась более предпочтительной модель со строгой типизацией View. Количество ViewModel-ей перестает волновать, когда они созданы по определенному соглашению, лежат в нужном месте и хорошо структурированы.
Полностью согласен. Еще обычно в таких проектах на 30 форм получается куча интерфейсов, репозиториев и сервисов, которые в итоге сводятся к простому селекту в базе данных.
когда даже над одной страницей работают больше 2х человек ( даже 2 — уже неудобно) — нестрогая типизация ViewModel приводит к головной боли — лазить и смотреть как же твой коллега назвал то, куда он данные Х сложил. А еще есть опечатки при верстке (верстальщик же не должен быть программистом, он сделал все правильно, это у вас не работает ) и рефакторинг такого — тот еще ад.
В общем я тоже всеми конечностями за строгую типизацию, ну или хотя бы за dynamic который в compile time проверяется…
Поясните, пожалуйста, мысль, каким образом dynamic может проверятся в compile time.
Никак не проверяется, видимо ошибся товарищ. С dynamic-типами работает DLR (используя позднее связывание).
Имелось ввиду использование ExpandoObject как вот тут (ноги растут отсюда)

Честно скажу — сам еще это не пробовал — пока некогда. Но как я понял — это все проверяется в Compile Time, по типу того же Clay part 1,part 2
Нет, нет, все проверяется в runtime (связывание происходит совершенно точно в DLR, с использованием DynamicMetaObject).
dynamic — это loose-контракт, и никак не альтернатива и не «хотя бы» по сравнению со строгой типизацией. Но использовать можно, просто для особых сценариев.
Хорошая статья и не менее хорошая к ней дискуссия.
1andy, если не будите писать дальше статьи про MVC, то я, пожалуй, буду являться к вам во сне, чтобы внушить вам это.
Мне нравится такой подход (максимально сокращённо, насколько позволяет формат комментариев)

Интерфейс

public interface IAccountRegisterForm
{
string Name { get; set; }
string State { get; set; }
void Validate();
}

Модель

public class AccountRegisterForm: IAccountRegisterForm
{
[Required]
public string Name { get; set; }
[Required]
public string State { get; set; }

public void Validate(){}
}

Вьюшка

@model IAccountRegisterForm

@Html.TextBoxFor(m => m.Name)


Тогда случай, описанный в статье, становится просто частным случаем. А одну и ту же вьюшку можно использовать для отображения общих данных различных моделей — если они наследуют один и тот же интерфейс.

В данном случае примером может быть, регистрация пользователя и администратора, если этот процесс несколько отличается валидацией и набором полей.
А как использовать один интерфейс, если поля различаются?
На одной из страниц:

@model AdminRegistrationModel

@Html.Partial(«CommonRegisterForm», Model)
@Html.TextBoxFor(m => m.AdminSpecifiField)

На другой
Случайно отправил неполный комментарий.

На другой

@model UserRegistrationModel

@Html.Partial(«CommonRegisterForm», Model)
@Html.TextBoxFor(m => m.UserSpecificField)

Разумный подход и хороший вариант для таких вот спорных моментов — как сохранить строгий контракт и не привязаться к слишком конкретному типу.
Тогда получается, что в одном и том же интерфейсе у вас будет и UserSpecificField и AdminSpecifiField. Что противоречит одному из принципов S.O.L.I.D, а именно Interface segregation principle.
Эм, нет. UserSpecificField и AdminSpecifiField — свойства разных моделей.

public class UserRegistrationModel: IAccountRegisterForm
{

String UserSpecificField{get; set;}
}

public class AdminRegistrationModel: IAccountRegisterForm
{

String AdminSpecifiField{get; set;}
}

За регистрацию пользователя и администратора отвечают различные страницы с различными моделями. Но общие свойства этих моделей описаны интерфейсом, и для отображения этих общих свойств используется общая вьюшка.
Да, я вначале не понял…
Тогда это действительно неплохое решение, если подсовывать этот интерфейс в PartialView.
В связи со всем прочитаным у меня возникло 2 вопроса:

1. А как же локализация и ресурсы?

Предположим локализация берется через интерфейс IResource с функцией string GetString(string)
Во вьюшке есть несколько локализованых строк.
— Вроде как в модели локализации делать нечего (или я не прав)?
— Или как вариант использовать расширение к Html. Не посчитается ли тогда, что вьюха шибко поумнела?

2. К вопросу о LayoutModel. А что делать, если в контроллере написано как-то так (стырено из генеренного AccountController):
        // **************************************
        // URL: /Account/LogOn
        // **************************************
        public ActionResult LogOn()
        {
            return View();
        }

Тоесть другими словами — модель = null, но при этом вьюха принимает вполне строгий тип? Хотя подозреваю, что рефлекшеном можно вытащить что угодно, но имхо это не самый лучший вариант.
Моё мнение по первому вопросу — это тот случай, когда кодогенерация удобна и уместна. Впрочем, это вопрос не столько программирования как такового, а, скорее, предпочтения определённого набора инструментов. Я стараюсь избегать привязки к каким-либо ключам, а всё строго типизировать, в таком случае решарпер и студия значительно помогают.
Потом не стесняюсь писать прямо во вьюшке что-то вроде
@MyApplication.Resources.Localization.Common.LogIn

Что касается второго вопроса — то я свято верю, что в данном случае именно вьюшка задаёт условия и соглашения, при которых её можно использовать. И если контроллер не в эти соглашения выполнить — нужно изменять контроллер, а не вьюшку.
А вот как уважаемая компания относится к тому, чтобы папки организовывать не по принципу разделения классов на роли (контроллеры сюда, модели туда), а по фичам? Я тут попробовал, и все гораздо удобнее стало. Работаешь над какой-нибудь фичей, все файлы в одном месте, под рукой. Кроме вью, за ним все время лазаешь в другую папку, и это раздражает. Но это можно вылечить созданием своего ViewEngine.
Примерчик по фичам можно?
Папка Profile, там лежит ProfileController, ProfileQuery, ProfileEditModel, ProfileViewModel
Общественность в моем лице относится к этому крайне положительно. Удобство работы возрастает в разы, структура улучшается и т.д.
Мда, вот я тормоз… :)
Only those users with full accounts are able to leave comments. Log in, please.