Skip to content

Commit dad87c5

Browse files
author
N. Taylor Mullen
committed
Add ValidationSummary helper.
This enables Html.ValidationSummary. This is in reference to WEBFX-97.
1 parent b901007 commit dad87c5

File tree

11 files changed

+289
-1
lines changed

11 files changed

+289
-1
lines changed

samples/MvcSample.Web/HomeController.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ public IActionResult Index()
1010
return View("MyView", User());
1111
}
1212

13+
public IActionResult ValidationSummary()
14+
{
15+
ModelState.AddModelError("something", "Something happened, show up in validation summary.");
16+
17+
return View("ValidationSummary");
18+
}
19+
1320
/// <summary>
1421
/// Action that shows metadata when model is <c>null</c>.
1522
/// </summary>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<style>
2+
.validationSummary {
3+
display: inline-block;
4+
padding: 10px;
5+
border-radius: 5px;
6+
border: 1px solid black;
7+
background-color: #e4e3e3;
8+
}
9+
</style>
10+
11+
<h1>ValidationSummary Test Page.</h1>
12+
<p>Below are all overloads for Html.ValidationSummary. You should see 5 validation summary titles and 4 validation summary error messages.</p>
13+
<br />
14+
15+
<section class="validationSummary">
16+
@Html.ValidationSummary()
17+
@Html.ValidationSummary(excludePropertyErrors: true)
18+
@Html.ValidationSummary(message: "Hello from validation message summary 1.")
19+
@Html.ValidationSummary(excludePropertyErrors: true, message: "Hello from validation message summary 2")
20+
@Html.ValidationSummary(message: "Hello from validation message summary 3", htmlAttributes: new { style = "color: red" })
21+
@Html.ValidationSummary(excludePropertyErrors: true, message: "Hello from validation message summary 4", htmlAttributes: new { style = "color: green" })
22+
@Html.ValidationSummary(message: "Hello from validation message summary 5", htmlAttributes: new Dictionary<string, object> { { "style", "color: blue" } })
23+
</section>

src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
77
{
88
public class ModelMetadata
99
{
10+
public static readonly int DefaultOrder = 10000;
11+
1012
private readonly Type _containerType;
1113
private readonly Type _modelType;
1214
private readonly string _propertyName;
@@ -15,6 +17,7 @@ public class ModelMetadata
1517
private bool _convertEmptyStringToNull = true;
1618
private object _model;
1719
private Func<object> _modelAccessor;
20+
private int _order = DefaultOrder;
1821
private IEnumerable<ModelMetadata> _properties;
1922
private Type _realModelType;
2023

@@ -57,6 +60,12 @@ public bool IsNullableValueType
5760

5861
public virtual bool IsReadOnly { get; set; }
5962

63+
public virtual int Order
64+
{
65+
get { return _order; }
66+
set { _order = value; }
67+
}
68+
6069
public object Model
6170
{
6271
get

src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelper.cs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.IO;
66
using System.Net;
77
using System.Reflection;
8+
using System.Text;
89
using System.Threading.Tasks;
910
using Microsoft.AspNet.Abstractions;
1011

@@ -22,6 +23,8 @@ public class HtmlHelper : ICanHasViewContext
2223
public static readonly string ValidationSummaryCssClassName = "validation-summary-errors";
2324
public static readonly string ValidationSummaryValidCssClassName = "validation-summary-valid";
2425

26+
private const string HiddenListItem = @"<li style=""display:none""></li>";
27+
2528
private ViewContext _viewContext;
2629
private IViewEngine _viewEngine;
2730

@@ -175,6 +178,97 @@ protected virtual async Task RenderPartialCoreAsync([NotNull] string partialView
175178
await viewEngineResult.View.RenderAsync(newViewContext);
176179
}
177180

181+
public virtual HtmlString ValidationSummary(bool excludePropertyErrors, string message, IDictionary<string, object> htmlAttributes)
182+
{
183+
var formContext = ViewContext.ClientValidationEnabled ? ViewContext.FormContext : null;
184+
185+
if (ViewData.ModelState.IsValid == true)
186+
{
187+
if (formContext == null ||
188+
ViewContext.UnobtrusiveJavaScriptEnabled &&
189+
excludePropertyErrors)
190+
{
191+
// No client side validation/updates
192+
return HtmlString.Empty;
193+
}
194+
}
195+
196+
string messageSpan;
197+
if (!string.IsNullOrEmpty(message))
198+
{
199+
var spanTag = new TagBuilder("span");
200+
spanTag.SetInnerText(message);
201+
messageSpan = spanTag.ToString(TagRenderMode.Normal) + Environment.NewLine;
202+
}
203+
else
204+
{
205+
messageSpan = null;
206+
}
207+
208+
var htmlSummary = new StringBuilder();
209+
var modelStates = ValidationHelpers.GetModelStateList(ViewData, excludePropertyErrors);
210+
211+
foreach (var modelState in modelStates)
212+
{
213+
foreach (var modelError in modelState.Errors)
214+
{
215+
string errorText = ValidationHelpers.GetUserErrorMessageOrDefault(modelError, modelState: null);
216+
217+
if (!string.IsNullOrEmpty(errorText))
218+
{
219+
var listItem = new TagBuilder("li");
220+
listItem.SetInnerText(errorText);
221+
htmlSummary.AppendLine(listItem.ToString(TagRenderMode.Normal));
222+
}
223+
}
224+
}
225+
226+
if (htmlSummary.Length == 0)
227+
{
228+
htmlSummary.AppendLine(HiddenListItem);
229+
}
230+
231+
var unorderedList = new TagBuilder("ul")
232+
{
233+
InnerHtml = htmlSummary.ToString()
234+
};
235+
236+
var divBuilder = new TagBuilder("div");
237+
divBuilder.MergeAttributes(htmlAttributes);
238+
239+
if (ViewData.ModelState.IsValid == true)
240+
{
241+
divBuilder.AddCssClass(HtmlHelper.ValidationSummaryValidCssClassName);
242+
}
243+
else
244+
{
245+
divBuilder.AddCssClass(HtmlHelper.ValidationSummaryCssClassName);
246+
}
247+
248+
divBuilder.InnerHtml = messageSpan + unorderedList.ToString(TagRenderMode.Normal);
249+
250+
if (formContext != null)
251+
{
252+
if (ViewContext.UnobtrusiveJavaScriptEnabled)
253+
{
254+
if (!excludePropertyErrors)
255+
{
256+
// Only put errors in the validation summary if they're supposed to be included there
257+
divBuilder.MergeAttribute("data-valmsg-summary", "true");
258+
}
259+
}
260+
else
261+
{
262+
// client validation summaries need an ID
263+
divBuilder.GenerateId("validationSummary", IdAttributeDotReplacement);
264+
formContext.ValidationSummaryId = divBuilder.Attributes["id"];
265+
formContext.ReplaceValidationSummary = !excludePropertyErrors;
266+
}
267+
}
268+
269+
return divBuilder.ToHtmlString(TagRenderMode.Normal);
270+
}
271+
178272
/// <summary>
179273
/// Returns the HTTP method that handles form input (GET or POST) as a string.
180274
/// </summary>

src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlString.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,23 @@
22
{
33
public class HtmlString
44
{
5+
private static readonly HtmlString _empty = new HtmlString(string.Empty);
6+
57
private readonly string _input;
68

79
public HtmlString(string input)
810
{
911
_input = input;
1012
}
1113

14+
public static HtmlString Empty
15+
{
16+
get
17+
{
18+
return _empty;
19+
}
20+
}
21+
1222
public override string ToString()
1323
{
1424
return _input;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System.Collections.Generic;
2+
3+
namespace Microsoft.AspNet.Mvc.Rendering
4+
{
5+
public static class ValidationExtensions
6+
{
7+
public static HtmlString ValidationSummary<T>(this IHtmlHelper<T> htmlHelper)
8+
{
9+
return ValidationSummary(htmlHelper, excludePropertyErrors: false);
10+
}
11+
12+
public static HtmlString ValidationSummary<T>(this IHtmlHelper<T> htmlHelper, bool excludePropertyErrors)
13+
{
14+
return ValidationSummary(htmlHelper, excludePropertyErrors, message: null);
15+
}
16+
17+
public static HtmlString ValidationSummary<T>(this IHtmlHelper<T> htmlHelper, string message)
18+
{
19+
return ValidationSummary(htmlHelper, excludePropertyErrors: false, message: message, htmlAttributes: (object)null);
20+
}
21+
22+
public static HtmlString ValidationSummary<T>(this IHtmlHelper<T> htmlHelper, bool excludePropertyErrors, string message)
23+
{
24+
return ValidationSummary(htmlHelper, excludePropertyErrors, message, htmlAttributes: (object)null);
25+
}
26+
27+
public static HtmlString ValidationSummary<T>(this IHtmlHelper<T> htmlHelper, string message, object htmlAttributes)
28+
{
29+
return ValidationSummary(htmlHelper, excludePropertyErrors: false, message: message, htmlAttributes: HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
30+
}
31+
32+
public static HtmlString ValidationSummary<T>(this IHtmlHelper<T> htmlHelper, bool excludePropertyErrors, string message, object htmlAttributes)
33+
{
34+
return htmlHelper.ValidationSummary(excludePropertyErrors, message, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
35+
}
36+
37+
public static HtmlString ValidationSummary<T>(this IHtmlHelper<T> htmlHelper, string message, IDictionary<string, object> htmlAttributes)
38+
{
39+
return htmlHelper.ValidationSummary(excludePropertyErrors: false, message: message, htmlAttributes: htmlAttributes);
40+
}
41+
}
42+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using Microsoft.AspNet.Mvc.ModelBinding;
5+
6+
namespace Microsoft.AspNet.Mvc.Rendering
7+
{
8+
internal static class ValidationHelpers
9+
{
10+
public static string GetUserErrorMessageOrDefault(ModelError modelError, ModelState modelState)
11+
{
12+
if (!string.IsNullOrEmpty(modelError.ErrorMessage))
13+
{
14+
return modelError.ErrorMessage;
15+
}
16+
17+
if (modelState == null)
18+
{
19+
return string.Empty;
20+
}
21+
22+
var attemptedValue = (modelState.Value != null) ? modelState.Value.AttemptedValue : "null";
23+
24+
return Resources.FormatCommon_ValueNotValidForProperty(attemptedValue);
25+
}
26+
27+
// Returns non-null list of model states, which caller will render in order provided.
28+
public static IEnumerable<ModelState> GetModelStateList(ViewDataDictionary viewData, bool excludePropertyErrors)
29+
{
30+
if (excludePropertyErrors)
31+
{
32+
ModelState ms;
33+
viewData.ModelState.TryGetValue(viewData.TemplateInfo.HtmlFieldPrefix, out ms);
34+
35+
if (ms != null)
36+
{
37+
return new[] { ms };
38+
}
39+
40+
return Enumerable.Empty<ModelState>();
41+
}
42+
else
43+
{
44+
// Sort modelStates to respect the ordering in the metadata.
45+
// ModelState doesn't refer to ModelMetadata, but we can correlate via the property name.
46+
var ordering = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
47+
48+
var metadata = viewData.ModelMetadata;
49+
if (metadata != null)
50+
{
51+
foreach (var data in metadata.Properties)
52+
{
53+
ordering[data.PropertyName] = data.Order;
54+
}
55+
56+
return viewData.ModelState
57+
.OrderBy(data => ordering[data.Key])
58+
.Select(ms => ms.Value);
59+
}
60+
61+
return viewData.ModelState.Values;
62+
}
63+
}
64+
}
65+
}

src/Microsoft.AspNet.Mvc.Rendering/IHtmlHelperOfT.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Linq.Expressions;
3+
using System.Collections.Generic;
34
using System.Threading.Tasks;
45

56
namespace Microsoft.AspNet.Mvc.Rendering
@@ -99,5 +100,14 @@ public interface IHtmlHelper<TModel>
99100
/// <param name="viewData">A <see cref="ViewDataDictionary"/> to pass into the partial view.</param>
100101
/// <returns>A task that represents when rendering has completed.</returns>
101102
Task RenderPartialAsync([NotNull] string partialViewName, object model, ViewDataDictionary viewData);
103+
104+
/// <summary>
105+
/// Returns an unordered list (ul element) of validation messages that are in the <see cref="ModelStateDictionary"/> object.
106+
/// </summary>
107+
/// <param name="excludePropertyErrors">true to have the summary display model-level errors only, or false to have the summary display all errors.</param>
108+
/// <param name="message">The message to display with the validation summary.</param>
109+
/// <param name="htmlAttributes">A dictionary that contains the HTML attributes for the element.</param>
110+
/// <returns>An <see cref="HtmlString"/> that contains an unordered list (ul element) of validation messages.</returns>
111+
HtmlString ValidationSummary(bool excludePropertyErrors, string message, IDictionary<string, object> htmlAttributes);
102112
}
103113
}

src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.Designer.cs

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@
129129
<data name="Common_PartialViewNotFound" xml:space="preserve">
130130
<value>The partial view '{0}' was not found or no view engine supports the searched locations. The following locations were searched:{1}</value>
131131
</data>
132+
<data name="Common_ValueNotValidForProperty" xml:space="preserve">
133+
<value>The value '{0}' is invalid.</value>
134+
</data>
132135
<data name="DynamicViewData_ViewDataNull" xml:space="preserve">
133136
<value>ViewData value must not be null.</value>
134137
</data>

0 commit comments

Comments
 (0)