Подтвердить что ты не робот

Проверка MVC3 - требуется от группы

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

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

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

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

4b9b3361

Ответ 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;
}, '');

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

Ответ 2

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

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

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

Итак, я переназначил метод 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;
};

Ответ 3

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

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

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

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

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

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

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

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

  

... будет проверяться, если не заполнено хотя бы одно из них.

partnumber: {require_from_group: [1, ". productinfo" ]}, описание: {require_from_group: [1, ". productinfo" ]}

options [0]: количество полей, которые должны быть заполнены в группе options 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 разрешает только имя типа проверки содержать буквенно-цифровой char и не должно начинаться с числа. Поэтому вам нужно добавить следующий 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, я думаю, что этот способ более общий.

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

Ответ 4

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