ASP.NET MVC 中的最佳实践 ViewModel 验证

Best Practices ViewModel Validation in ASP.NET MVC

我正在使用 DataAnnotations 在客户端使用 jquery.validate.unobtrusive 验证我的 ViewModel,在服务器端使用 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');

所以我有一些问题:

  1. 是否有一些其他有用的做法可以帮助集中应用程序中的所有验证规则?
  2. 什么是解决 Add/Edit ViewModel 验证问题的最佳方法?我可以将 DataAnnotationsFluentValidation 一起使用,还是单独使用添加和编辑 ViewModels 仍然是最佳选择?
  3. 有没有更好的方法来初始化通过 ajax 调用 收到的新 DOM 元素的验证我提到的其他内容?

我不是在问如何创建自己的 DataValidators 我知道该怎么做。我正在寻找如何以更高效和易于维护的方式使用它们的方法。

Jquery 不显眼的验证通过将属性应用于 INPUT 元素来工作,这些元素指示客户端库使用映射到相应属性的规则来验证该元素。例如:data-val-required html 属性被不显眼的库识别,并使其根据相应的规则验证该元素。

.NET MVC 中,您可以通过将特性应用到模型属性来使某些特定规则自动发生这种情况。 RequiredMaxLength 之类的属性之所以有效,是因为 Html 助手知道如何读取这些属性并将相应的 HTML 属性添加到不显眼的库可以理解的输出中。

如果您在 IValidatableObject 中或使用 FluentValidation 向模型添加验证规则,HTML Helper 将看不到这些规则,因此不会尝试将它们转化为不显眼的属性。

换句话说,到目前为止,您通过将属性应用于模型并获得客户端验证而看到的 "free" 协调仅限于验证属性,而且(默认情况下)仅限于那些属性直接映射到不显眼的规则。

好的一面是,您可以自由创建自己的自定义验证属性,并且通过实施 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's 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.EditorFor 时,这会导致 Html 助手在 INPUT 元素上呈现以下两个属性:

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;
});

完成所有这些后,只需将该属性应用于模型,我就可以在我的应用程序的任何其他地方获得 "free" 的验证规则。

要解决关于如何根据模型是在添加操作还是编辑操作中使用有条件地应用规则的问题:这可能可以通过向自定义属性添加额外的逻辑并同时具有 IsValid 方法 GetClientValidation 规则方法尝试使用反射从模型中收集一些上下文。但老实说,这对我来说似乎一团糟。为此,我只依赖服务器验证以及您选择使用 IValidatableObject.Validate() 方法应用的任何规则。

首先回答您的第 3 个问题:不,没有比您正在做的更简单的方法了。两行代码让它工作再简单不过了。尽管您可以使用插件,如问题 unobtrusive validation not working with dynamic content

中所述

你的第一个问题,如何集中验证,我通常使用一个单独的 class 文件来存储我所有的验证规则。这样我就不必浏览每个 class 文件来查找规则,而是将它们全部放在一个地方。如果那更好,那是选择的问题。我开始使用它的主要原因是能够为自动生成的 classes 添加验证,例如来自 Entity Framework.

的 classes

所以我的数据层中有一个名为 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; }
}

正如您所注意到的,我没有提供实际的错误消息。我使用 conventions by Haacked 添加消息。它使添加本地化验证规则变得简单。

它基本上归结为一个包含类似内容的资源文件:

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>

你的第二个问题,关于 add/edit 的不同验证,可以用两种方式处理。最好的方法是按照实际目的使用视图。这意味着您不会将实际模型传递给视图,而是创建一个仅包含数据的视图模型。因此,您有一个具有正确验证规则的 Create 的视图模型和一个具有正确规则的 Edit 的视图模型,当它们通过时,您将结果插入到您的实际模型中。 然而,这需要更多的代码和手动工作,所以我可以想象你不会真的愿意这样做。

另一种选择是使用 conditional validation,就像 viperguynaz 解释的那样。现在,我的 classes 需要在 edit/add 之间进行更改,而不是布尔值,而是 primary key Id int。所以我检查是否 Id>0 以确定它是否是编辑。

更新:

如果您想在每次 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 returns 表单要验证,那么是的,更新验证是个好习惯。但我想更好的问题是 "Do I really need to send the form by AJAX?" AJAX很好玩也很有用,但要慎重使用。

有多种方法可以进行客户端验证,例如 Microsoft 用于 MVC 的方法,它与 ubobtrusive 自己创建的用于与 DataAnnotations 集成的库一起使用。 但是,在使用这个有用的工具几年之后,我厌倦了它,在我们需要单独的 ViewModels(并且可能单独的 ViewModels 用于 create/edit 个模板)。

另一种方法是使用 MVVM,它与 MVC 配合得很好,因为这两种范例非常相似。 在 MVC 中,当客户端向服务器发送内容时,您有一个仅在服务器端绑定的模型而 MVVM 直接在客户端 上将本地模型与 UI 绑定。看看 Knockoutjs,这是一个已知的帮助您了解如何使用 MVVM 的工具。

考虑到这一点,我将按顺序回答您的问题:

  1. 除非通过 创建共享 类 并通过单独调用来重用它们 Models/ViewModels.
  2. 如果您想使用 Microsoft Validator,请将 Add/Edit 分开 ViewModels 是最佳选择,因为它的可读性和更简单的方法 改变。
  3. 我从没说过 Knockoutjs 更好,它们是不同的 彼此之间,只是给你一些创建视图的灵活性 根据模型要求。这也让你远离 集中验证:(

正如其他人所说,没有这样的技巧,没有简单的方法来集中验证。

我有几个您可能感兴趣的方法。请注意,这是“我们”之前解决相同问题的方式。如果您能找到我们的解决方案可维护且高效,这取决于您。

I know that there is a problem with Add/Edit ViewModels that could have similar fields but different ValidationRules.

继承方法

您可以使用基 class 实现集中验证,并使用 subclasses 进行特定验证。

// 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; }
}

Is there any better way to initialize validation on new DOM elements that received with ajax call other that I mention?

这就是我在动态元素上重新绑定验证器的方式 (jQuery)

/** 
* 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);
};

用法(JQuery)

// 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');