mvc3 ValidationSummary исключает ошибки свойств IValidatableObject - PullRequest
5 голосов
/ 22 июня 2011

Моя модель (класс A) имеет свойство (называемое b) типа B с реализованным IValidatableObject.

Просмотр получил @Html.ValidationSummary(true)

В сводке проверки я хочу исключить ошибки, связанные со свойствами. В классе B IValidatableObject реализация возвращает ValidationResult без memberNames

Но ошибки валидации класса B из IValidatableObject не отображаются, поскольку класс B является свойством класса A

Как отобразить ошибки проверки не-свойства класса B?

Ответы [ 6 ]

3 голосов
/ 06 мая 2012

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

1) Объявить мои модели.

public class ClassA
{
    [Required]
    public string Name { get; set; }
    public ClassB CBProp { get; set; }
}

public class ClassB:IValidatableObject
{
    [Required]
    public string MyProperty { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (!string.IsNullOrWhiteSpace(MyProperty) && MyProperty.Length > 10)
            yield return new ValidationResult("MaxLength reached");
    }
}

2) Объявлять простые действия.

public class HomeController : Controller
{       
    [HttpGet]
    public ActionResult Test()
    {
        ClassA ca = new ClassA();
        return View(ca);
    }

    [HttpPost]
    public ActionResult Test(ClassA ca)
    {            
        return View(ca);
    }
}

3) Позвольте мне создать простое представление и шаблон редактора для ClassB.

Тестовый просмотр:

@using (Html.BeginForm()) {
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>ClassA</legend>

        <div class="editor-label">
            @Html.LabelFor(model => model.Name)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Name)
            @Html.ValidationMessageFor(model => model.Name)
        </div>
            @Html.EditorFor(m => m.CBProp, "TestB")    
        <p>
            <input type="submit" value="Create" />
        </p>
    </fieldset>
}

EditorTemplate

<div class="editor-label">
        @Html.LabelFor(model => model.MyProperty)
    </div>
    <div class="editor-field">
        @Html.EditorFor(model => model.MyProperty)
        @Html.ValidationMessageFor(model => model.MyProperty)
    </div>

4) Теперь вид будет выглядеть,

enter image description here

5) Теперь, если мы нажмем кнопку «Отправить». без ввода каких-либо данных. Будет отображаться ошибка проверки свойства, как и ожидалось.

enter image description here

6) Теперь, если мы введем строку с длиной> 10 в MyProperty, она должна показать сообщение об ошибке «MaxLength достигла», Но ошибка не будет отображаться :):)

enter image description here

Причина этого

Итак, если мы видим код для представления, мы можем найти строку

 @Html.ValidationSummary(true)  /// true, will excludePropertyErrors

Поскольку CBProp является свойством в ClassA, ValidationSummary(true) исключит любую ошибку в CBProp. Таким образом, вы не найдете сообщение об ошибке отображается. Как бы то ни было, есть несколько вариантов для этого.

Опция

1) Набор @Html.ValidationSummary()

Это отобразит сообщение об ошибке, но также отобразит любое другое сообщение об ошибке (которое является избыточным), например,

enter image description here

2) Установите @Html.ValidationSummary(true) в шаблоне редактора. Но он покажет сообщение об ошибке, как,

enter image description here

3) В методе Validate ClassB укажите имя свойства вместе с сообщением об ошибке в ValidationResult. Теперь оно будет рассматриваться как ошибка проверки свойства и будет отображаться @Html.ValidationMessageFor(model => model.MyProperty) в шаблоне редактора.

Код

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (!string.IsNullOrWhiteSpace(MyProperty) && MyProperty.Length > 10)
            yield return new ValidationResult("MaxLength reached", new List<string> { "MyProperty" });
    }

Ошибка выглядит как

enter image description here

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

Приветствия

2 голосов
/ 04 мая 2012

Несмотря на то, что такое поведение резюме проверки может быть неуместным в вашем случае, его в целом следует считать «правильным».Когда ошибки создаются в подобъектах, содержащихся в модели, к ошибкам добавляется правильный префикс.То есть, если подобъект содержится в свойстве MyProp, то префикс MyProp автоматически добавляется ко всем ошибкам.Это необходимо, чтобы дать правильное имя всем создаваемым ошибкам - без этого ни Validationsummary, ни ValidationMessageFor не будут работать должным образом - потому что они ссылаются на полные имена (одна, включая полные префиксы).Это единственный способ избежать двусмысленности, потому что у вас может быть два свойства Name в двух разных подобъектах.

Однако часто такое «правильное» поведение не подходит, когда ошибки, генерируемые в подэлементеОбъект - это не просто ошибки уровня свойств, а ошибки уровня "всего объекта".В таких случаях вы можете пожелать, чтобы они появлялись в общей сводке проверки.

С этими проблемами вы можете столкнуться двумя способами:

  1. Использование другой сводки проверки, специфичной для подобъекта
  2. Пузыри ошибок - я часто использую пузыри ошибок, чтобы сигнализировать, что части модели - не показанные на экране - содержат ошибки, поэтому пользователь может открыть подробное окно (диалоговое окно jQuery или подобное), чтобы увидеть их.По сути, пузыри ошибок заключаются в обработке всех ошибок в ModelState с foreach, а затем в продвижении некоторых из них.Повышение означает удаление последней части префикса ошибки.При объявлении ошибки вы можете оставить исходную ошибку или нет - сохранить исходную ошибку тоже проще и в большинстве случаев это правильная вещь.Обратите внимание, что вы не можете удалить запись во время цикла всех записей - вы должны поместить ее в список, а затем удалить ее после завершения цикла.

Критерии продвижения могут зависеть от ваших потребностей,Я приведу вам несколько примеров:

  • Повышение ошибки уровня свойства превращает ее в ошибку уровня объекта.
  • Повышение ошибки уровня подчиненного объекта превращает ее в ошибку уровня объектавнешний объект.Это тот случай, который должен вас заинтересовать - просто сообщите об ошибках уровня объекта, связанных со свойствами корня ViewModel, который содержит простые объекты вместо простых значений!

Обработка ошибок может быть выполнена в течениепользовательский ActionFilter, который вы можете определить и повторно использовать с несколькими методами действий.

Ниже приведен код простого PromoteAttribute ActionFilter.Он используется:

[Promote("prop1.SubProp1 Prop2")]
public ActionResult MyMethod( ...

То есть вы передаете ему список ошибок выражений, которые вы хотели бы добавить в корневую модель, и если он находит ошибки, соответствующие им в ModelState, он продвигает их - очевидно, это простопростой пример - вы можете продвигать только один уровень вверх вместо корневого, и вы можете использовать сложный критерий для поиска ошибок, которые нужно продвигать, вместо перечисления их:

public class PromoteAttribute : ActionFilterAttribute
{
    string[] expressions;
    public PromoteAttribute(string toPromote)
    {
        expressions = toPromote.Split(' ');
    }
    public override void OnActionExecuted(ActionExecutedContext filterContext)
    {
        ModelStateDictionary modelState=filterContext.Controller.ViewData.ModelState;
        foreach(var x in expressions)
        {
            if (modelState.ContainsKey(x))
            {
                var entry = modelState[x];
                if (entry.Errors.Count == 0) continue; 

                foreach (var error in entry.Errors) modelState.AddModelError("", error.ErrorMessage);

            }
        }
    }
}
0 голосов
/ 12 января 2018

ModelState modelState = по умолчанию (ModelState); Model.TryGetValue (this.ViewData.TemplateInfo.HtmlFieldPrefix, out modelState);

var isExcludePropertyErrors = modelState! = Null;

0 голосов
/ 17 марта 2014

Для тех, кто находит этот вопрос, взгляните на ViewData.TemplateInfo.HtmlFieldPrefix (как уже упоминалось в других ответах) ...

Если явно добавить ошибку проверки сводного уровня, вы обычно можете сделать это (для объекта root ) ...

ModelState.AddModelError("", "This is a summary level error text for the model");

И при добавлении итоговой ошибки проверки для свойства модели, которая сама по себе является объектом, вы можете сделать следующее:

ModelState.AddModelError("b", "This is a 'summary' error for the property named b");

Где b - это имя свойства, которое само по себе является свойством.

Для пояснения: при непосредственном добавлении ошибки проверки сводного уровня можно просто указать префикс HTML для свойства объекта.

Это можно получить, используя ViewData.TemplateInfo.HtmlFieldPrefix.

0 голосов
/ 04 мая 2012

Давайте начнем с того, что мы знаем:

Как следует из описания, если у нас есть наши модели:

Модель A:

public class A
{
    public B ModelB { get; set; }
}

Модель B:

public class B : IValidatableObject
{
    public string Name { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        List<ValidationResult> errors = new List<ValidationResult>();

        if (string.IsNullOrEmpty(Name)) {
            errors.Add(new ValidationResult("Please enter your name"));
        }

        return errors;
    }
}

И наш взгляд:

@model A

@Html.ValidationSummary(true)

@using (Html.BeginForm())
{
    @Html.EditorFor(model => model.ModelB.Name)

    <input type="submit" value="submit" />
}

Тогда редактор выведет строку:

<input class="text-box single-line" id="ModelB_Name" name="ModelB.Name" type="text" value="" />

Если у нас есть сообщение , определенное как:

[HttpPost]
public ActionResult Index(A model)
{
    if (ModelState.IsValid)
    {
        return RedirectToAction("NextAction");
    }
    return View();
}

Затем при привязке к модели A, DefaultModelBinder будет искать свойство с именем ModelB.Name, которое будет найдено и успешно привязано.

Однако проверка модели, выполненная DefaultModelBinder в сравнении с моделью A, вызовет проверку, определенную для модели B. Эта проверка вернет сообщение об ошибке, которое не определено для свойства, но поскольку эта проверка является частью сложной модели, она добавляется в ModelState с ключом «ModelB».

Когда вызывается ValidationSummary, он ищет ключи, которые не заполнены (т.е. определены для модели, а не свойства). Поскольку пустых ключей не существует, ошибка не отображается.

В качестве простого обходного пути вы можете определить EditorTemplate для модели B.

В папке, содержащей ваш вид, определите папку с именем EditorTemplates, и в этом случае создайте вид с тем же именем, что и у модели (например, B.cshtml в моем случае). Содержимое шаблона редактора будет определено как:

@model MvcApplication14.Models.B

@Html.EditorFor(m => m.Name)

Затем измените основной вид, чтобы EditorFor выглядело так:

@Html.EditorFor(model => model.ModelB, null, "")

"" указывает, что никакие поля, выводимые нашим шаблоном редактора, не будут иметь префикс имени к полю. Следовательно, теперь вывод будет:

<input class="text-box single-line" id="Name" name="Name" type="text" value="" /> 

Однако теперь это предотвратит связывание ModelB, поэтому его нужно будет отдельно связать в пост-действии и добавить к нашей A модели:

[HttpPost]
public ActionResult Index(A modelA, B modelB)
{
    modelA.ModelB = modelB;
    if (ModelState.IsValid)
    {
        return RedirectToAction("NextAction");
    }
    return View();
}

Теперь, когда modelB привязан, сообщение проверки будет записано в ModelState с ключом "". Следовательно, теперь это можно отобразить с помощью процедуры @ValidationMessage().

Предупреждение : Обходное решение предполагает, что modelB не имеет тех же имен полей, что и modelA. Например, если modelB и modelA имели поле Name, то DefaultModelBinder может не связывать поля с их правильными эквивалентами. Например, если модель A также имеет поле с именем Name, тогда оно должно быть записано в представление как:

@Html.EditorFor(model => model.Name, null, "modelA.Name")

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

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

0 голосов
/ 03 мая 2012

Выкопал исходный код MVC3 и отредактировал, чтобы разрешить включение свойств.

@Html.ValidationSummary(new [] { "PropertyName" })

будет включать свойство с именем PropertyName

@Html.ValidationSummary(new [] { "ArrayName[]" })

будет включать свойства ArrayName [0], ArrayName [1] и т. Д.

@Html.ValidationSummary(new [] { "ArrayName[]", "PropertyName" })

будет включать оба.

public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, string[] includePropertyErrors)
{
    return ValidationSummary(htmlHelper, includePropertyErrors, null, null);
}

public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, string[] includePropertyErrors, string message)
{
    return ValidationSummary(htmlHelper, includePropertyErrors, message, null);
}

public static MvcHtmlString ValidationSummary(this HtmlHelper htmlHelper, string[] includePropertyErrors, string message, IDictionary<string, object> htmlAttributes)
{
    if (htmlHelper == null)
    {
        throw new ArgumentNullException("htmlHelper");
    }

    FormContext formContext = htmlHelper.ViewContext.ClientValidationEnabled ? htmlHelper.ViewContext.FormContext : null;
    if (htmlHelper.ViewData.ModelState.IsValid)
    {
        if (formContext == null)
        {  // No client side validation
            return null;
        }

        // TODO: This isn't really about unobtrusive; can we fix up non-unobtrusive to get rid of this, too?
        if (htmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled)
        {  // No client-side updates
            return null;
        }
    }

    string messageSpan;
    if (!string.IsNullOrEmpty(message))
    {
        TagBuilder spanTag = new TagBuilder("span");
        spanTag.SetInnerText(message);
        messageSpan = spanTag.ToString(TagRenderMode.Normal) + Environment.NewLine;
    }
    else
    {
        messageSpan = null;
    }

    StringBuilder htmlSummary = new StringBuilder();
    TagBuilder unorderedList = new TagBuilder("ul");

    IEnumerable<ModelState> modelStates = from ms in htmlHelper.ViewData.ModelState
                                            where ms.Key == htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix ||
                                                includePropertyErrors.Any(property =>
                                                {
                                                    string prefixedProperty = string.IsNullOrEmpty(htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix) ? property : htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix + "." + property;
                                                    if (property.EndsWith("[]"))
                                                    {
                                                        return prefixedProperty.Substring(0, property.Length - 2) == Regex.Replace(ms.Key, @"\[[^\]]+\]", string.Empty);
                                                    }
                                                    else
                                                    {
                                                        return property == ms.Key;
                                                    }
                                                })
                                            select ms.Value;

    if (modelStates != null)
    {
        foreach (ModelState modelState in modelStates)
        {
            foreach (ModelError modelError in modelState.Errors)
            {
                string errorText = GetUserErrorMessageOrDefault(htmlHelper.ViewContext.HttpContext, modelError);
                if (!String.IsNullOrEmpty(errorText))
                {
                    TagBuilder listItem = new TagBuilder("li");
                    listItem.SetInnerText(errorText);
                    htmlSummary.AppendLine(listItem.ToString(TagRenderMode.Normal));
                }
            }
        }
    }

    if (htmlSummary.Length == 0)
    {
        htmlSummary.AppendLine(@"<li style=""display:none""></li>");
    }

    unorderedList.InnerHtml = htmlSummary.ToString();

    TagBuilder divBuilder = new TagBuilder("div");
    divBuilder.MergeAttributes(htmlAttributes);
    divBuilder.AddCssClass((htmlHelper.ViewData.ModelState.IsValid) ? HtmlHelper.ValidationSummaryValidCssClassName : HtmlHelper.ValidationSummaryCssClassName);
    divBuilder.InnerHtml = messageSpan + unorderedList.ToString(TagRenderMode.Normal);

    if (formContext != null)
    {
        if (!htmlHelper.ViewContext.UnobtrusiveJavaScriptEnabled)
        {
            // client val summaries need an ID
            divBuilder.GenerateId("validationSummary");
            formContext.ValidationSummaryId = divBuilder.Attributes["id"];
            formContext.ReplaceValidationSummary = false;
        }
    }

    return new MvcHtmlString(divBuilder.ToString(TagRenderMode.Normal));
}

private static string GetUserErrorMessageOrDefault(HttpContextBase httpContext, ModelError error)
{
    return string.IsNullOrEmpty(error.ErrorMessage) ? null : error.ErrorMessage;
}
...