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

teo*_*kot 31 c# validation asp.net-mvc data-annotations unobtrusive-validation

DataAnnotations用于在ASP.NET MVC应用程序中ViewModel使用jquery.validate.unobtrusive和在服务器端验证我的客户端.

不久前,我发现我可以写这样的验证:

[Required(ErrorMessage = "{0} is required")]
public string Name { get; set; }
Run Code Online (Sandbox Code Playgroud)

这样我就可以在配置或资源中轻松定义一些通用字符串,并始终使用它DataAnnotations.因此,将来在整个应用程序中更改验证消息会更容易.

另外我知道有一个FluentValidation库允许向现有的验证规则添加验证规则ViewModel.我知道添加/编辑有一个问题ViewModels,可能有类似的字段,但ValidationRules不同.

客户端验证的另一个问题是,应该解析新添加到DOM的 html (使用ajax请求)以启用验证.我是这样做的:

$('#some-ajax-form').data('validator', null); 
$.validator.unobtrusive.parse('#some-ajax-form');
Run Code Online (Sandbox Code Playgroud)

所以我有一些问题:

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

我不是问如何创建自己的DataValidators我知道怎么做.我想方设法如何以更高效和易维护的方式使用它们.

Hug*_*ing 14

首先回答你的第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; }
}
Run Code Online (Sandbox Code Playgroud)

现在您注意到我没有提供实际的错误消息.我使用Haacked的约定来添加消息.它使添加本地化验证规则变得简单.

它基本上归结为包含以下内容的recource文件:

Test_Name = "Provide name"
Test_Name_Required = "Name is required"
Run Code Online (Sandbox Code Playgroud)

当您调用常规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>
Run Code Online (Sandbox Code Playgroud)

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

另一种选择是使用像viperguynaz所解释的条件验证.现在我的类需要在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);
    })
});
Run Code Online (Sandbox Code Playgroud)

如果这是你想要的东西,取决于你收到表格的频率AJAX.如果你有很多AJAX请求,比如每隔10秒轮询一次状态,那么你就不需要了.如果您偶尔有一个AJAX请求,主要包含一个表单,那么您可以使用它.

如果您AJAX返回要验证的表单,则是,最好更新验证.但我想一个更好的问题是"我真的需要通过AJAX发送表单吗?" AJAX是有趣和有用的,但应谨慎使用.


esm*_*e68 5

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

.NET MVC中,可以通过将属性应用于模型属性来自动执行某些特定规则。属性之所以喜欢RequiredMaxLength起作用,是因为HTML帮助器知道如何读取这些属性并将相应的HTML属性添加到不引人注目的库可以理解的输出中。

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

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

好的一面是,您可以自由创建自己的自定义验证属性,并且通过实施IClientValidatable,Html帮助器将添加一个具有您所选择名称的不引人注目的属性,然后您可以教导该不引人注意的库遵守。

这是我们使用的自定义属性,可确保一个日期晚于另一个日期:

    [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;
    }
}
Run Code Online (Sandbox Code Playgroud)

我们可以这样将属性应用于模型:

    [DateGreaterThan("Birthdate", "You have to be born before you can die")]
    public DateTime DeathDate { get; set; }
Run Code Online (Sandbox Code Playgroud)

这将导致Html帮助INPUT器在调用Html.EditorFor具有此属性的模型属性时在元素上呈现以下两个属性:

data-val-dategreaterthan="You have to be born before you can die" 
data-val-dategreaterthan-otherpropertyname="Birthdate" 
Run Code Online (Sandbox Code Playgroud)

到目前为止,一切都很好,但是现在我必须教大家进行简单的验证,以使用这些属性。首先,我必须为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());
});
Run Code Online (Sandbox Code Playgroud)

然后为该规则添加一个不引人注目的适配器,该适配器将属性映射到该规则:

jQuery.validator.unobtrusive.adapters.add("dategreaterthan", ["otherpropertyname"], function (options) {
    options.rules["dategreaterthan"] = "#" + options.params.otherpropertyname;
    options.messages["dategreaterthan"] = options.message;
});
Run Code Online (Sandbox Code Playgroud)

完成所有这些操作后,只需将属性应用于模型,即可在应用程序的其他任何地方获得“免费”的验证规则。

要解决您的问题,即如何基于模型是在添加操作中还是在编辑操作中使用条件来有条件地应用规则:可以通过向自定义属性中添加其他逻辑,并让规则方法尝试收集这两种IsValid方法来完成此操作GetClientValidation使用反射从模型中获取一些上下文。但老实说,这对我来说似乎是一团糟。为此,我将仅依靠服务器验证以及您选择使用IValidatableObject.Validate()方法应用的任何规则。


Yor*_*rro 5

就像其他人所说的那样,没有这样的技巧,没有简单的方法来集中你的验证.

我有几种方法可能会让你感兴趣.请注意,这就是"我们"之前解决同样问题的方式.如果您能找到我们的解决方案可维护且高效,那么由您决定.

我知道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; }
}
Run Code Online (Sandbox Code Playgroud)

扩展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;
    }
}
Run Code Online (Sandbox Code Playgroud)

你会这样使用

public class AddUserViewModel
{
    [CustomEmail]
    public string Email { get; set; }

    [CustomEmail]
    public string RetypeEmail { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

有没有更好的方法来初始化对我提到的ajax调用收到的新DOM元素的验证?

这就是我如何在动态元素上重新绑定验证器.

/** 
* 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);
};
Run Code Online (Sandbox Code Playgroud)

用法

// 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');
Run Code Online (Sandbox Code Playgroud)