5

My model ( class A ) has a property ( called b ) of type B with IValidatableObject implemented.

View has got @Html.ValidationSummary(true)

In the validation summary I want to exclude errors related to properties. In class B IValidatableObject implementation is returning ValidationResult with no memberNames

IValidatableObject但是由于 B 类是 A 类的属性,因此不显示B 类验证错误

如何显示 B 类非属性验证错误?

4

6 回答 6

3

我认为这非常简单,让我用一个例子来解释。 首先让我创建您面临的问题,然后我将解释如何解决。

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

编辑器模板

<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)现在视图看起来像,

在此处输入图像描述

5) 现在,如果我们点击提交。无需输入任何数据。它将按预期显示属性验证错误。

在此处输入图像描述

6) 现在如果我们在 MyProperty 中输入长度 > 10 的字符串,它应该会显示错误消息“MaxLength reached”,但不会显示错误:) :)

在此处输入图像描述

这样做的原因

所以,如果我们看到视图的代码,我们可以找到这一行

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

由于 CBProp 是 ClassA 中的一个属性,ValidationSummary(true)因此将排除 CBProp 中的任何错误。所以你不会发现正在显示的错误消息。无论如何,有几个选项可用于此。

选项

1套@Html.ValidationSummary()

这将显示错误消息,但它也会显示任何其他错误消息(这是多余的),例如,

在此处输入图像描述

2)@Html.ValidationSummary(true)在编辑器模板中设置。但它会显示错误消息,例如,

在此处输入图像描述

3) 在ClassB 的Validate 方法中,在ValidationResult 中指定属性名称和错误信息。现在它将被视为属性的验证错误,并@Html.ValidationMessageFor(model => model.MyProperty)在Editor Template 中显示。

代码

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

错误看起来像

在此处输入图像描述

我认为现在很清楚为什么没有显示错误消息,以及可用的选项有哪些。由您决定最适合您的方法。

干杯

于 2012-05-06T18:57:42.110 回答
2

虽然验证摘要的这种行为可能不适合您的情况,但通常必须将其视为“正确”。在模型中包含的子对象中创建错误时,会将正确的前缀添加到错误中。也就是说,如果子对象包含在属性中,MyProp则前缀MyProp会自动添加到所有错误中。这对于为创建的所有错误提供正确的名称是必要的 - 没有这个, theValidationsummary和 theValidationMessageFor都不会正常工作 - 因为它们指的是全名(包括整个前缀的那个)。这是避免歧义的唯一方法,因为您可能Name在两个不同的子对象中有两个属性。

但是,当子对象中生成的错误不是简单的属性级别错误而是“整个对象”级别错误时,这种“正确”行为通常是不合适的。在这种情况下,您可能希望它们出现在一般验证摘要中。

您可以通过两种方式面对这个问题:

  1. 使用另一个特定于子对象的验证摘要
  2. 错误冒泡 - 我经常使用错误冒泡来表示模型的子部分 - 未显示在屏幕上 - 包含错误,因此用户可以打开详细信息窗口(jQuery Dialog 或类似窗口)来查看它们。基本上,错误冒泡包括用 a 处理 ModelState 中的所有错误foreach,然后提升其中的一些。提升意味着删除错误前缀的最后一部分。在提升错误时,您可以保留或不保留原始错误 - 保留原始错误也更容易,并且在大多数情况下,这是正确的做法。请注意,在循环遍历所有条目时无法删除条目 - 您必须将其放入列表中,然后在循环终止后将其删除。

推广标准可能取决于您的需求。我给你举个例子:

  • 提升属性级错误会将其转换为对象级错误。
  • 提升子对象级别错误会将其转换为外部对象的对象级别错误。这是您应该感兴趣的情况 - 只需提升与ViewModel包含整个对象而不是简单值的根属性相关联的对象级错误!

错误处理可以在您可以定义的自定义 ActionFilter 中执行,并与多个操作方法一起重复使用。

下面是一个简单的 PromotionAttribute ActionFilter 的代码。它的用途是:

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

也就是说,你向它传递一个你想要提升到 Root 模型的表达式错误列表,如果它在 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);

            }
        }
    }
}
于 2012-05-04T16:20:22.253 回答
0

让我们从我们所知道的开始:

正如描述所暗示的,如果我们有我们的模型:

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="" />

如果我们将post 操作定义为:

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

然后在绑定到A模型时,DefaultModelBinder将查找命名的属性ModelB.Name,它将找到并成功绑定到该属性。

DefaultModelBinder但是,针对模型执行的模型验证A将调用为 Model 定义的验证B。此验证将返回未针对属性定义的错误消息,但由于此验证是复杂模型的一部分,因此它使用“ModelB”键添加到 ModelState。

ValidationSummary调用它时,它会查找空白的键(即为模型而不是属性定义的)。由于不存在空白键,因此不会显示错误。

作为一个简单的解决方法,您可以EditorTemplateB模型定义一个。

在包含您的视图的文件夹中,定义一个名为的文件夹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因此需要在 post 操作中单独绑定并添加到我们的A模型中:

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

现在,当modelB绑定时,验证消息将ModelState使用键“”写入。因此,现在可以使用@ValidationMessage()例程来显示它。

警告:上述变通方法假定modelB不具有与 相同的字段名称modelAmodelB例如,如果两者modelA都有一个Name字段,则DefaultModelBinder可能无法将字段绑定到其正确的等效项。例如,如果A模型还有一个名为的字段Name,则必须将其写入视图:

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

以确保正确绑定。

希望这应该允许您使用已在 MVC3 框架中定义的方法来实现所需的结果。

于 2012-05-04T10:35:08.927 回答
0

对于那些发现这个问题的人,请看一下ViewData.TemplateInfo.HtmlFieldPrefix(如其他答案中所述)......

如果显式添加摘要级别验证错误,您通常可以这样做(对对象)...

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.

于 2014-03-17T12:14:17.927 回答
0

挖出 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;
}
于 2012-05-03T14:29:19.403 回答
0

模型状态模型状态 = 默认(模型状态);Model.TryGetValue(this.ViewData.TemplateInfo.HtmlFieldPrefix, out modelState);

var isExcludePropertyErrors = modelState != null;

于 2018-01-12T11:40:31.573 回答