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

时间:2015-01-22 13:37:33

标签: c# asp.net-mvc validation data-annotations unobtrusive-validation

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

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

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

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

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

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

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

所以我有一些问题:

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

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

4 个答案:

答案 0 :(得分:12)

首先回答你的第3个问题:没有比你正在做的更简单的方法。使它工作的两行代码几乎不容易。虽然您可以使用插件,如问题unobtrusive validation not working with dynamic content

中所述

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

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

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

另一个选择是使用viperguynaz解释的conditional validation。现在我的类需要在编辑/添加之间进行更改而不是布尔值,而是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既有趣又有用,但应谨慎使用。

答案 1 :(得分:5)

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

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

  

我知道添加/编辑ViewModel存在问题,可能有类似字段但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');

答案 2 :(得分:4)

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

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

如果您在IValidatableObject或使用 FluentValidation 向模型添加验证规则,则HTML帮助程序将不会看到这些规则,因此不会尝试将其转换为不显眼的属性。

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

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

完成所有这些后,我可以获得这个验证规则&#34; free&#34;只需将该属性应用于模型,即可在我的应用程序中的任何其他位置使用。

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

答案 3 :(得分:1)

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

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

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

  1. 除非通过,否则您无法在应用程序中集中验证规则 创建共享类并通过单独调用来重用它们 型号/的ViewModels。
  2. 如果您想使用Microsoft Validator,请分离添加/编辑 ViewModels是一个最好的选择,因为它具有可读性和更简单的方法 变化。
  3. 我从来没有说 Knockoutjs 更好,他们是不同的 彼此之间,只是为您提供了一些创建视图的灵活性 根据型号要求。这也让你远离 集中验证:(