简体   繁体   English

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

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

I am using DataAnnotations to validate my ViewModel on client side with jquery.validate.unobtrusive and on server side in ASP.NET MVC application. 我使用DataAnnotations在客户端使用jquery.validate.unobtrusiveASP.NET MVC应用程序的服务器端验证我的ViewModel

Not so long time ago, I figured out that I can write validation like this: 不久前,我发现我可以写这样的验证:

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

That way I can easily define some general strings in config or in resources and always use it in DataAnnotations . 这样我就可以在config或资源中轻松定义一些通用字符串,并始终在DataAnnotations使用它。 So it will be easier to change validation messages in my whole application in future. 因此,将来在整个应用程序中更改验证消息会更容易。

Also I know that there is a FluentValidation library that allows to add validation rules to already existing ViewModel . 另外我知道有一个FluentValidation库允许向现有的ViewModel添加验证规则。 I know that there is a problem with Add/Edit ViewModels that could have similar fields but different ValidationRules. 我知道Add / Edit ViewModels存在一个问题,它可能有类似的字段,但ValidationRules不同。

Another problem that comes from client validation is that html newly added to DOM (using ajax request ) should be parsed to enable validation. 客户端验证的另一个问题是,应该解析新添加到DOM的 html(使用ajax请求 )以启用验证。 This is how I do it: 我是这样做的:

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

So I have some questions: 所以我有一些问题:

  1. Is there some other useful practises that could help centralize all validation rules in application? 是否有一些其他有用的实践可以帮助集中应用程序中的所有验证规则?
  2. What's is a best way to solve Add/Edit ViewModel Validation problem? 什么是解决添加/编辑ViewModel验证问题的最佳方法? Can I use DataAnnotations with FluentValidation or separate Add and Edit ViewModels still is a best option? 我可以将DataAnnotationsFluentValidation一起使用,还是单独添加和编辑ViewModels仍然是最佳选择?
  3. Is there any better way to initialize validation on new DOM elements that received with ajax call other that I mention? 有没有更好的方法来初始化对我提到的ajax调用收到的新DOM元素的验证?

I'm not asking how to create my own DataValidators I know how to do it. 我不是问如何创建我自己的DataValidators我知道如何做到这一点。 I seeking of ways how to use them in more productive and easy maintainable way. 我想方设法如何以更高效和易维护的方式使用它们。

To answer your 3th question first: No there is no easier way then what you are doing. 首先回答你的第3个问题:没有比你正在做的更简单的方法。 Two lines of code to get it working can hardly be easier. 使它工作的两行代码几乎不容易。 Although there is a plug-in you could use, like explained in the question unobtrusive validation not working with dynamic content 虽然有一个插件你可以使用,如问题中解释的不引人注意的验证不使用动态内容

Your first question, how to centralize validation, I normally use a separate class file to store all my validation rules. 您的第一个问题,如何集中验证,我通常使用单独的类文件来存储我的所有验证规则。 This way I don't have to browse through every single class file to find the rules, but have them all in one place. 这样我就不必浏览每个单独的类文件来查找规则,而是将它们全部放在一个地方。 If that's better, is matter of choice. 如果那更好,那就是选择的问题。 The main reason I started to use it, is to be able to add validation to auto-generated classes, like classes from the Entity Framework. 我开始使用它的主要原因是能够为自动生成的类添加验证,比如来自Entity Framework的类。

So I have a file called ModelValidation.cs in my data layer, and have code for all my models like 所以我的数据层中有一个名为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; }
}

Now as you noticed I don't provide the actual error message. 现在您注意到我没有提供实际的错误消息。 I use conventions by Haacked to add the messages. 我使用Haacked的约定来添加消息。 It makes it simple to add localized validation rules. 它使添加本地化验证规则变得简单。

It basically comes down to a recource file containing something like: 它基本上归结为包含以下内容的recource文件:

Test_Name = "Provide name"
Test_Name_Required = "Name is required"

And these messages and naming will be used when you call regular MVC view code like 当您调用常规的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>

Your second question, about different validation for add/edit can be handled in two ways. 关于添加/编辑的不同验证的第二个问题可以通过两种方式处理。 The best way, would be to use views as they are actually intended. 最好的方法是使用视图,因为它们实际上是预期的。 That means you don't pass your actual models to the views, but you create a view model that contains only the data. 这意味着您不会将实际模型传递给视图,而是创建仅包含数据的视图模型。 So you have a view model for Create with the proper validation rules and a view model for Edit with the proper rules, and when they pass you insert the result in your actual model. 因此,您可以使用适当的验证规则Create的视图模型,使用适当的规则Create Edit的视图模型,当它们通过时,将结果插入到实际模型中。 This however requires a lot more code and manual work, so I can imagine you're not really willing to do it like this. 然而,这需要更多的代码和手动工作,所以我可以想象你不是真的愿意这样做。

Another option would be to use conditional validation like explained by viperguynaz. 另一种选择是使用像viperguynaz所解释的条件验证 Now instead of a boolean, my classes that require a change between edit/add have a primary key Id int . 现在,我需要在edit / add之间进行更改,而不是布尔值,我的类具有primary key Id int So I check if Id>0 to determine if it is an edit or not. 所以我检查Id>0是否确定它是否是编辑。

UPDATE: 更新:

If you want to update validation on every ajax call, you could use jQuery ajaxComplete . 如果要在每个ajax调用上更新验证,可以使用jQuery ajaxComplete This will revalidate all forms after every ajax request. 这将在每次ajax请求后重新验证所有表单。

$( document ).ajaxComplete(function() {
    $('form').each(function() {
        var $el = $(this);
        $el.data('validator', null); 
        $.validator.unobtrusive.parse($el);
    })
});

If this is something you want, depends on how often you receive a form via AJAX . 如果这是您想要的,取决于您通过AJAX收到表单的频率。 If you have a lot of AJAX request, like polling a status every 10seconds, than you don't want this. 如果你有很多AJAX请求,比如每隔10秒轮询一次状态,那么你就不需要了。 If you have an occasional AJAX request, that mostly contains a form, then you could use it. 如果您偶尔有一个AJAX请求,主要包含一个表单,那么您可以使用它。

If your AJAX returns a form you want to validate, then yes, it is good practise to update the validation. 如果您的AJAX返回了您要验证的表单,那么是的,更新验证是一种很好的做法。 But I guess a better question would be "Do I really need to send the form by AJAX?" 但我想一个更好的问题是“我真的需要通过AJAX发送表单吗?” AJAX is fun and useful, but it should be used with care and thought. AJAX既有趣又有用,但应谨慎使用。

Jquery unobtrusive validation works by applying attributes to INPUT elements that instruct the client library to validate that element using a rule that is mapped to the respective attribute. Jquery不显眼验证通过将属性应用于INPUT元素来工作,该元素指示客户端库使用映射到相应属性的规则来验证该元素。 For instance: the data-val-required html attribute is recognized by the unobtrusive library, and causes it to validate that element against the corresponding rule. 例如: data-val-required html属性被不显眼的库识别,并使其根据相应的规则验证该元素。

In .NET MVC , you can make this happen automatically for some specific rules by applying attributes to your model properties. .NET MVC中 ,您可以通过将属性应用于模型属性来自动执行某些特定规则。 Attributes like Required and MaxLength work because the Html helpers know how to read those attributes and add corresponding HTML attributes to their output that the unobtrusive library understands. RequiredMaxLength这样的属性起作用,因为Html帮助程序知道如何读取这些属性并将相应的HTML属性添加到不显眼的库理解的输出中。

If you add validation rules to your models in IValidatableObject or using FluentValidation , the HTML Helper will not see these rules, and therefore not try to translate them to unobtrusive attributes. 如果在IValidatableObject使用FluentValidation向模型添加验证规则,则HTML Helper将不会看到这些规则,因此不会尝试将它们转换为不显眼的属性。

In other words the "free" coordination you've seen thus far by applying attributes to your model and getting client validation is limited to validation attributes, and further, is limited (by default) only to those attributes that map directly to unobtrusive rules. 换句话说,到目前为止,您通过将属性应用于模型并获得客户端验证而看到的“自由”协调仅限于验证属性,并且进一步限制(默认情况下)仅限于直接映射到不显眼规则的那些属性。

The bright side is, you are free to create your own custom validation attributes, and by implementing IClientValidatable , the Html Helper will add an unobtrusive attribute with the name of your choosing that you can then teach the unobtrusive library to respect. 好的一面是,您可以自由创建自己的自定义验证属性,通过实现IClientValidatable ,Html Helper将添加一个不显眼的属性,其中包含您选择的名称,然后您可以教导不引人注目的库。

This is a custom attribute we use that ensures that one date falls after another date: 这是我们使用的自定义属性,可确保一个日期落在另一个日期之后:

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

We may apply the attribute to the model as such: 我们可以将属性应用于模型:

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

This causes the Html helper to render the following two attributes on the INPUT element when calling Html.EditorFor on a model property that has this attribute: 这会导致Html帮助Html.EditorFor在具有此属性的模型属性上调用Html.EditorFor时在INPUT元素上呈现以下两个属性:

data-val-dategreaterthan="You have to be born before you can die" 
data-val-dategreaterthan-otherpropertyname="Birthdate" 

So far so good, but now I have to teach unobtrusive validation what to do with those attributes. 到目前为止一切顺利,但现在我必须教导不引人注目的验证如何处理这些属性。 First, I have to create a named rule for jquery validation: 首先,我必须为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());
});

And then add an unobtrusive adaptor for that rule that maps the attribute to the rule: 然后为该规则添加一个不显眼的适配器,将该属性映射到规则:

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

After Ive done all this, I can get this validation rule for "free" anywhere else in my application just by applying that attribute to the model. 完成所有这些之后,我可以通过将该属性应用于模型,在我的应用程序中的任何其他位置获得“免费”验证规则。

To address your question of how to apply rules conditionally based on whether the model is being used in an add or an edit operation: this can probably be done by adding additional logic to your custom attributes and having both the IsValid method the GetClientValidation rules method attempt to glean some context from the model using reflection. 要解决有关如何根据模型是否在添加或编辑操作中使用条件而有条件地应用规则的问题:这可以通过向自定义属性添加其他逻辑并使IsValid方法同时使用GetClientValidation规则方法尝试来完成使用反射从模型中收集一些上下文。 But honestly, that seems like a mess to me. 但老实说,这对我来说似乎是一团糟。 For this, I'd just rely on server validation and whatever rules you choose to apply using IValidatableObject.Validate() method. 为此,我只依靠服务器验证以及您选择使用IValidatableObject.Validate()方法应用的规则。

Like others have said, there is no such tricks, no easy way to centralize your validations. 就像其他人所说的那样,没有这样的技巧,没有简单的方法可以集中验证。

I have a couple of approaches that might interest you. 我有几种方法可能会让你感兴趣。 Take note that this is how "we" solved the same problem before. 请注意,这就是“我们”之前解决同样问题的方式。 Its up to you if you can find our solution maintainable and productive. 如果您能找到我们的解决方案可维护且高效,那么由您决定。

I know that there is a problem with Add/Edit ViewModels that could have similar fields but different ValidationRules. 我知道Add / Edit ViewModels存在一个问题,它可能有类似的字段,但ValidationRules不同。

Inheritance Approach 继承方法

You can achieve centralized validation using a base class, and use subclasses for specific validations. 您可以使用基类实现集中验证,并使用子类进行特定验证。

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

Extending the ValidationAttribute Approach 扩展ValidationAttribute方法

Using custom ValidationAtribute , you can achieve centralized validation. 使用自定义ValidationAtribute ,您可以实现集中验证。 This is only the basic implementation, I am just showing you the idea. 这只是基本的实现,我只是向您展示这个想法。

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

You would use as such 你会这样使用

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? 有没有更好的方法来初始化对我提到的ajax调用收到的新DOM元素的验证?

This is how I rebind validators on dynamic elements. 这就是我如何在动态元素上重新绑定验证器。

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

Usage 用法

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

There are various ways to have Client Validation, like one that Microsoft uses for MVC, works with ubobtrusive library created by itself for integrating with DataAnnotations . 有多种方法可以让客户端验证,例如Microsoft用于MVC的方法,与ubobtrusive创建的ubobtrusive库一起使用,以便与DataAnnotations集成。 But , after some years of working with this helpful tool, I tired of it which is boring and tedious to employ in cases that we need separate ViewModels (and likely separate ViewModels for create/edit templates). 但是 ,经过几年使用这个有用的工具,我厌倦了它,在我们需要单独的ViewModels (可能还有用于创建/编辑模板的ViewModels )的情况下使用它是无聊和乏味的。

Another way is using MVVM which works well with MVC since the two paradigms are quite similar. 另一种方法是使用MVVM,它适用于MVC,因为这两种范例非常相似。 In MVC you have a Model that is bounded just on the server side when the client send content to the server . 在MVC中,当客户端将内容发送到服务器时,您有一个仅在服务器端受限制的模型 While MVVM binds a local model with the UI directly on the client . 而MVVM直接在客户端上绑定本地模型和UI Take a look at the Knockoutjs , the known one which help you to understand how to work with MVVM. 看一下Knockoutjs ,这是一个可以帮助您了解如何使用MVVM的已知方法。

With this in mind, I'll answer your questions in order: 考虑到这一点,我将按顺序回答您的问题:

  1. You can't centralize validation rules in the application unless by creating shared classes and reuse them by calling in separate Models/ViewModels. 除非通过创建共享类并通过调用单独的Models / ViewModel来重用它们,否则无法在应用程序中集中验证规则。
  2. If you wanna use Microsoft Validator, separating the Add/Edit ViewModels is a best option because of its readability and easier way to change. 如果您想使用Microsoft Validator,分离Add / Edit ViewModels是一个最佳选择,因为它具有可读性和更容易的更改方式。
  3. I never said that the Knockoutjs is better, they are different from each other, just gives you some flexibility to create views based on model requirements. 我从来没有说Knockoutjs更好,它们彼此不同,只是给你一些灵活性,可以根据模型要求创建视图。 This also take you away from centralizing validations :( 这也会让你远离集中验证:(

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

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