Проверка MVC3 - требуется один из группы - PullRequest
36 голосов
/ 30 августа 2011

Учитывая следующую модель представления:

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

Я хочу создать пользовательский атрибут, который проверяет хотя бы одно из доступных свойств 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; }
}

Я хотел бы проверить на стороне клиента перед отправкой формы, так как значения в форме изменяются, поэтому я предпочитаю избегать атрибутов уровня класса, если это возможно.

Для этого потребуется проверка как на стороне сервера, так и на стороне клиента, чтобы найти все свойства, имеющие идентичные значения имени группы, переданные в качестве параметра для пользовательского атрибута. Это возможно? Любое руководство высоко ценится.

Ответы [ 4 ]

75 голосов
/ 30 августа 2011

Вот один из способов продолжения (есть и другие способы, я просто иллюстрирую тот, который будет соответствовать вашей модели представления как есть):

[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;
}, '');

Исходя из ваших конкретных требований, код может быть адаптирован.

4 голосов
/ 12 августа 2016

Использование require_from_group из команды jquery-validation:

jQuery-validation Проект имеет подпапку в папке src , которая называется Additional .Вы можете проверить это здесь .

В этой папке у нас есть много дополнительных методов проверки, которые не распространены, поэтому они не добавляются по умолчанию.

Как вы видите в этой папке, существует так много методов, которые вам нужно выбрать, выбрав, какой метод проверки вам действительно нужен.

Исходя из вашего вопроса, нужный метод проверки называется require_from_group из дополнительной папки.Просто скачайте этот связанный файл, который находится здесь и поместите его в папку приложения Scripts.

Документация этого метода объясняет это:

Позволяетвы говорите: «по крайней мере, X входов, которые соответствуют селектору Y, должны быть заполнены».

Конечным результатом является то, что ни один из этих входов:

... не будет проверен, если толькопо крайней мере один из них заполнен.

номер_партии: {require_from_group: [1, ". productinfo"]}, описание: {require_from_group: [1, ". productinfo"]}

опции[0]: количество полей, которые должны быть заполнены в параметрах группы 2 : селектор CSS, который определяет группу условно обязательных полей

Почему вам нужно выбрать эту реализацию:

Этот метод проверки является общим и работает для всех input (текст, флажок, радио и т. Д.), textarea и select. Этот метод также позволяет указать минимальное количество обязательных входных данных, которые необходимо заполнить например,

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

Я создал два класса RequireFromGroupAttribute и RequireFromGroupFieldAttribute, которые помогут вам в обоихпроверки на стороне сервера и на стороне клиента

RequireFromGroupAttribute определение класса

RequireFromGroupAttribute происходит только от Attribute.Класс используется только для настройки, например, для установки количества полей, которые необходимо заполнить для проверки.Вы должны предоставить этому классу CSS-класс селектора, который будет использоваться методом проверки для получения всех элементов в одной группе.Поскольку количество обязательных полей по умолчанию равно 1, этот атрибут используется только для украшения вашей модели, если минимальное требование в группе spcefied больше номера по умолчанию.

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

По умолчанию вам не нужно украшать вашу модель RequireFromGroupAttribute, потому что количество обязательных полей по умолчанию равно 1. Но если вы хотите, чтобы количество обязательных полей отличалось от 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;
});

Часть javascript действительно проста, потому что при реализации функции адаптера мы просто делегируем проверку правильному методу require_from_group.

Поскольку он работает с каждым типом элементов input, textarea и select, я могу подумать, что этот способ является более общим.

Надеюсь, что это поможет!

1 голос
/ 11 июля 2014

Я внедрил удивительный ответ Дарина в свое приложение, за исключением того, что добавил его для строк, а не для логических значений.Это было для таких вещей, как имя / компания или телефон / электронная почта.Я любил это за исключением одного незначительного придира.

Я пытался отправить свою форму без рабочего телефона, мобильного телефона, домашнего телефона или электронной почты.Я получил четыре отдельных ошибки проверки на стороне клиента.Это нормально для меня, потому что позволяет пользователям точно знать, какие поля можно заполнить, чтобы ошибка исчезла.

Я ввел адрес электронной почты.Теперь единственная проверка по электронной почте прошла, но все три остались под телефонными номерами.Это также больше не ошибки.

Итак, я переназначил метод 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;
};
0 голосов
/ 22 мая 2013

Я знаю, что это старая ветка, но я только что натолкнулся на тот же сценарий, нашел несколько решений и увидел решение, которое решает вопрос Мэтта выше, поэтому я подумал, что поделюсь тем, кто сталкивается с этим ответом.Проверьте: MVC3 ненавязчивая группа проверки входов

...