我的模型(A类)有一个B类属性(称为b),实现了IValidatableObject
。
视图已获得@Html.ValidationSummary(true)
在验证摘要中,我想排除与属性相关的错误。
在B类IValidatableObject
实现中返回ValidationResult
而没有memberNames
但由于B类是A类的属性,因此不会显示来自IValidatableObject
的B类诽谤错误
如何显示B类非属性验证错误?
答案 0 :(得分: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)现在,如果我们输入长度为&gt;的字符串10在MyProperty中,它应显示错误消息“MaxLength已达到”,但错误将不会显示 :):)
所以,如果我们看到视图的代码,我们可以找到行
@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)
显示。
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (!string.IsNullOrWhiteSpace(MyProperty) && MyProperty.Length > 10)
yield return new ValidationResult("MaxLength reached", new List<string> { "MyProperty" });
}
我认为现在很清楚为什么没有显示错误消息,以及有哪些可用选项。由您来决定最适合您的方法。
干杯
答案 1 :(得分:2)
虽然验证摘要的这种行为在您的情况下可能不合适,但通常必须将其视为“正确”。在模型中包含的子对象中创建错误时,会将正确的前缀添加到错误中。也就是说,如果子对象包含在属性MyProp
中,则前缀MyProp
会自动添加到所有错误中。这对于为所有创建的错误赋予正确的名称是必要的 - 如果没有这个,Validationsummary
和ValidationMessageFor
都不会正常工作 - 因为它们引用全名(包括整个前缀的名称) 。这是避免歧义的唯一方法,因为在两个不同的子对象中可能有两个Name
属性。
但是,当子对象中生成的错误不是简单的属性级别错误而是“整个对象”级错误时,这种“正确”行为通常是不合适的。在这种情况下,您可能希望它们出现在常规验证摘要中。
您可以通过两种方式面对这些问题:
foreach
处理ModelState中的所有错误,然后将其中的一些错误提升。提升意味着删除错误前缀的最后部分。在提升错误时,您可能会保留原始错误 - 保持原始错误也更容易,在大多数情况下,这是正确的做法。请注意,在循环遍历所有条目时无法删除条目 - 您必须将其放入列表中,然后在循环终止后将其删除。促销标准可能取决于您的需求。我举几个例子:
ViewModel
的属性相关联的对象级错误!错误处理可以在您定义的自定义ActionFilter中执行,并可以使用多种操作方法重复使用。
下面是一个简单的PromoteAttribute 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);
}
}
}
}
答案 2 :(得分: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;
}
答案 3 :(得分: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="" />
如果我们将后期操作定义为:
[HttpPost]
public ActionResult Index(A model)
{
if (ModelState.IsValid)
{
return RedirectToAction("NextAction");
}
return View();
}
然后,当绑定到A
模型时,DefaultModelBinder
会查找名为ModelB.Name
的属性,它将找到并成功绑定。
但是,DefaultModelBinder
针对A
模型执行的模型验证将调用为模型B
定义的验证。此验证将返回未针对属性定义的错误消息,但由于此验证是复杂模型的一部分,因此将其添加到具有“ModelB”键的ModelState中。
当调用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框架中已定义的方法来实现所需的结果。
答案 4 :(得分:0)
对于那些发现此问题的人,请查看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
获得。
答案 5 :(得分:0)
ModelState modelState = default(ModelState); Model.TryGetValue(this.ViewData.TemplateInfo.HtmlFieldPrefix,out modelState);
var isExcludePropertyErrors = modelState!= null;