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

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

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

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

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

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

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

Спасибо,

Адриан

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

Ответ 1

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

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

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

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

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

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

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

3) Crazy Interface Dynamic Proxy Thing Последний метод, который я пытался использовать, - использовать интерфейсы для ViewModels. В результате у меня был объект User, унаследованный от таких интерфейсов, как IAdminEdit и IUserRegistration. IAdminEdit и IUserRegistration будут содержать атрибуты DataAnnotation, которые выполняли всю проверку, специфичную для контекста, как свойство Password с интерфейсами.

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

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

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

Ответ 2

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

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

Ответ 3

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

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

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

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

Ответ 4

Я не знаю, как это будет действовать для проверки на стороне клиента, но если частичная проверка является вашей проблемой, вы можете изменить обсуждаемый здесь 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);
     }
}

Ответ 5

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

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

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