Повторное использование атрибутов проверки в пользовательских ViewModels

Когда я начал использовать xVal для проверки на стороне клиента, я только реализовывал методы действий, которые использовали объекты модели предметной области в качестве модель представления или встроенные экземпляры этих объектов в модели представления.

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

Один (уродливый) обходной путь состоит в том, чтобы иметь скрытое поле ввода в форме для каждого свойства, которое иначе не присутствует в форме.

По-видимому, наилучшей практикой здесь является создание пользовательской модели представления, которая содержит только свойства, относящиеся к представлению, и заполнение модели представления с помощью Automapper. Это намного чище, поскольку я передаю только данные, относящиеся к представлению, но это далеко не идеально, поскольку мне приходится повторять те же атрибуты проверки, которые уже присутствуют в объекте модели предметной области.

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

Есть ли какой-нибудь элегантный обходной путь для этого? Я подумывал взломать исходный код xVal, но, возможно, есть какой-то другой способ, который я до сих пор упускал из виду.

Спасибо,

Адриан

Изменить. С появлением ASP.NET MVC 2 эта проблема больше не связана только с атрибутами проверки, но также относится к атрибутам редактора и отображения.


person Adrian Grigore    schedule 13.01.2010    source источник
comment
Мне тоже это интересно - в итоге я просто проверил пользовательскую модель представления и переместил ее из своей модели предметной области.   -  person Parrots    schedule 13.01.2010
comment
Итак, вы повторяете атрибуты проверки снова и снова?   -  person Adrian Grigore    schedule 13.01.2010
comment
В конце концов, мне не нужно, нет. Обычно у меня нет нескольких страниц (и, следовательно, моделей просмотра), которые могут устанавливать одно и то же значение (скажем, имя для человека). Редкие случаи мне приходится, да.   -  person Parrots    schedule 13.01.2010
comment
К сожалению, в моем проекте это не так. Например, у меня есть бизнес-объект, который изменяется в 6 различных формах.   -  person Adrian Grigore    schedule 13.01.2010
comment
Ctrl + F '16, ничего. Есть ли какие-либо обновления по ответу на этот вопрос в 2016 году?   -  person Worthy7    schedule 26.08.2016


Ответы (5)


Это основная причина, по которой ваши экраны ввода не должны быть тесно связаны с вашей моделью. Этот вопрос на самом деле всплывает здесь, в теге MVC, примерно 3-4 раза в месяц. Я бы обманул, если бы смог найти предыдущий вопрос, и некоторые комментарии здесь интересны. ;)

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

Все варианты обхода неоптимальны. Я работал над этой проблемой для трех проектов, и реализация следующих решений никогда не была чистой и обычно разочаровывающей. Я постараюсь быть практичным и забыть все обсуждения DDD/db/model/hotnessofthemonth, которые ведут все остальные.

1) Множественные модели просмотра Наличие почти одинаковых моделей просмотра нарушает принцип DRY, но я считаю, что затраты на этот подход очень низкие. Обычно нарушение DRY увеличивает затраты на техническое обслуживание, но ИМХО затраты на это самые низкие и невелики. Гипотетически говоря, вы не меняете максимальное количество символов в поле LastName очень часто.

2) Динамические метаданные В MVC 2 есть хуки для предоставления ваших собственных метаданных для модели. При таком подходе вы можете использовать все, что вы используете для предоставления метаданных, исключая определенные поля на основе текущего HTTPRequest и, следовательно, Action и Controller. Я использовал этот метод для создания системы разрешений на основе базы данных, которая обращается к базе данных и сообщает подклассу DataAnnotationsMetadataProvider, чтобы исключить значения на основе свойств, хранящиеся в базе данных.

Этот метод отлично работает, но единственная проблема заключается в проверке с помощью UpdateModel(). Чтобы решить эту проблему, мы создали метод SmartUpdateModel(), который также обращается к базе данных и автоматически генерирует массив exclude string[], чтобы любые недопустимые поля не проверялись. Мы, конечно, кэшировали это из соображений производительности, так что это неплохо.

Просто хочу повторить, что мы использовали [ValidationAttributes] в наших моделях, а затем заменили их новыми правилами во время выполнения. Конечным результатом было то, что поле [Required] User.LastName не проверялось, если у пользователя не было разрешения на доступ к нему.

3) Сумасшедший интерфейс с динамическими прокси-вещами Последним методом, который я пробовал, было использование интерфейсов для ViewModels. Конечным результатом было то, что у меня был объект User, унаследованный от таких интерфейсов, как IAdminEdit и IUserRegistration. И IAdminEdit, и IUserRegistration будут содержать атрибуты DataAnnotation, которые выполняют все проверки, зависящие от контекста, такие как свойство Password с интерфейсами.

Это требовало некоторого хакерства и было скорее академическим упражнением, чем чем-либо еще. Проблема со 2 и 3 заключается в том, что UpdateModel и провайдер DataAnnotationsAttribute нужно было настроить, чтобы узнать об этом методе.

Моим самым большим камнем преткновения было то, что я никогда не хотел отправлять весь пользовательский объект в представление, поэтому в итоге я использовал динамические прокси для создания экземпляров IAdminEdit во время выполнения.

Теперь я понимаю, что это очень специфичный для xVal вопрос, но все пути к динамической проверке, подобные этому, ведут к настройке внутренних поставщиков метаданных MVC. Поскольку все метаданные являются новыми, на данный момент нет ничего такого чистого или простого. Работа, которую вам придется проделать для настройки поведения проверки MVC, несложна, но требует глубоких знаний о том, как работают все внутренние компоненты.

person John Farrell    schedule 01.11.2010
comment
Спасибо за исчерпывающий ответ! Поскольку я написал этот OP в начале этого года, я пробовал варианты подходов 1) и 3), которые вы упомянули. Я согласен, что оба были далеко не идеальными по причинам, которые вы уже описали. Что касается подхода 2: не могли бы вы указать мне какую-либо статью в блоге или аналогичный ресурс, в котором описывается, как это будет сделано? Это не должно быть связано с xVal, так как я использую обычную привязку модели MVC 2 и метаданные. - person Adrian Grigore; 02.11.2010

Мы переместили наши атрибуты проверки на слой ViewModel. В нашем случае это в любом случае обеспечило более четкое разделение проблем, поскольку мы смогли спроектировать нашу модель предметной области таким образом, чтобы она изначально не могла попасть в недопустимое состояние. Например, Date может потребоваться для объекта BillingTransaction. Поэтому мы не хотим делать его Nullable. Но в нашей ViewModel нам может понадобиться выставить Nullable, чтобы мы могли поймать ситуацию, когда пользователь не ввел значение.

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

person Jimmy Bogard    schedule 17.01.2010
comment
Итак, вы повторяете атрибуты проверки для представлений, которые обрабатывают изменения в одних и тех же бизнес-объектах? - person Adrian Grigore; 18.01.2010
comment
Нет, мы не используем атрибуты проверки для бизнес-сущностей. Бизнес-сущности не могут переходить в недопустимое состояние, поэтому нет необходимости помещать в них атрибуты проверки. - person Jimmy Bogard; 18.01.2010
comment
Проверка — это проблема домена, которая меняется вместе с доменом, а не с пользовательским интерфейсом. Размещение этой доменной логики в пользовательском интерфейсе стирает это разделение. В лучшем случае ViewModels должны проверять простые требования, которые не зависят от домена и не меняются вместе с доменом. Например, обязательные поля и форматирование электронной почты. Все остальные проверки должны приниматься (в большинстве случаев) строго на стороне сервера в домене, без автоматически созданной поддержки на стороне клиента. - person G-Wiz; 23.01.2010
comment
@gWiz: я думаю, он имел в виду, что объекты домена обеспечивают свою достоверность либо с помощью безопасности типов во время компиляции, либо с помощью проверки ввода в реальном времени, в отличие от принятия неверных данных и предоставления способа получить ошибки. Я склонен согласиться с таким подходом. Существует тонкая разница между проверкой ввода и проверкой домена, о которой часто забывают в MVC. - person Aaronaught; 23.01.2010
comment
Называете ли вы это проверкой ввода или проверкой домена, суть в том, чтобы предотвратить сохранение неверных данных в домене. Я хочу сказать, что вы хотите сохранить сложную логику, специфичную для домена, но общую для всех пользовательских интерфейсов в домене. Я понимаю цель недопущения того, чтобы объекты домена когда-либо попадали в недопустимое состояние. Это стандартно поддерживается в ASP.NET MVC через DeafultModelBinder.SetProperty, который перехватывает исключения, возникающие при попытке установить свойство в ModelState. Не уверен, что дополнительный слой модели представления был бы лучше. - person G-Wiz; 23.01.2010
comment
Я предпочитаю использовать методы для обновления элементов, а не вкладывать много логики в установщики свойств. Это приводит к тому, что порядок операций (StartDate должен быть меньше EndDate, но что, если сначала установить EndDate?), который становится все более и более непрозрачным. Проверка, как правило, сосредоточена на конкретных командах, а не на объектах. Состояние, действительное в одном контексте, может быть недопустимым в другом. - person Jimmy Bogard; 25.01.2010

Если вам гипотетически навязывают ViewModels, я рекомендую применять только требования, не зависящие от предметной области. Это включает в себя такие вещи, как «требуется имя пользователя» и «электронная почта отформатирована правильно».

Если вы дублируете проверку моделей предметной области в моделях представления, то вы тесно связываете предметную область с пользовательским интерфейсом. При изменении проверки домена («можно применять только 2 купона в неделю» становится «можно применять только 1 купон в неделю»), необходимо обновить пользовательский интерфейс. Вообще говоря, это было бы ужасно и пагубно для ловкости.

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

Если вы не можете предсказать будущее и исключить эти возможности, проверяйте только требования, не зависящие от предметной области.

person G-Wiz    schedule 23.01.2010
comment
Вы описываете именно ту проблему, которая привела меня к написанию этого ОП. Но какое решение вы предлагаете? Что именно вы имеете в виду, говоря только о проверке требований, не зависящих от предметной области? И что вы предлагаете в отношении метаданных редактора и отображения шаблона? - person Adrian Grigore; 02.11.2010
comment
К доменно-независимым требованиям относятся такие вещи, как формат телефонного номера, формат электронной почты, расстояние (положительное число), возраст (положительное целое число) и т. д. Эти вещи не будут меняться вместе с бизнесом — это концептуальные константы, внешние по отношению к системе. Если модели представления зависят от экрана, метаданные для экрана могут быть добавлены непосредственно к ним. В противном случае используйте словарь данных. (см. bit.ly/bvL5KM). На самом деле я какое-то время не использовал ASP.NET MVC, но когда Я использовал версию 1, я обнаружил, что расширения ViewData со строгой типизацией от MvcContrib незаменимы. - person G-Wiz; 06.11.2010

Я не знаю, как это повлияет на проверку на стороне клиента, но если частичная проверка является вашей проблемой, вы можете изменить DataAnnotationsValidationRunner, обсуждаемый здесь, чтобы принять IEnumerable<string> список имен свойств, как показано ниже:

public static class DataAnnotationsValidationRunner
{
     public static IEnumerable<ErrorInfo> GetErrors(object instance, IEnumerable<string> fieldsToValidate)
     {
           return from prop in TypeDescriptor.GetProperties(instance).Cast<PropertyDescriptor>().Where(p => fieldsToValidate.Contains(p.Name))
                  from attribute in prop.Attributes.OfType<ValidationAttribute>()
                  where !attribute.IsValid(prop.GetValue(instance))
                  select new ErrorInfo(prop.Name, attribute.FormatErrorMessage(string.Empty), instance);
     }
}
person Brandon Linton    schedule 18.02.2010
comment
Хотя подобное исправление можно было бы реализовать на стороне клиента путем ветвления xVal, я бы не стал делать это таким образом, поскольку он опирается на магические строки, и слишком легко забыть добавить здесь строку после добавления нового свойства в модель. - person Adrian Grigore; 20.02.2010
comment
Я согласен, что это не идеальное решение, но мы решаем проблему магических строк с помощью постоянных коллекций имен полей, названных в честь шага в наших рабочих процессах, на который они ссылаются. Таким образом, когда контроллер вызывает класс, который выполняет проверку во время публикации, он передает коллекцию полей, которые, как вы знаете, должны быть проверены для этого представления. - person Brandon Linton; 20.02.2010

Я собираюсь рискнуть отрицательными голосами и заявить, что нет никакой пользы от ViewModels (в ASP.NET MVC), особенно с учетом накладных расходов на их создание и поддержку. Если идея состоит в том, чтобы отделиться от домена, это неоправданно. Пользовательский интерфейс, отделенный от домена, не является пользовательским интерфейсом для этого домена. Пользовательский интерфейс должен зависеть от домена, поэтому либо ваши представления/действия будут связаны с моделью предметной области, либо ваша логика управления ViewModel будет связана с моделью предметной области. Аргумент об архитектуре, таким образом, спорный.

Если идея состоит в том, чтобы запретить пользователям взламывать вредоносные HTTP-запросы POST, которые используют привязку модели ASP.NET MVC для изменения полей, которые им не должно быть разрешено изменять, то A) домен должен применять это требование, и B) действия должны предоставлять белые списки обновляемых свойств для связывателя модели.

Если ваш домен не предоставляет что-то сумасшедшее, например, живой граф объектов в памяти вместо копий сущностей, ViewModels — это напрасная трата усилий. Итак, чтобы ответить на ваш вопрос, сохраните проверку домена в модели домена.

person G-Wiz    schedule 23.01.2010
comment
Пользовательский интерфейс зависит от домена. Он не должен зависеть непосредственно от домена. Практически для любого отчета потребуется DTO; аналогичным образом, большинство форм могут не идеально сопоставляться с одним объектом предметной области и поэтому требуют специализированного экземпляра пользовательского интерфейса. Логика преобразования DTO и ViewModel по-прежнему является частью модели предметной области. Ни разу вы не нарушили никаких абстракций или инвариантов. WPF даже кристаллизует этот принцип в виде MVVM (Model-View-ViewModel). - person Aaronaught; 23.01.2010
comment
Я бы сказал, что отчетность — это отдельная область от области бизнеса. Один пользовательский интерфейс может объединить оба домена (зависит от обоих). И я не стесняюсь объединять несколько объектов предметной области в один класс для строго типизированных представлений. Помещение ViewModels в домен является вопиющим нарушением разделения интересов. Теперь вы не можете изменить свой пользовательский интерфейс без смены домена. И спорно, что ViewModels можно повторно использовать между различными приложениями/интерфейсами, которые поддерживают разные сценарии использования. - person G-Wiz; 23.01.2010
comment
Кстати, насколько я понимаю, на языке WPF ViewModel — это модель представления. Это абстракция характеристик представления. Поэтому он изначально определен в пользовательском интерфейсе. Смысл такого класса в том, чтобы позволить представлению привязываться к свойствам модели, которые нельзя напрямую сопоставить с простым элементом пользовательского интерфейса (например, раскрывающимися списками). Но в соответствии с шаблоном ViewModel фактически составляет модель предметной области и выполняет связывающие преобразования между пользовательским интерфейсом и моделью. - person G-Wiz; 23.01.2010
comment
gWiz: Я полностью согласен с тем, что правила проверки в идеале должны находиться в модели и не должны дублироваться в ViewModel. Однако просто неверно, что вам вообще не нужны пользовательские модели просмотра. Есть много случаев, когда вам абсолютно необходимо использовать пользовательскую модель представления, если вы хотите, чтобы ваше представление было безопасным. Для примеров приложений ASP.NET MVC, таких как NerdDinner, это может быть не так, но это происходит довольно часто в больших проектах с более сложным пользовательским интерфейсом. - person Adrian Grigore; 23.01.2010
comment
Я согласен с @Adrian. Я все еще новичок в ASP.NET MVC, но уже встречал примеры, для которых требовался ViewModel. Во-первых: мой User содержит Username и HashedPassword, но для моего просмотра сброса пароля нужны Password и PasswordConfirmation. Здесь нет логического сопоставления между представлением и моделью. Я использую aViewModel для проверки того, что два пароля совпадают и соответствуют некоторым другим правилам длины и т. д., затем один пароль передается в репозиторий, где он хешируется и сохраняется в базе данных. Как это сделать без ViewModel? - person devuxer; 31.01.2010
comment
Спасибо за комментарий, это интересный момент. Я только что использовал словарь ViewData и сделал проверку в ActionFilter. Подтверждение такого рода было единственным случаем, когда у меня была проверка на уровне пользовательского интерфейса в моем предыдущем проекте. Поскольку эти проверки уникальны для методов действий, повторное использование подхода ViewModel было чрезмерным IMHO. Я изучу использование ViewModels для подтверждения. Я думал, что проверка входа в систему может также использовать ViewModel, но это похоже на то, чтобы втиснуть что-то в модель проверки ASP.NET MVC. - person G-Wiz; 31.01.2010
comment
Если вы используете ViewData, вы можете легко обойтись без пользовательских моделей представления. Но правильная модель представления вместе со строго типизированными помощниками ввода в ASP.NET MVC 2 намного чище и безопаснее. Добавьте к этому проверку представления во время компиляции, и вы получите гораздо более надежные представления. Это, безусловно, превосходит словарь ViewData. - person Adrian Grigore; 31.01.2010