繁体   English   中英

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

[英]Best Practices ViewModel Validation in ASP.NET MVC

我使用DataAnnotations在客户端使用jquery.validate.unobtrusiveASP.NET MVC应用程序的服务器端验证我的ViewModel

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

[Required(ErrorMessage = "{0} is required")]
public string Name { get; set; }

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

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

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

$('#some-ajax-form').data('validator', null); 
$.validator.unobtrusive.parse('#some-ajax-form');

所以我有一些问题:

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

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

首先回答你的第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的视图模型,使用适当的规则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);
    })
});

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

如果您的AJAX返回了您要验证的表单,那么是的,更新验证是一种很好的做法。 但我想一个更好的问题是“我真的需要通过AJAX发送表单吗?” AJAX既有趣又有用,但应谨慎使用。

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

.NET MVC中 ,您可以通过将属性应用于模型属性来自动执行某些特定规则。 RequiredMaxLength这样的属性起作用,因为Html帮助程序知道如何读取这些属性并将相应的HTML属性添加到不显眼的库理解的输出中。

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

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

好的一面是,您可以自由创建自己的自定义验证属性,通过实现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帮助Html.EditorFor在具有此属性的模型属性上调用Html.EditorFor时在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;
});

完成所有这些之后,我可以通过将该属性应用于模型,在我的应用程序中的任何其他位置获得“免费”验证规则。

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

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

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

我知道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; }
}

有没有更好的方法来初始化对我提到的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);
};

用法

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

有多种方法可以让客户端验证,例如Microsoft用于MVC的方法,与ubobtrusive创建的ubobtrusive库一起使用,以便与DataAnnotations集成。 但是 ,经过几年使用这个有用的工具,我厌倦了它,在我们需要单独的ViewModels (可能还有用于创建/编辑模板的ViewModels )的情况下使用它是无聊和乏味的。

另一种方法是使用MVVM,它适用于MVC,因为这两种范例非常相似。 在MVC中,当客户端将内容发送到服务器时,您有一个仅在服务器端受限制的模型 而MVVM直接在客户端上绑定本地模型和UI 看一下Knockoutjs ,这是一个可以帮助您了解如何使用MVVM的已知方法。

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

  1. 除非通过创建共享类并通过调用单独的Models / ViewModel来重用它们,否则无法在应用程序中集中验证规则。
  2. 如果您想使用Microsoft Validator,分离Add / Edit ViewModels是一个最佳选择,因为它具有可读性和更容易的更改方式。
  3. 我从来没有说Knockoutjs更好,它们彼此不同,只是给你一些灵活性,可以根据模型要求创建视图。 这也会让你远离集中验证:(

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM