Проверка эффективности ViewModel в Best Practices в ASP.NET MVC
Я использую DataAnnotations
для проверки моей ViewModel
на стороне клиента с jquery.validate.unobtrusive
и на стороне сервера в приложении ASP.NET MVC.
Не так давно я понял, что могу написать валидацию следующим образом:
[Required(ErrorMessage = "{0} is required")]
public string Name { get; set; }
Таким образом, я могу легко определить некоторые общие строки в config или в ресурсах и всегда использовать его в DataAnnotations
. Таким образом, будет легче изменить сообщения проверки в моем приложении в будущем.
Также я знаю, что существует библиотека FluentValidation, которая позволяет добавлять правила проверки уже существующим ViewModel
. Я знаю, что есть проблема с Add/Edit ViewModels
, которая может иметь похожие поля, но различные ValidationRules.
Другая проблема, возникающая в результате проверки клиента, заключается в том, что html, недавно добавленный в DOM (с использованием ajax-запроса), должен быть проанализирован для включения проверки. Вот как я это делаю:
$('#some-ajax-form').data('validator', null);
$.validator.unobtrusive.parse('#some-ajax-form');
Итак, у меня есть несколько вопросов:
- Есть ли другие полезные методы, которые могут помочь централизовать все правила проверки в приложении?
- Каков наилучший способ решить проблему с добавлением/редактированием
ViewModel
? Могу ли я использовать DataAnnotations
с FluentValidation или отдельно добавить и отредактировать ViewModels
по-прежнему является лучшим вариантом?
- Есть ли лучший способ инициализировать проверку для новых элементов DOM, которые были получены с помощью ajax-вызова, который я упоминаю?
Я не спрашиваю, как создать свой собственный DataValidators
Я знаю, как это сделать. Я ищу способы использования их более продуктивным и легким для обслуживания способом.
Ответы
Ответ 1
Сначала ответьте на 3-й вопрос: Нет, нет более простого способа, чем то, что вы делаете. Две строки кода для его работы вряд ли могут быть проще. Хотя есть плагин, который вы могли бы использовать, как описано в вопросе ненавязчивая проверка, не работающая с динамическим контентом
Ваш первый вопрос, как централизовать проверку, я обычно использую отдельный файл класса для хранения всех моих правил проверки. Таким образом, мне не нужно просматривать каждый файл класса, чтобы найти правила, но имейте все это в одном месте. Если это лучше, это вопрос выбора. Основная причина, по которой я начал использовать его, - это добавить подтверждение для автоматически генерируемых классов, например классов из Entity Framework.
Итак, у меня есть файл под названием ModelValidation.cs
в моем слое данных, и у меня есть код для всех моих моделей, например
/// <summary>
/// Validation rules for the <see cref="Test"/> object
/// </summary>
/// <remarks>
/// 2015-01-26: Created
/// </remarks>
[MetadataType(typeof(TestValidation))]
public partial class Test { }
public class TestValidation
{
/// <summary>Name is required</summary>
[Required]
[StringLength(100)]
public string Name { get; set; }
/// <summary>Text is multiline</summary>
[DataType(DataType.MultilineText)]
[AllowHtml]
public string Text { get; set; }
}
Теперь, когда вы заметили, я не предоставляю фактическое сообщение об ошибке. Я использую соглашения Haacked для добавления сообщений. Это упрощает добавление локализованных правил проверки.
В основном это сводится к файлу-recource, содержащему что-то вроде:
Test_Name = "Provide name"
Test_Name_Required = "Name is required"
И эти сообщения и именование будут использоваться, когда вы вызываете обычный MVC view
код, например
<div class="editor-container">
<div class="editor-label">
@Html.LabelFor(model => model.Name) <!--"Provide name"-->
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name) <!--"Name is required"-->
</div>
</div>
Второй вопрос, касающийся различной проверки для добавления/редактирования, можно обрабатывать двумя способами. Лучшим способом было бы использовать представления, поскольку они на самом деле предназначены. Это означает, что вы не передаете свои фактические модели в представления, но вы создаете модель представления, содержащую только данные. Таким образом, у вас есть модель представления для Create
с правильными правилами проверки и моделью просмотра для Edit
с правильными правилами, а когда они проходят, вы вставляете результат в свою фактическую модель.
Однако для этого требуется гораздо больше кода и ручной работы, поэтому я могу себе представить, что вы не очень хотите это сделать.
Другим вариантом было бы использовать условную проверку, как это объяснялось viperguynaz. Теперь вместо логического, мои классы, требующие изменения между edit/add, имеют primary key
Id
int
. Поэтому я проверяю, если Id>0
определить, является ли это редактированием или нет.
UPDATE:
Если вы хотите обновить валидацию при каждом вызове ajax, вы можете использовать jQuery ajaxComplete
. Это будет проверять все формы после каждого запроса ajax.
$( document ).ajaxComplete(function() {
$('form').each(function() {
var $el = $(this);
$el.data('validator', null);
$.validator.unobtrusive.parse($el);
})
});
Если это то, что вы хотите, зависит от того, как часто вы получаете форму через AJAX
. Если у вас есть много запросов AJAX
, например, опрос статуса каждые 10 секунд, чем вы этого не хотите. Если у вас есть случайный запрос AJAX
, который в основном содержит форму, вы можете использовать его.
Если ваш AJAX
возвращает форму, которую вы хотите проверить, то да, это хорошая практика для обновления проверки. Но я думаю, лучший вопрос будет: "Мне действительно нужно отправить форму AJAX?"
AJAX
является забавным и полезным, но его следует использовать с осторожностью и мыслью.
Ответ 2
Как говорили другие, таких трюков нет, нет простого способа централизовать ваши проверки.
У меня есть несколько подходов, которые могут вас заинтересовать. Обратите внимание, что так "мы" решили ту же проблему раньше. Это зависит от вас, если вы сможете найти наше решение, поддерживающее и производительное.
Я знаю, что есть проблема с Add/Edit ViewModels, которые могут иметь похожие поля, но разные ValidationRules.
Подход наследования
Вы можете добиться централизованной проверки с использованием базового класса и использовать подклассы для конкретных проверок.
// Base class. That will be shared by the add and edit
public class UserModel
{
public int ID { get; set; }
public virtual string FirstName { get; set; } // Notice the virtual?
// This validation is shared on both Add and Edit.
// A centralized approach.
[Required]
public string LastName { get; set; }
}
// Used for creating a new user.
public class AddUserViewModel : UserModel
{
// AddUser has its own specific validation for the first name.
[Required]
public override string FirstName { get; set; } // Notice the override?
}
// Used for updating a user.
public class EditUserViewModel : UserModel
{
public override string FirstName { get; set; }
}
Расширение подхода ValidationAttribute
Используя пользовательский ValidationAtribute
, вы можете добиться централизованной проверки. Это только базовая реализация, я просто показываю вам эту идею.
using System.ComponentModel.DataAnnotations;
public class CustomEmailAttribute : ValidationAttribute
{
public CustomEmailAttribute()
{
this.ErrorMessage = "Error Message Here";
}
public override bool IsValid(object value)
{
string email = value as string;
// Put validation logic here.
return valid;
}
}
Вы использовали бы как таковой
public class AddUserViewModel
{
[CustomEmail]
public string Email { get; set; }
[CustomEmail]
public string RetypeEmail { get; set; }
}
Есть ли лучший способ инициализировать проверку на новых элементах DOM, которые были получены с помощью ajax-вызова, о котором я упоминаю?
Вот как я восстанавливаю валидаторы на динамических элементах.
/**
* Rebinds the MVC unobtrusive validation to the newly written
* form inputs. This is especially useful for forms loaded from
* partial views or ajax.
*
* Credits: http://www.mfranc.com/javascript/unobtrusive-validation-in-partial-views/
*
* Usage: Call after pasting the partial view
*
*/
function refreshValidators(formSelector) {
//get the relevant form
var form = $(formSelector);
// delete validator in case someone called form.validate()
$(form).removeData("validator");
$.validator.unobtrusive.parse(form);
};
Использование
// Dynamically load the add-user interface from a partial view.
$('#add-user-div').html(partialView);
// Call refresh validators on the form
refreshValidators('#add-user-div form');
Ответ 3
Функция ненавязчивого JQuery работает, применяя атрибуты к элементам INPUT, которые инструктируют библиотеку клиента проверять этот элемент, используя правило, которое сопоставляется с соответствующим атрибутом. Например: атрибут data-val-required
html распознается ненавязчивой библиотекой и заставляет его проверять этот элемент на соответствие правилу.
В .NET MVC вы можете сделать это автоматически для некоторых определенных правил, применяя атрибуты к вашим свойствам модели. Атрибуты, такие как Required
и MaxLength
работают, потому что помощники Html знают, как читать эти атрибуты и добавлять соответствующие атрибуты HTML к их выводам, которые понимает ненавязчивая библиотека.
Если вы добавите правила проверки в свои модели в IValidatableObject
или используете FluentValidation, HTML-помощник не увидит эти правила и поэтому не попытается перевести их на ненавязчивые атрибуты.
Другими словами, "свободная" координация, которую вы видели до сих пор, применяя атрибуты к вашей модели и получая проверку клиента, ограничена атрибутами проверки и, кроме того, ограничена (по умолчанию) только теми атрибутами, которые отображаются непосредственно на ненавязчивые правила.
Яркая сторона: вы можете создавать свои собственные атрибуты проверки, и, реализуя IClientValidatable
, Html Helper добавит ненавязчивый атрибут с выбранным вами именем, чтобы потом научить ненавязчивую библиотеку уважать.
Это пользовательский атрибут, который мы используем, который гарантирует, что одна дате выпадет после другой даты:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class DateGreaterThanAttribute : ValidationAttribute, IClientValidatable
{
string otherPropertyName;
public DateGreaterThanAttribute(string otherPropertyName, string errorMessage = null)
: base(errorMessage)
{
this.otherPropertyName = otherPropertyName;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
ValidationResult validationResult = ValidationResult.Success;
// Using reflection we can get a reference to the other date property, in this example the project start date
var otherPropertyInfo = validationContext.ObjectType.GetProperty(this.otherPropertyName);
// Let check that otherProperty is of type DateTime as we expect it to be
if (otherPropertyInfo.PropertyType.Equals(new DateTime().GetType()))
{
DateTime toValidate = (DateTime)value;
DateTime referenceProperty = (DateTime)otherPropertyInfo.GetValue(validationContext.ObjectInstance, null);
// if the end date is lower than the start date, than the validationResult will be set to false and return
// a properly formatted error message
if (toValidate.CompareTo(referenceProperty) < 1)
{
validationResult = new ValidationResult(this.GetErrorMessage(validationContext));
}
}
else
{
// do nothing. We're not checking for a valid date here
}
return validationResult;
}
public override string FormatErrorMessage(string name)
{
return "must be greater than " + otherPropertyName;
}
private string GetErrorMessage(ValidationContext validationContext)
{
if (!this.ErrorMessage.IsNullOrEmpty())
return this.ErrorMessage;
else
{
var thisPropName = !validationContext.DisplayName.IsNullOrEmpty() ? validationContext.DisplayName : validationContext.MemberName;
var otherPropertyInfo = validationContext.ObjectType.GetProperty(this.otherPropertyName);
var otherPropName = otherPropertyInfo.Name;
// Check to see if there is a Displayname attribute and use that to build the message instead of the property name
var displayNameAttrs = otherPropertyInfo.GetCustomAttributes(typeof(DisplayNameAttribute), false);
if (displayNameAttrs.Length > 0)
otherPropName = ((DisplayNameAttribute)displayNameAttrs[0]).DisplayName;
return "{0} must be on or after {1}".FormatWith(thisPropName, otherPropName);
}
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
//string errorMessage = this.FormatErrorMessage(metadata.DisplayName);
string errorMessage = ErrorMessageString;
// The value we set here are needed by the jQuery adapter
ModelClientValidationRule dateGreaterThanRule = new ModelClientValidationRule();
dateGreaterThanRule.ErrorMessage = errorMessage;
dateGreaterThanRule.ValidationType = "dategreaterthan"; // This is the name the jQuery adapter will use
//"otherpropertyname" is the name of the jQuery parameter for the adapter, must be LOWERCASE!
dateGreaterThanRule.ValidationParameters.Add("otherpropertyname", otherPropertyName);
yield return dateGreaterThanRule;
}
}
Мы можем применить атрибут к модели как таковой:
[DateGreaterThan("Birthdate", "You have to be born before you can die")]
public DateTime DeathDate { get; set; }
Это приводит к тому, что хелпер Html отображает следующие два атрибута в элементе INPUT
при вызове Html.EditorFor
для свойства модели, имеющего этот атрибут:
data-val-dategreaterthan="You have to be born before you can die"
data-val-dategreaterthan-otherpropertyname="Birthdate"
До сих пор так хорошо, но теперь я должен учить ненавязчивую проверку того, что делать с этими атрибутами. Во-первых, я должен создать именованное правило для проверки jquery:
// Value is the element to be validated, params is the array of name/value pairs of the parameters extracted from the HTML, element is the HTML element that the validator is attached to
jQuery.validator.addMethod("dategreaterthan", function (value, element, params) {
return Date.parse(value) > Date.parse($(params).val());
});
И затем добавьте ненавязчивый адаптер для этого правила, которое сопоставляет атрибут правилу:
jQuery.validator.unobtrusive.adapters.add("dategreaterthan", ["otherpropertyname"], function (options) {
options.rules["dategreaterthan"] = "#" + options.params.otherpropertyname;
options.messages["dategreaterthan"] = options.message;
});
После того, как я все это сделал, я могу получить это правило проверки для "свободного" в другом месте в моем приложении, просто применив этот атрибут к модели.
Чтобы решить вопрос о том, как применять правила условно на основе того, используется ли модель в операции добавления или редактирования, это, вероятно, можно сделать, добавив дополнительную логику к вашим пользовательским атрибутам и имея как метод IsValid
метод правил GetClientValidation
пытается извлечь некоторый контекст из модели с использованием отражения. Но, честно говоря, это кажется беспорядочным для меня. Для этого я просто полагаюсь на проверку сервера и любые правила, которые вы решите применять с помощью метода IValidatableObject.Validate()
.
Ответ 4
Существуют различные способы проверки клиента, как тот, который Microsoft использует для MVC, работает с библиотекой ubobtrusive
, созданной самостоятельно для интеграции с DataAnnotations
. Но, после нескольких лет работы с этим полезным инструментом, я устал от этого, что скучно и утомительно использовать в случаях, когда нам нужен отдельный ViewModels
(и, вероятно, отдельный ViewModels
для создания/редактирования шаблоны).
Другой способ - использовать MVVM, который хорошо работает с MVC, поскольку две парадигмы весьма схожи. В MVC у вас есть модель, ограниченная только на стороне сервера, когда клиент отправляет контент на сервер. Хотя MVVM связывает локальную модель с пользовательским интерфейсом непосредственно на клиенте. Взгляните на Knockoutjs, известный, который поможет вам понять, как работать с MVVM.
С учетом этого я отвечу на ваши вопросы по порядку:
- Вы не можете централизовать правила проверки в приложении, если
создания общих классов и повторного использования их путем вызова в отдельном
Модели /ViewModels.
- Если вы хотите использовать Microsoft Validator, разделив Add/Edit
ViewModels - лучший вариант из-за его читаемости и более простого способа
изменение.
- Я никогда не говорил, что Knockoutjs лучше, они разные
друг от друга, просто дает вам некоторую гибкость для создания представлений
на основе требований модели. Это также уводит вас от
централизация валидаций: (