diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..58841d9458
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.AspNet.Mvc.TagHelpers.Test")]
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/Properties/Resources.Designer.cs
index f326434f03..6254f108c6 100644
--- a/src/Microsoft.AspNet.Mvc.TagHelpers/Properties/Resources.Designer.cs
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Properties/Resources.Designer.cs
@@ -58,6 +58,22 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
return string.Format(CultureInfo.CurrentCulture, GetString("FormTagHelper_CannotDetermineAction"), p0, p1, p2, p3);
}
+ ///
+ /// Cannot parse '{1}' value '{2}' for {0}. Acceptable values are '{3}', '{4}' and '{5}'.
+ ///
+ internal static string ValidationSummaryTagHelper_InvalidValidationSummaryValue
+ {
+ get { return GetString("ValidationSummaryTagHelper_InvalidValidationSummaryValue"); }
+ }
+
+ ///
+ /// Cannot parse '{1}' value '{2}' for {0}. Acceptable values are '{3}', '{4}' and '{5}'.
+ ///
+ internal static string FormatValidationSummaryTagHelper_InvalidValidationSummaryValue(object p0, object p1, object p2, object p3, object p4, object p5)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("ValidationSummaryTagHelper_InvalidValidationSummaryValue"), p0, p1, p2, p3, p4, p5);
+ }
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/Resources.resx b/src/Microsoft.AspNet.Mvc.TagHelpers/Resources.resx
index db5e382720..91d10324dd 100644
--- a/src/Microsoft.AspNet.Mvc.TagHelpers/Resources.resx
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/Resources.resx
@@ -126,4 +126,7 @@
Cannot determine an {1} for {0}. A {0} with a URL-based {1} must not have attributes starting with {3} or a {2} attribute.
+
+ Cannot parse '{1}' value '{2}' for {0}. Acceptable values are '{3}', '{4}' and '{5}'.
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/ValidationSummary.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/ValidationSummary.cs
new file mode 100644
index 0000000000..5b7c111c06
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/ValidationSummary.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNet.Mvc.TagHelpers
+{
+ ///
+ /// Acceptable validation summary rendering modes.
+ ///
+ public enum ValidationSummary
+ {
+ ///
+ /// No validation summary.
+ ///
+ None,
+
+ ///
+ /// Validation summary with model-level errors only (excludes all property errors).
+ ///
+ ModelOnly,
+
+ ///
+ /// Validation summary with all errors.
+ ///
+ All
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/ValidationSummaryTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/ValidationSummaryTagHelper.cs
new file mode 100644
index 0000000000..0bb03e4298
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.TagHelpers/ValidationSummaryTagHelper.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Microsoft.AspNet.Mvc.Rendering;
+using Microsoft.AspNet.Razor.Runtime.TagHelpers;
+using Microsoft.AspNet.Razor.TagHelpers;
+
+namespace Microsoft.AspNet.Mvc.TagHelpers
+{
+ ///
+ /// implementation targeting <div> elements with a validation-summary
+ /// attribute.
+ ///
+ [TagName("div")]
+ [ContentBehavior(ContentBehavior.Append)]
+ public class ValidationSummaryTagHelper : TagHelper
+ {
+ // Protected to ensure subclasses are correctly activated. Internal for ease of use when testing.
+ [Activate]
+ protected internal ViewContext ViewContext { get; set; }
+
+ // Protected to ensure subclasses are correctly activated. Internal for ease of use when testing.
+ [Activate]
+ protected internal IHtmlGenerator Generator { get; set; }
+
+ // TODO: Change to ValidationSummary enum once https://github.com/aspnet/Razor/issues/196 has been completed.
+ ///
+ /// If All or ModelOnly , appends a validation summary. Acceptable values are defined by the
+ /// enum.
+ ///
+ [HtmlAttributeName("validation-summary")]
+ public string ValidationSummaryValue { get; set; }
+
+ ///
+ /// Does nothing if
is
null , empty or "None".
+ public override void Process(TagHelperContext context, TagHelperOutput output)
+ {
+ if (!string.IsNullOrEmpty(ValidationSummaryValue))
+ {
+ ValidationSummary validationSummaryValue;
+ if (!Enum.TryParse(ValidationSummaryValue, ignoreCase: true, result: out validationSummaryValue))
+ {
+ throw new InvalidOperationException(
+ Resources.FormatValidationSummaryTagHelper_InvalidValidationSummaryValue(
+ "
",
+ "validation-summary",
+ ValidationSummaryValue,
+ ValidationSummary.All,
+ ValidationSummary.ModelOnly,
+ ValidationSummary.None));
+ }
+ else if (validationSummaryValue == ValidationSummary.None)
+ {
+ return;
+ }
+
+ var validationModelErrorsOnly = validationSummaryValue == ValidationSummary.ModelOnly;
+ var tagBuilder = Generator.GenerateValidationSummary(
+ ViewContext,
+ excludePropertyErrors: validationModelErrorsOnly,
+ message: null,
+ headerTag: null,
+ htmlAttributes: null);
+
+ if (tagBuilder != null)
+ {
+ output.MergeAttributes(tagBuilder);
+ output.Content += tagBuilder.InnerHtml;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ValidationSummaryTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ValidationSummaryTagHelperTest.cs
new file mode 100644
index 0000000000..40ff2351ee
--- /dev/null
+++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ValidationSummaryTagHelperTest.cs
@@ -0,0 +1,289 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Threading.Tasks;
+using Microsoft.AspNet.Http;
+using Microsoft.AspNet.Mvc.ModelBinding;
+using Microsoft.AspNet.Mvc.Razor;
+using Microsoft.AspNet.Mvc.Rendering;
+using Microsoft.AspNet.PipelineCore;
+using Microsoft.AspNet.Razor.Runtime.TagHelpers;
+using Microsoft.AspNet.Routing;
+using Moq;
+using Xunit;
+
+namespace Microsoft.AspNet.Mvc.TagHelpers
+{
+ public class ValidationSummaryTagHelperTest
+ {
+ [Fact]
+ public async Task ProcessAsync_GeneratesExpectedOutput()
+ {
+ // Arrange
+ var metadataProvider = new DataAnnotationsModelMetadataProvider();
+ var validationSummaryTagHelper = new ValidationSummaryTagHelper
+ {
+ ValidationSummaryValue = "All"
+ };
+
+ var tagHelperContext = new TagHelperContext(new Dictionary
());
+ var output = new TagHelperOutput(
+ "div",
+ attributes: new Dictionary
+ {
+ { "class", "form-control" }
+ },
+ content: "Custom Content");
+
+ var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
+ Model model = null;
+ var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);
+ validationSummaryTagHelper.ViewContext = viewContext;
+ validationSummaryTagHelper.Generator = htmlGenerator;
+
+ // Act
+ await validationSummaryTagHelper.ProcessAsync(tagHelperContext, output);
+
+ // Assert
+ Assert.Equal(2, output.Attributes.Count);
+ var attribute = Assert.Single(output.Attributes, kvp => kvp.Key.Equals("class"));
+ Assert.Equal("form-control validation-summary-valid", attribute.Value);
+ attribute = Assert.Single(output.Attributes, kvp => kvp.Key.Equals("data-valmsg-summary"));
+ Assert.Equal("true", attribute.Value);
+ Assert.Equal("Custom Content " + Environment.NewLine + " ",
+ output.Content);
+ Assert.Equal("div", output.TagName);
+ }
+
+ [Fact]
+ public async Task ProcessAsync_CallsIntoGenerateValidationSummaryWithExpectedParameters()
+ {
+ // Arrange
+ var validationSummaryTagHelper = new ValidationSummaryTagHelper
+ {
+ ValidationSummaryValue = "ModelOnly",
+ };
+ var output = new TagHelperOutput(
+ "div",
+ attributes: new Dictionary(),
+ content: "Content of validation summary");
+ var expectedViewContext = CreateViewContext();
+ var generator = new Mock();
+ generator
+ .Setup(mock => mock.GenerateValidationSummary(expectedViewContext, true, null, null, null))
+ .Returns(new TagBuilder("div"))
+ .Verifiable();
+ validationSummaryTagHelper.ViewContext = expectedViewContext;
+ validationSummaryTagHelper.Generator = generator.Object;
+
+ // Act & Assert
+ await validationSummaryTagHelper.ProcessAsync(context: null, output: output);
+
+ generator.Verify();
+ Assert.Equal("div", output.TagName);
+ Assert.Empty(output.Attributes);
+ Assert.Equal("Content of validation summary", output.Content);
+ }
+
+ [Fact]
+ public async Task ProcessAsync_MergesTagBuilderFromGenerateValidationSummary()
+ {
+ // Arrange
+ var validationSummaryTagHelper = new ValidationSummaryTagHelper
+ {
+ ValidationSummaryValue = "ModelOnly"
+ };
+ var output = new TagHelperOutput(
+ "div",
+ attributes: new Dictionary(),
+ content: "Content of validation summary");
+ var tagBuilder = new TagBuilder("span2")
+ {
+ InnerHtml = "New HTML"
+ };
+
+ tagBuilder.Attributes.Add("data-foo", "bar");
+ tagBuilder.Attributes.Add("data-hello", "world");
+ tagBuilder.Attributes.Add("anything", "something");
+
+ var generator = new Mock(MockBehavior.Strict);
+ generator
+ .Setup(mock => mock.GenerateValidationSummary(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .Returns(tagBuilder);
+ var viewContext = CreateViewContext();
+ validationSummaryTagHelper.ViewContext = viewContext;
+ validationSummaryTagHelper.Generator = generator.Object;
+
+ // Act
+ await validationSummaryTagHelper.ProcessAsync(context: null, output: output);
+
+ // Assert
+ Assert.Equal(output.TagName, "div");
+ Assert.Equal(3, output.Attributes.Count);
+ var attribute = Assert.Single(output.Attributes, kvp => kvp.Key.Equals("data-foo"));
+ Assert.Equal("bar", attribute.Value);
+ attribute = Assert.Single(output.Attributes, kvp => kvp.Key.Equals("data-hello"));
+ Assert.Equal("world", attribute.Value);
+ attribute = Assert.Single(output.Attributes, kvp => kvp.Key.Equals("anything"));
+ Assert.Equal("something", attribute.Value);
+ Assert.Equal("Content of validation summaryNew HTML", output.Content);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(null)]
+ public async Task ProcessAsync_DoesNothingIfNullOrEmptyValidationSummaryValue(string validationSummaryValue)
+ {
+ // Arrange
+ var validationSummaryTagHelper = new ValidationSummaryTagHelper
+ {
+ ValidationSummaryValue = validationSummaryValue
+ };
+ var output = new TagHelperOutput(
+ "div",
+ attributes: new Dictionary(),
+ content: "Content of validation message");
+
+ var generator = new Mock(MockBehavior.Strict);
+ var viewContext = CreateViewContext();
+ validationSummaryTagHelper.ViewContext = viewContext;
+ validationSummaryTagHelper.Generator = generator.Object;
+
+ // Act
+ await validationSummaryTagHelper.ProcessAsync(context: null, output: output);
+
+ // Assert
+ Assert.Equal("div", output.TagName);
+ Assert.Empty(output.Attributes);
+ Assert.Equal("Content of validation message", output.Content);
+ }
+
+ [Theory]
+ [InlineData("All")]
+ [InlineData("all")]
+ [InlineData("ModelOnly")]
+ [InlineData("modelonly")]
+ public async Task ProcessAsync_GeneratesValidationSummaryWhenNotNone_IgnoresCase(string validationSummary)
+ {
+ // Arrange
+ var validationSummaryTagHelper = new ValidationSummaryTagHelper
+ {
+ ValidationSummaryValue = validationSummary
+ };
+ var output = new TagHelperOutput(
+ "div",
+ attributes: new Dictionary(),
+ content: "Content of validation message");
+ var tagBuilder = new TagBuilder("span2")
+ {
+ InnerHtml = "New HTML"
+ };
+
+ var generator = new Mock();
+ generator
+ .Setup(mock => mock.GenerateValidationSummary(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny()))
+ .Returns(tagBuilder)
+ .Verifiable();
+ var viewContext = CreateViewContext();
+ validationSummaryTagHelper.ViewContext = viewContext;
+ validationSummaryTagHelper.Generator = generator.Object;
+
+ // Act
+ await validationSummaryTagHelper.ProcessAsync(context: null, output: output);
+
+ // Assert
+ Assert.Equal("div", output.TagName);
+ Assert.Empty(output.Attributes);
+ Assert.Equal("Content of validation messageNew HTML", output.Content);
+ generator.Verify();
+ }
+
+ [Theory]
+ [InlineData("None")]
+ [InlineData("none")]
+ public async Task ProcessAsync_DoesNotGenerateValidationSummaryWhenNone_IgnoresCase(string validationSummary)
+ {
+ // Arrange
+ var validationSummaryTagHelper = new ValidationSummaryTagHelper
+ {
+ ValidationSummaryValue = validationSummary
+ };
+ var output = new TagHelperOutput(
+ "div",
+ attributes: new Dictionary(),
+ content: "Content of validation message");
+ var tagBuilder = new TagBuilder("span2")
+ {
+ InnerHtml = "New HTML"
+ };
+
+ var generator = new Mock(MockBehavior.Strict);
+
+ // Act
+ await validationSummaryTagHelper.ProcessAsync(context: null, output: output);
+
+ // Assert
+ Assert.Equal("div", output.TagName);
+ Assert.Empty(output.Attributes);
+ Assert.Equal("Content of validation message", output.Content);
+ }
+
+ [Fact]
+ public async Task ProcessAsync_ThrowsWhenInvalidValidationSummaryValue()
+ {
+ // Arrange
+ var validationSummaryTagHelper = new ValidationSummaryTagHelper
+ {
+ ValidationSummaryValue = "Hello World"
+ };
+ var output = new TagHelperOutput(
+ "div",
+ attributes: new Dictionary(),
+ content: "Content of validation message");
+ var expectedViewContext = CreateViewContext();
+ var expectedMessage = "Cannot parse 'validation-summary' value 'Hello World' for . Acceptable values " +
+ "are 'All', 'ModelOnly' and 'None'.";
+
+ // Act
+ var ex = await Assert.ThrowsAsync(() =>
+ validationSummaryTagHelper.ProcessAsync(context: null, output: output));
+
+ // Assert
+ Assert.Equal(expectedMessage, ex.Message);
+ }
+
+ private static ViewContext CreateViewContext()
+ {
+ var actionContext = new ActionContext(
+ new DefaultHttpContext(),
+ new RouteData(),
+ new ActionDescriptor());
+
+ return new ViewContext(
+ actionContext,
+ Mock.Of(),
+ new ViewDataDictionary(
+ new EmptyModelMetadataProvider()),
+ TextWriter.Null);
+ }
+
+ private class Model
+ {
+ public string Text { get; set; }
+ }
+ }
+}
\ No newline at end of file