8

いくつかのDataAnnotations検証を含むViewModelがあり、より複雑な検証ではIValidatableObjectを実装し、Validateメソッドを使用します。

私が期待していた動作はこれでした。最初にすべてのDataAnnotationsを実行し、次にエラーがない場合にのみValidateメソッドを実行しました。しかし、これが常に正しいとは限らないことがわかりました。私のViewModel(デモ1)には、1つstring、1つ、decimalおよび1つの3つのファイルがありdecimal?ます。3つのプロパティはすべて、必須属性のみを持っています。stringおよびの動作decimal?は期待どおりですが、がdecimal空の場合、必須の検証は失敗し(これまでのところ良好)、Validateメソッドを実行します。プロパティを調べると、その値はゼロです。

ここで何が起こっているのですか?私は何が欠けていますか?

:Required属性は、値がnullかどうかをチェックすることを想定していることを知っています。したがって、null許容型では必須属性を使用しないように指示されるか(トリガーされないため)、または、属性がPOST値を理解し、フィールドが入力されていないことに注意してください。最初のケースでは、属性はトリガーされず、Validateメソッドが起動する必要があります。2番目のケースでは、属性がトリガーされ、Validateメソッドが起動されないはずです。しかし、私の結果は次のとおりです。属性がトリガーされ、Validateメソッドが起動します。

コードは次のとおりです(特別なことは何もありません):

コントローラ:

public ActionResult Index()
{
    return View(HomeModel.LoadHome());
}

[HttpPost]
public ActionResult Index(HomeViewModel viewModel)
{
    try
    {
        if (ModelState.IsValid)
        {
            HomeModel.ProcessHome(viewModel);
            return RedirectToAction("Index", "Result");
        }
    }
    catch (ApplicationException ex)
    {
        ModelState.AddModelError(string.Empty, ex.Message);
    }
    catch (Exception ex)
    {
        ModelState.AddModelError(string.Empty, "Internal error.");
    }
    return View(viewModel);
}

モデル:

public static HomeViewModel LoadHome()
{
    HomeViewModel viewModel = new HomeViewModel();
    viewModel.String = string.Empty;
    return viewModel;
}

public static void ProcessHome(HomeViewModel viewModel)
{
    // Not relevant code
}

ViewModel:

public class HomeViewModel : IValidatableObject
{
    [Required(ErrorMessage = "Required {0}")]
    [Display(Name = "string")]
    public string String { get; set; }

    [Required(ErrorMessage = "Required {0}")]
    [Display(Name = "decimal")]
    public decimal Decimal { get; set; }

    [Required(ErrorMessage = "Required {0}")]
    [Display(Name = "decimal?")]
    public decimal? DecimalNullable { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        yield return new ValidationResult("Error from Validate method");
    }
}

意見:

@model MVCTest1.ViewModels.HomeViewModel 

@{
    Layout = "~/Views/Shared/_Layout.cshtml";
}

@using (Html.BeginForm(null, null, FormMethod.Post))
{
    <div>
        @Html.ValidationSummary()
    </div>
    <label id="lblNombre" for="Nombre">Nombre:</label>
    @Html.TextBoxFor(m => m.Nombre)
    <label id="lblDecimal" for="Decimal">Decimal:</label>
    @Html.TextBoxFor(m => m.Decimal)
    <label id="lblDecimalNullable" for="DecimalNullable">Decimal?:</label>
    @Html.TextBoxFor(m => m.DecimalNullable)
    <button type="submit" id="aceptar">Aceptar</button>
    <button type="submit" id="superAceptar">SuperAceptar</button>
    @Html.HiddenFor(m => m.Accion)
}
4

1 に答える 1

14

コメント交換後の考慮事項:

開発者の間で合意され、期待される動作は、検証属性がトリガーされない場合にのみIValidatableObjectのメソッドが呼び出されることです。Validate()要するに、予想されるアルゴリズムは次のとおりです (前のリンクから取得)。

  1. プロパティ レベルの属性を検証する
  2. 無効なバリデータがある場合は、失敗を返す検証を中止します
  3. オブジェクト レベルの属性を検証する
  4. 無効なバリデータがある場合は、失敗を返す検証を中止します
  5. デスクトップ フレームワークで、オブジェクトが IValidatableObject を実装している場合、その Validate メソッドを呼び出して、失敗を返します。

ただし、質問のコードを使用すると、トリガーのValidate後でも呼び出され[Required]ます。これは明らかなMVC バグのようです。ここで報告されています。

考えられる 3 つの回避策:

  1. ここには回避策がありますが、MVC の予期される動作を壊すことを除けば、その使用法に関するいくつかの問題が述べられています。同じフィールドに対して複数のエラーが表示されないようにいくつかの変更を加えたコードを次に示します。

    viewModel
        .Validate(new ValidationContext(viewModel, null, null))
        .ToList()
        .ForEach(e => e.MemberNames.ToList().ForEach(m =>
        {
            if (ModelState[m].Errors.Count == 0)
                ModelState.AddModelError(m, e.ErrorMessage);
        }));
    
  2. 忘れIValidatableObjectて、属性のみを使用してください。クリーンで直接的で、ローカリゼーションの処理に優れており、何よりもすべてのモデルで再利用できます。実行する検証ごとにValidationAttributeを実装するだけです。すべてのモデルまたは特定のプロパティを検証できますが、それはあなた次第です。デフォルトで利用可能な属性 (DataType、Regex、Required など) とは別に、最も使用されている検証を備えたライブラリがいくつかあります。「欠けているもの」を実装するのはFluentValidationです。

  3. データ アノテーションIValidatableObjectを破棄するインターフェイスのみを実装します。これは、非常に特殊なモデルであり、多くの検証を必要としない場合に妥当なオプションと思われます。ほとんどの場合、開発者は、属性が使用されている場合、デフォルトで既に実装されている検証でコードの重複につながる、通常の一般的な検証 (つまり、必須など) をすべて実行します。また、再利用性もありません。

コメントの前に答える:

まず、提供されたコードのみを使用して、ゼロから新しいプロジェクトを作成しました。データ注釈と Validate メソッドの両方を同時にトリガーすることは決してありません。

とにかく、これを知って、

設計上、MVC3 は、 、または yes[Required]などの null 非許容値型に属性を追加します。そのため、そこから必須属性を削除しても、そこにあるものと同じように機能します。intDateTimedecimaldecimal

これが間違っているかどうかについては議論の余地がありますが、それは設計された方法です。

あなたの例では:

  • [Required] が存在し、値が指定されていない場合、「DataAnnotation」がトリガーされます。私の立場からすれば十分に理解できる
  • 「DataAnnotation」は、[Required] が存在しないが、値が null 非許容の場合にトリガーされます。議論の余地がありますが、プロパティがnull不可の場合は値を入力する必要があるため、同意する傾向があります。それ以外の場合は、ユーザーに表示しないか、 nullable を使用するだけdecimalです。

この動作は、Application_Start メソッド内でこれを使用して無効にすることができます。

DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;

プロパティの名前は一目瞭然だと思います。

とにかく、ユーザーに不要なものを入力させ、そのプロパティをnull可能にしない理由がわかりません。nullの場合は、コントローラ内で検証する前に null にしたくない場合は、それをチェックするのがあなたの仕事です。

public ActionResult Index(HomeViewModel viewModel)
{
    // Complete values that the user may have 
    // not filled (all not-required / nullables)

    if (viewModel.Decimal == null) 
    {
        viewModel.Decimal = 0m;
    }

    // Now I can validate the model

    if (ModelState.IsValid)
    {
        HomeModel.ProcessHome(viewModel);
        return RedirectToAction("Ok");
    }
}

このアプローチで何が間違っていると思いますか、またはこのようにするべきではありませんか?

于 2011-11-17T13:42:07.957 に答える