web-dev-qa-db-ja.com

MVC3検証-グループから1つ必要

次のビューモデルがあるとします。

public class SomeViewModel
{
  public bool IsA { get; set; }
  public bool IsB { get; set; }
  public bool IsC { get; set; } 
  //... other properties
}

使用可能なプロパティの少なくとも1つがtrueであることを検証するカスタム属性を作成したいと思います。プロパティに属性を付加し、次のようにグループ名を割り当てることができると思います。

public class SomeViewModel
{
  [RequireAtLeastOneOfGroup("Group1")]
  public bool IsA { get; set; }

  [RequireAtLeastOneOfGroup("Group1")]
  public bool IsB { get; set; }

  [RequireAtLeastOneOfGroup("Group1")]
  public bool IsC { get; set; } 

  //... other properties

  [RequireAtLeastOneOfGroup("Group2")]
  public bool IsY { get; set; }

  [RequireAtLeastOneOfGroup("Group2")]
  public bool IsZ { get; set; }
}

フォームを送信する前に、フォームの値が変更されたときにクライアント側で検証したいので、可能であればクラスレベルの属性を避けたいと思います。

これには、カスタム属性のパラメーターとして渡された同一のグループ名値を持つすべてのプロパティを見つけるために、サーバー側とクライアント側の両方の検証が必要になります。これは可能ですか?どんなガイダンスでも大歓迎です。

36
Shawn

続行する1つの方法は次のとおりです(他の方法もありますが、ビューモデルにそのまま一致する方法を示しています)。

[AttributeUsage(AttributeTargets.Property)]
public class RequireAtLeastOneOfGroupAttribute: ValidationAttribute, IClientValidatable
{
    public RequireAtLeastOneOfGroupAttribute(string groupName)
    {
        ErrorMessage = string.Format("You must select at least one value from group \"{0}\"", groupName);
        GroupName = groupName;
    }

    public string GroupName { get; private set; }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        foreach (var property in GetGroupProperties(validationContext.ObjectType))
        {
            var propertyValue = (bool)property.GetValue(validationContext.ObjectInstance, null);
            if (propertyValue)
            {
                // at least one property is true in this group => the model is valid
                return null;
            }
        }
        return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
    }

    private IEnumerable<PropertyInfo> GetGroupProperties(Type type)
    {
        return
            from property in type.GetProperties()
            where property.PropertyType == typeof(bool)
            let attributes = property.GetCustomAttributes(typeof(RequireAtLeastOneOfGroupAttribute), false).OfType<RequireAtLeastOneOfGroupAttribute>()
            where attributes.Count() > 0
            from attribute in attributes
            where attribute.GroupName == GroupName
            select property;
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var groupProperties = GetGroupProperties(metadata.ContainerType).Select(p => p.Name);
        var rule = new ModelClientValidationRule
        {
            ErrorMessage = this.ErrorMessage
        };
        rule.ValidationType = string.Format("group", GroupName.ToLower());
        rule.ValidationParameters["propertynames"] = string.Join(",", groupProperties);
        yield return rule;
    }
}

それでは、コントローラーを定義しましょう。

public class HomeController : Controller
{
    public ActionResult Index()
    {
        var model = new SomeViewModel();
        return View(model);        
    }

    [HttpPost]
    public ActionResult Index(SomeViewModel model)
    {
        return View(model);
    }
}

とビュー:

@model SomeViewModel

<script src="@Url.Content("~/Scripts/jquery.validate.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.js")" type="text/javascript"></script>

@using (Html.BeginForm())
{
    @Html.EditorFor(x => x.IsA)
    @Html.ValidationMessageFor(x => x.IsA)
    <br/>
    @Html.EditorFor(x => x.IsB)<br/>
    @Html.EditorFor(x => x.IsC)<br/>

    @Html.EditorFor(x => x.IsY)
    @Html.ValidationMessageFor(x => x.IsY)
    <br/>
    @Html.EditorFor(x => x.IsZ)<br/>
    <input type="submit" value="OK" />
}

残っている最後の部分は、クライアント側の検証用のアダプターを登録することです。

jQuery.validator.unobtrusive.adapters.add(
    'group', 
    [ 'propertynames' ],
    function (options) {
        options.rules['group'] = options.params;
        options.messages['group'] = options.message;
    }
);

jQuery.validator.addMethod('group', function (value, element, params) {
    var properties = params.propertynames.split(',');
    var isValid = false;
    for (var i = 0; i < properties.length; i++) {
        var property = properties[i];
        if ($('#' + property).is(':checked')) {
            isValid = true;
            break;
        }
    }
    return isValid;
}, '');

特定の要件に基づいて、コードが適合される場合があります。

75
Darin Dimitrov

Jquery-validationteamからのrequire_from_groupの使用:

jQuery-validationプロジェクトには、srcフォルダーにadditionalというサブフォルダーがあります。 。あなたはそれをチェックすることができます ここ

そのフォルダーには、一般的ではない追加の検証メソッドが多数あります。そのため、デフォルトでは追加されません。

そのフォルダーに表示されているように、実際に必要な検証方法を選択して選択する必要がある方法が多数存在します。

あなたの質問に基づいて、あなたが必要とする検証方法は、追加のフォルダからrequire_from_groupという名前が付けられています。 ここ にあるこの関連ファイルをダウンロードして、Scriptsアプリケーションフォルダに配置するだけです。

このメソッドのドキュメントはこれを説明しています:

「セレクターYに一致する少なくともX個の入力を入力する必要があります」と言うことができます。

最終結果は、これらの入力のどちらも次のことではありません。

...それらの少なくとも1つが満たされない限り、検証されます。

部品番号:{require_from_group:[1、 "。productinfo"]}、説明:{require_from_group:[1、 "。productinfo"]}

options [0]:グループオプションに入力する必要のあるフィールドの数 2 :条件付きで必要なフィールドのグループを定義するCSSセレクター

この実装を選択する必要がある理由:

この検証方法は一般的であり、すべてのinput(テキスト、チェックボックス、ラジオなど)、textareaおよびselectに対して機能します。 このメソッドでは、入力する必要のある必要な入力の最小数を指定することもできます例:

partnumber:     {require_from_group: [2,".productinfo"]},
category:       {require_from_group: [2,".productinfo"]},
description:    {require_from_group: [2,".productinfo"]}

サーバー側とクライアント側の両方の検証に役立つ2つのクラスRequireFromGroupAttributeRequireFromGroupFieldAttributeを作成しました

RequireFromGroupAttributeクラス定義

RequireFromGroupAttributeAttributeからのみ派生します。このクラスは、構成のためだけに使用されます。検証のために入力する必要のあるフィールドの数を設定します。同じグループのすべての要素を取得するために検証メソッドで使用されるCSSセレクタークラスをこのクラスに提供する必要があります。必須フィールドのデフォルト数は1であるため、この属性は、指定されたグループの最小要件がデフォルト数よりも大きい場合にのみ、モデルを装飾するために使用されます。

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class RequireFromGroupAttribute : Attribute
{
    public const short DefaultNumber = 1;

    public string Selector { get; set; }

    public short Number { get; set; }

    public RequireFromGroupAttribute(string selector)
    {
        this.Selector = selector;
        this.Number = DefaultNumber;
    }

    public static short GetNumberOfRequiredFields(Type type, string selector)
    {
        var requiredFromGroupAttribute = type.GetCustomAttributes<RequireFromGroupAttribute>().SingleOrDefault(a => a.Selector == selector);
        return requiredFromGroupAttribute?.Number ?? DefaultNumber;
    }
}

RequireFromGroupFieldAttributeクラス定義

RequireFromGroupFieldAttributeから派生し、ValidationAttributeを実装するIClientValidatable。グループ検証に参加するモデルの各プロパティでこのクラスを使用する必要があります。 cssセレクタークラスを渡す必要があります。

[AttributeUsage(AttributeTargets.Property)]
public class RequireFromGroupFieldAttribute : ValidationAttribute, IClientValidatable
{
    public string Selector { get; }

    public bool IncludeOthersFieldName { get; set; }

    public RequireFromGroupFieldAttribute(string selector)
        : base("Please fill at least {0} of these fields")
    {
        this.Selector = selector;
        this.IncludeOthersFieldName = true;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var properties = this.GetInvolvedProperties(validationContext.ObjectType); ;
        var numberOfRequiredFields = RequireFromGroupAttribute.GetNumberOfRequiredFields(validationContext.ObjectType, this.Selector);

        var values = new List<object> { value };
        var otherPropertiesValues = properties.Where(p => p.Key.Name != validationContext.MemberName)
                                              .Select(p => p.Key.GetValue(validationContext.ObjectInstance));
        values.AddRange(otherPropertiesValues);

        if (values.Count(s => !string.IsNullOrWhiteSpace(Convert.ToString(s))) >= numberOfRequiredFields)
        {
            return ValidationResult.Success;
        }

        return new ValidationResult(this.GetErrorMessage(numberOfRequiredFields, properties.Values), new List<string> { validationContext.MemberName });
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var properties = this.GetInvolvedProperties(metadata.ContainerType);
        var numberOfRequiredFields = RequireFromGroupAttribute.GetNumberOfRequiredFields(metadata.ContainerType, this.Selector);
        var rule = new ModelClientValidationRule
        {
            ValidationType = "requirefromgroup",
            ErrorMessage = this.GetErrorMessage(numberOfRequiredFields, properties.Values)
        };
        rule.ValidationParameters.Add("number", numberOfRequiredFields);
        rule.ValidationParameters.Add("selector", this.Selector);

        yield return rule;
    }

    private Dictionary<PropertyInfo, string> GetInvolvedProperties(Type type)
    {
        return type.GetProperties()
                   .Where(p => p.IsDefined(typeof(RequireFromGroupFieldAttribute)) &&
                               p.GetCustomAttribute<RequireFromGroupFieldAttribute>().Selector == this.Selector)
                   .ToDictionary(p => p, p => p.IsDefined(typeof(DisplayAttribute)) ? p.GetCustomAttribute<DisplayAttribute>().Name : p.Name);
    }

    private string GetErrorMessage(int numberOfRequiredFields, IEnumerable<string> properties)
    {
        var errorMessage = string.Format(this.ErrorMessageString, numberOfRequiredFields);
        if (this.IncludeOthersFieldName)
        {
            errorMessage += ": " + string.Join(", ", properties);
        }

        return errorMessage;
    }
}

ビューモデルでどのように使用しますか?

あなたのモデルでは、これがそれを使用する方法です:

public class SomeViewModel
{
    internal const string GroupOne = "Group1";
    internal const string GroupTwo = "Group2";

    [RequireFromGroupField(GroupOne)]
    public bool IsA { get; set; }

    [RequireFromGroupField(GroupOne)]
    public bool IsB { get; set; }

    [RequireFromGroupField(GroupOne)]
    public bool IsC { get; set; }

    //... other properties

    [RequireFromGroupField(GroupTwo)]
    public bool IsY { get; set; }

    [RequireFromGroupField(GroupTwo)]
    public bool IsZ { get; set; }
}

デフォルトでは、必須フィールドのデフォルト数は1であるため、モデルをRequireFromGroupAttributeで装飾する必要はありません。ただし、必須フィールドの数を1と異なるものにする場合は、次のようにします。

[RequireFromGroup(GroupOne, Number = 2)]
public class SomeViewModel
{
    //...
}

ビューコードでどのように使用しますか?

@model SomeViewModel

<script src="@Url.Content("~/Scripts/jquery.validate.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/require_from_group.js")" type="text/javascript"></script>

@using (Html.BeginForm())
{
    @Html.CheckBoxFor(x => x.IsA, new { @class="Group1"})<span>A</span>
    @Html.ValidationMessageFor(x => x.IsA)
    <br />
    @Html.CheckBoxFor(x => x.IsB, new { @class = "Group1" }) <span>B</span><br />
    @Html.CheckBoxFor(x => x.IsC, new { @class = "Group1" }) <span>C</span><br />

    @Html.CheckBoxFor(x => x.IsY, new { @class = "Group2" }) <span>Y</span>
    @Html.ValidationMessageFor(x => x.IsY)
    <br />
    @Html.CheckBoxFor(x => x.IsZ, new { @class = "Group2" })<span>Z</span><br />
    <input type="submit" value="OK" />
}

RequireFromGroupField属性を使用するときに指定したグループセレクターは、グループに含まれる各入力のクラスとして指定することにより、ビューで使用されていることに注意してください。

サーバー側の検証は以上です。

クライアント側の検証について話しましょう。

GetClientValidationRulesクラスのRequireFromGroupFieldAttribute実装を確認すると、requirefromgroupプロパティのメソッド名としてrequire_from_groupではなく文字列ValidationTypeを使用していることがわかります。これは、ASP.Net MVCでは、検証タイプの名前に英数字の文字のみを含めることができ、数字で始めることはできないためです。したがって、次のJavaScriptを追加する必要があります。

$.validator.unobtrusive.adapters.add("requirefromgroup", ["number", "selector"], function (options) {
    options.rules["require_from_group"] = [options.params.number, options.params.selector];
    options.messages["require_from_group"] = options.message;
});

アダプター関数の実装では、検証を正しいrequire_from_groupメソッドに委任するだけなので、javascriptの部分は本当に単純です。

あらゆるタイプのinputtextarea、およびselect要素で機能するため、この方法の方が一般的だと思います。

お役に立てば幸いです。

4
CodeNotFound

ブール値ではなく文字列に追加したことを除いて、Darinのすばらしい答えをアプリケーションに実装しました。これは、名前/会社、または電話/電子メールのようなもののためのものでした。私は1つのマイナーなニッチを除いてそれを愛していました。

職場の電話、携帯電話、自宅の電話、または電子メールなしでフォームを送信しようとしました。クライアント側で4つの個別の検証エラーが発生しました。これは、エラーをなくすためにどのフィールドに入力できるかをユーザーに正確に知らせるため、私には問題ありません。

メールアドレスを入力しました。これで、電子メールでの1つの検証はなくなりましたが、3つは電話番号の下に残りました。これらもエラーではなくなりました。

そこで、これを説明するために検証をチェックするjQueryメソッドを再割り当てしました。以下のコード。それが誰かを助けることを願っています。

jQuery.validator.prototype.check = function (element) {

   var elements = [];
   elements.Push(element);
   var names;

   while (elements.length > 0) {
      element = elements.pop();
      element = this.validationTargetFor(this.clean(element));

      var rules = $(element).rules();

      if ((rules.group) && (rules.group.propertynames) && (!names)) {
         names = rules.group.propertynames.split(",");
         names.splice($.inArray(element.name, names), 1);

         var name;
         while (name = names.pop()) {
            elements.Push($("#" + name));
         }
      }

      var dependencyMismatch = false;
      var val = this.elementValue(element);
      var result;

      for (var method in rules) {
         var rule = { method: method, parameters: rules[method] };
         try {

            result = $.validator.methods[method].call(this, val, element, rule.parameters);

            // if a method indicates that the field is optional and therefore valid,
            // don't mark it as valid when there are no other rules
            if (result === "dependency-mismatch") {
               dependencyMismatch = true;
               continue;
            }
            dependencyMismatch = false;

            if (result === "pending") {
               this.toHide = this.toHide.not(this.errorsFor(element));
               return;
            }

            if (!result) {
               this.formatAndAdd(element, rule);
               return false;
            }
         } catch (e) {
            if (this.settings.debug && window.console) {
               console.log("Exception occurred when checking element " + element.id + ", check the '" + rule.method + "' method.", e);
            }
            throw e;
         }
      }
      if (dependencyMismatch) {
         return;
      }
      if (this.objectLength(rules)) {
         this.successList.Push(element);
      }
   }

   return true;
};
1
Paul

これが古いスレッドであることは知っていますが、同じシナリオに出くわし、いくつかの解決策を見つけて、上記のマットの質問を解決するものを見たので、この答えに出くわした人たちと共有したいと思いました。チェックアウト: MVC3の控えめな入力の検証グループ

0
Chris Searles