Add ValidationSummary helper.

This enables Html.ValidationSummary.  This is in reference to WEBFX-97.
This commit is contained in:
N. Taylor Mullen 2014-03-31 15:01:50 -07:00
parent b9010072aa
commit dad87c5239
11 changed files with 289 additions and 1 deletions

View File

@ -10,6 +10,13 @@ namespace MvcSample.Web
return View("MyView", User());
}
public IActionResult ValidationSummary()
{
ModelState.AddModelError("something", "Something happened, show up in validation summary.");
return View("ValidationSummary");
}
/// <summary>
/// Action that shows metadata when model is <c>null</c>.
/// </summary>

View File

@ -0,0 +1,23 @@
<style>
.validationSummary {
display: inline-block;
padding: 10px;
border-radius: 5px;
border: 1px solid black;
background-color: #e4e3e3;
}
</style>
<h1>ValidationSummary Test Page.</h1>
<p>Below are all overloads for Html.ValidationSummary. You should see 5 validation summary titles and 4 validation summary error messages.</p>
<br />
<section class="validationSummary">
@Html.ValidationSummary()
@Html.ValidationSummary(excludePropertyErrors: true)
@Html.ValidationSummary(message: "Hello from validation message summary 1.")
@Html.ValidationSummary(excludePropertyErrors: true, message: "Hello from validation message summary 2")
@Html.ValidationSummary(message: "Hello from validation message summary 3", htmlAttributes: new { style = "color: red" })
@Html.ValidationSummary(excludePropertyErrors: true, message: "Hello from validation message summary 4", htmlAttributes: new { style = "color: green" })
@Html.ValidationSummary(message: "Hello from validation message summary 5", htmlAttributes: new Dictionary<string, object> { { "style", "color: blue" } })
</section>

View File

@ -7,6 +7,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class ModelMetadata
{
public static readonly int DefaultOrder = 10000;
private readonly Type _containerType;
private readonly Type _modelType;
private readonly string _propertyName;
@ -15,6 +17,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private bool _convertEmptyStringToNull = true;
private object _model;
private Func<object> _modelAccessor;
private int _order = DefaultOrder;
private IEnumerable<ModelMetadata> _properties;
private Type _realModelType;
@ -57,6 +60,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public virtual bool IsReadOnly { get; set; }
public virtual int Order
{
get { return _order; }
set { _order = value; }
}
public object Model
{
get

View File

@ -5,6 +5,7 @@ using System.Globalization;
using System.IO;
using System.Net;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Abstractions;
@ -22,6 +23,8 @@ namespace Microsoft.AspNet.Mvc.Rendering
public static readonly string ValidationSummaryCssClassName = "validation-summary-errors";
public static readonly string ValidationSummaryValidCssClassName = "validation-summary-valid";
private const string HiddenListItem = @"<li style=""display:none""></li>";
private ViewContext _viewContext;
private IViewEngine _viewEngine;
@ -175,6 +178,97 @@ namespace Microsoft.AspNet.Mvc.Rendering
await viewEngineResult.View.RenderAsync(newViewContext);
}
public virtual HtmlString ValidationSummary(bool excludePropertyErrors, string message, IDictionary<string, object> htmlAttributes)
{
var formContext = ViewContext.ClientValidationEnabled ? ViewContext.FormContext : null;
if (ViewData.ModelState.IsValid == true)
{
if (formContext == null ||
ViewContext.UnobtrusiveJavaScriptEnabled &&
excludePropertyErrors)
{
// No client side validation/updates
return HtmlString.Empty;
}
}
string messageSpan;
if (!string.IsNullOrEmpty(message))
{
var spanTag = new TagBuilder("span");
spanTag.SetInnerText(message);
messageSpan = spanTag.ToString(TagRenderMode.Normal) + Environment.NewLine;
}
else
{
messageSpan = null;
}
var htmlSummary = new StringBuilder();
var modelStates = ValidationHelpers.GetModelStateList(ViewData, excludePropertyErrors);
foreach (var modelState in modelStates)
{
foreach (var modelError in modelState.Errors)
{
string errorText = ValidationHelpers.GetUserErrorMessageOrDefault(modelError, modelState: null);
if (!string.IsNullOrEmpty(errorText))
{
var listItem = new TagBuilder("li");
listItem.SetInnerText(errorText);
htmlSummary.AppendLine(listItem.ToString(TagRenderMode.Normal));
}
}
}
if (htmlSummary.Length == 0)
{
htmlSummary.AppendLine(HiddenListItem);
}
var unorderedList = new TagBuilder("ul")
{
InnerHtml = htmlSummary.ToString()
};
var divBuilder = new TagBuilder("div");
divBuilder.MergeAttributes(htmlAttributes);
if (ViewData.ModelState.IsValid == true)
{
divBuilder.AddCssClass(HtmlHelper.ValidationSummaryValidCssClassName);
}
else
{
divBuilder.AddCssClass(HtmlHelper.ValidationSummaryCssClassName);
}
divBuilder.InnerHtml = messageSpan + unorderedList.ToString(TagRenderMode.Normal);
if (formContext != null)
{
if (ViewContext.UnobtrusiveJavaScriptEnabled)
{
if (!excludePropertyErrors)
{
// Only put errors in the validation summary if they're supposed to be included there
divBuilder.MergeAttribute("data-valmsg-summary", "true");
}
}
else
{
// client validation summaries need an ID
divBuilder.GenerateId("validationSummary", IdAttributeDotReplacement);
formContext.ValidationSummaryId = divBuilder.Attributes["id"];
formContext.ReplaceValidationSummary = !excludePropertyErrors;
}
}
return divBuilder.ToHtmlString(TagRenderMode.Normal);
}
/// <summary>
/// Returns the HTTP method that handles form input (GET or POST) as a string.
/// </summary>

View File

@ -2,6 +2,8 @@
{
public class HtmlString
{
private static readonly HtmlString _empty = new HtmlString(string.Empty);
private readonly string _input;
public HtmlString(string input)
@ -9,6 +11,14 @@
_input = input;
}
public static HtmlString Empty
{
get
{
return _empty;
}
}
public override string ToString()
{
return _input;

View File

@ -0,0 +1,42 @@
using System.Collections.Generic;
namespace Microsoft.AspNet.Mvc.Rendering
{
public static class ValidationExtensions
{
public static HtmlString ValidationSummary<T>(this IHtmlHelper<T> htmlHelper)
{
return ValidationSummary(htmlHelper, excludePropertyErrors: false);
}
public static HtmlString ValidationSummary<T>(this IHtmlHelper<T> htmlHelper, bool excludePropertyErrors)
{
return ValidationSummary(htmlHelper, excludePropertyErrors, message: null);
}
public static HtmlString ValidationSummary<T>(this IHtmlHelper<T> htmlHelper, string message)
{
return ValidationSummary(htmlHelper, excludePropertyErrors: false, message: message, htmlAttributes: (object)null);
}
public static HtmlString ValidationSummary<T>(this IHtmlHelper<T> htmlHelper, bool excludePropertyErrors, string message)
{
return ValidationSummary(htmlHelper, excludePropertyErrors, message, htmlAttributes: (object)null);
}
public static HtmlString ValidationSummary<T>(this IHtmlHelper<T> htmlHelper, string message, object htmlAttributes)
{
return ValidationSummary(htmlHelper, excludePropertyErrors: false, message: message, htmlAttributes: HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}
public static HtmlString ValidationSummary<T>(this IHtmlHelper<T> htmlHelper, bool excludePropertyErrors, string message, object htmlAttributes)
{
return htmlHelper.ValidationSummary(excludePropertyErrors, message, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
}
public static HtmlString ValidationSummary<T>(this IHtmlHelper<T> htmlHelper, string message, IDictionary<string, object> htmlAttributes)
{
return htmlHelper.ValidationSummary(excludePropertyErrors: false, message: message, htmlAttributes: htmlAttributes);
}
}
}

View File

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNet.Mvc.ModelBinding;
namespace Microsoft.AspNet.Mvc.Rendering
{
internal static class ValidationHelpers
{
public static string GetUserErrorMessageOrDefault(ModelError modelError, ModelState modelState)
{
if (!string.IsNullOrEmpty(modelError.ErrorMessage))
{
return modelError.ErrorMessage;
}
if (modelState == null)
{
return string.Empty;
}
var attemptedValue = (modelState.Value != null) ? modelState.Value.AttemptedValue : "null";
return Resources.FormatCommon_ValueNotValidForProperty(attemptedValue);
}
// Returns non-null list of model states, which caller will render in order provided.
public static IEnumerable<ModelState> GetModelStateList(ViewDataDictionary viewData, bool excludePropertyErrors)
{
if (excludePropertyErrors)
{
ModelState ms;
viewData.ModelState.TryGetValue(viewData.TemplateInfo.HtmlFieldPrefix, out ms);
if (ms != null)
{
return new[] { ms };
}
return Enumerable.Empty<ModelState>();
}
else
{
// Sort modelStates to respect the ordering in the metadata.
// ModelState doesn't refer to ModelMetadata, but we can correlate via the property name.
var ordering = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var metadata = viewData.ModelMetadata;
if (metadata != null)
{
foreach (var data in metadata.Properties)
{
ordering[data.PropertyName] = data.Order;
}
return viewData.ModelState
.OrderBy(data => ordering[data.Key])
.Select(ms => ms.Value);
}
return viewData.ModelState.Values;
}
}
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Linq.Expressions;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Microsoft.AspNet.Mvc.Rendering
@ -99,5 +100,14 @@ namespace Microsoft.AspNet.Mvc.Rendering
/// <param name="viewData">A <see cref="ViewDataDictionary"/> to pass into the partial view.</param>
/// <returns>A task that represents when rendering has completed.</returns>
Task RenderPartialAsync([NotNull] string partialViewName, object model, ViewDataDictionary viewData);
/// <summary>
/// Returns an unordered list (ul element) of validation messages that are in the <see cref="ModelStateDictionary"/> object.
/// </summary>
/// <param name="excludePropertyErrors">true to have the summary display model-level errors only, or false to have the summary display all errors.</param>
/// <param name="message">The message to display with the validation summary.</param>
/// <param name="htmlAttributes">A dictionary that contains the HTML attributes for the element.</param>
/// <returns>An <see cref="HtmlString"/> that contains an unordered list (ul element) of validation messages.</returns>
HtmlString ValidationSummary(bool excludePropertyErrors, string message, IDictionary<string, object> htmlAttributes);
}
}

View File

@ -74,6 +74,22 @@ namespace Microsoft.AspNet.Mvc.Rendering
return string.Format(CultureInfo.CurrentCulture, GetString("Common_PartialViewNotFound"), p0, p1);
}
/// <summary>
/// The value '{0}' is invalid.
/// </summary>
internal static string Common_ValueNotValidForProperty
{
get { return GetString("Common_ValueNotValidForProperty"); }
}
/// <summary>
/// The value '{0}' is invalid.
/// </summary>
internal static string FormatCommon_ValueNotValidForProperty(object p0)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Common_ValueNotValidForProperty"), p0);
}
/// <summary>
/// ViewData value must not be null.
/// </summary>

View File

@ -129,6 +129,9 @@
<data name="Common_PartialViewNotFound" xml:space="preserve">
<value>The partial view '{0}' was not found or no view engine supports the searched locations. The following locations were searched:{1}</value>
</data>
<data name="Common_ValueNotValidForProperty" xml:space="preserve">
<value>The value '{0}' is invalid.</value>
</data>
<data name="DynamicViewData_ViewDataNull" xml:space="preserve">
<value>ViewData value must not be null.</value>
</data>

View File

@ -16,7 +16,10 @@ namespace Microsoft.AspNet.Mvc.Rendering
public ViewContext([NotNull] ViewContext viewContext)
: this(viewContext.ServiceProvider, viewContext.HttpContext, viewContext.ViewEngineContext)
{ }
{
UnobtrusiveJavaScriptEnabled = viewContext.UnobtrusiveJavaScriptEnabled;
ClientValidationEnabled = viewContext.ClientValidationEnabled;
}
public ViewContext(IServiceProvider serviceProvider, HttpContext httpContext,
IDictionary<string, object> viewEngineContext)
@ -25,6 +28,8 @@ namespace Microsoft.AspNet.Mvc.Rendering
HttpContext = httpContext;
ViewEngineContext = viewEngineContext;
_formContext = _defaultFormContext;
UnobtrusiveJavaScriptEnabled = true;
ClientValidationEnabled = true;
}
public IViewComponentHelper Component { get; set; }
@ -48,6 +53,10 @@ namespace Microsoft.AspNet.Mvc.Rendering
public IUrlHelper Url { get; set; }
public bool UnobtrusiveJavaScriptEnabled { get; set; }
public bool ClientValidationEnabled { get; set; }
public dynamic ViewBag
{
get