web-dev-qa-db-ja.com

ASP.NET MVCのベストプラクティスViewModel検証

DataAnnotationsを使用して、クライアント側でViewModeljquery.validate.unobtrusiveおよびASP.NET MVCアプリケーションのサーバー側。

少し前まで、私はこのような検証を書くことができることを見つけました:

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

そうすれば、configまたはリソースでいくつかの一般的な文字列を簡単に定義でき、常にDataAnnotationsで使用できます。そのため、今後アプリケーション全体で検証メッセージを変更するのが簡単になります。

また、既存のViewModelに検証ルールを追加できるFluentValidationライブラリがあることも知っています。 Add/Edit ViewModelsに問題があり、フィールドが似ているがValidationRulesが異なる可能性があることを知っています。

クライアント検証に由来する別の問題は、htmlが[〜#〜] dom [〜#〜]に新たに追加されたことです(ajax request)は、検証を有効にするために解析する必要があります。これは私がそれを行う方法です:

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

だから私はいくつか質問があります:

  1. アプリケーションのすべての検証ルールを一元化するのに役立つ他の便利なプラクティスはありますか?
  2. ViewModel検証問題の追加/編集を解決する最良の方法は何ですか? DataAnnotationsFluentValidationで使用することも、個別の追加と編集ViewModelsを使用することも最適なオプションですか?
  3. [〜#〜] dom [〜#〜]要素で検証を初期化するより良い方法はありますかajax call私が言及した他の?

自分でDataValidatorsを作成する方法を尋ねているわけではありません。より生産的で簡単に保守可能な方法でそれらを使用する方法を探しています。

31
teo van kot

3番目の質問に最初に答えるには:いいえ、あなたがしていることより簡単な方法はありません。動作させるための2行のコードは、これ以上簡単になることはほとんどありません。質問で説明されているように、使用できるプラグインがありますが、 邪魔にならない検証が動的コンテンツで機能しない

最初の質問、検証を集中化する方法、私は通常、すべての検証ルールを保存するために別のクラスファイルを使用します。このように、ルールを見つけるためにすべてのクラスファイルを参照する必要はありませんが、すべてを1か所にまとめます。それがよければ、選択の問題です。私がそれを使い始めた主な理由は、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による規約 を使用してメッセージを追加します。ローカライズされた検証ルールを簡単に追加できます。

基本的には、次のようなものを含むリソースファイルになります。

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>

追加/編集のさまざまな検証に関する2番目の質問は、2つの方法で処理できます。最良の方法は、実際に意図したとおりにビューを使用することです。つまり、実際のモデルをビューに渡すのではなく、データのみを含むビューモデルを作成します。したがって、適切な検証ルールを持つCreateのビューモデルと適切なルールを持つEditのビューモデルがあり、それらが渡されると、実際のモデルに結果が挿入されます。ただし、これにはさらに多くのコードと手作業が必要になるため、このように実行するのは本当に嫌だと想像できます。

別のオプションは、viperguynazで説明されているように 条件付き検証 を使用することです。現在、ブール値の代わりに、編集と追加の間に変更を必要とするクラスには、primary keyIdintがあります。そこで、Id>0が編集であるかどうかを確認します。

UPDATE:

すべてのajax呼び出しで検証を更新する場合は、jQuery ajaxCompleteを使用できます。これにより、すべてのajaxリクエストの後にすべてのフォームが再検証されます。

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

これが必要な場合は、AJAXを介してフォームを受信する頻度に依存します。 10秒ごとにステータスをポーリングするなど、AJAXリクエストが多数ある場合は、これは望ましくありません。ほとんどがフォームを含むAJAXリクエストがときどきある場合は、それを使用できます。

AJAXが検証するフォームを返す場合、はい、検証を更新することをお勧めします。しかし、より良い質問は「AJAXでフォームを送信する必要が本当にあるのでしょうか?」 AJAXは楽しくて便利ですが、注意して使用する必要があります。

14
Hugo Delsing

JQueryの控えめな検証は、属性をINPUT要素に適用することにより機能します。これは、それぞれの属性にマップされたルールを使用してその要素を検証するようクライアントライブラリに指示します。たとえば:_data-val-required_ html属性は控えめなライブラリによって認識され、対応するルールに対してその要素を検証します。

。NET MVCでは、モデルプロパティに属性を適用することにより、特定のルールに対してこれを自動的に行うことができます。 RequiredMaxLengthなどの属性が機能するのは、Htmlヘルパーがそれらの属性を読み取り、控えめなライブラリが理解する出力に対応するHTML属性を追加する方法を知っているためです。

検証ルールをモデルにIValidatableObjectで追加する場合、またはFluentValidationを使用する場合、HTMLヘルパーはこれらのルールを表示しないため、目立たない属性に変換しようとしません。

つまり、モデルに属性を適用してクライアント検証を取得することでこれまで見てきた「自由な」調整は、検証属性に制限され、さらに(デフォルトで)控えめなルールに直接マップする属性にのみ制限されます。

明るい面は、独自のカスタム検証属性を自由に作成できることです。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;
    }
}
_

このような属性をモデルに適用できます:

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

これにより、Htmlヘルパーは、この属性を持つモデルプロパティで_Html.EditorFor_を呼び出すときに、INPUT要素で次の2つの属性をレンダリングします。

_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()メソッドを使用して適用するために選択したルールに依存します。

5
esmoore68

他の人が言ったように、そのようなトリックはなく、検証を一元化する簡単な方法もありません。

あなたに興味があるかもしれないアプローチがいくつかあります。これが以前に同じ問題を「私たち」が解決した方法であることに注意してください。当社のソリューションが維持可能で生産的であることがわかるかどうかはあなた次第です。

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');
5
Yorro