diff --git a/samples/TagHelperSample.Web/Views/Home/Create.cshtml b/samples/TagHelperSample.Web/Views/Home/Create.cshtml index 63dd93cf61..78da21d8c5 100644 --- a/samples/TagHelperSample.Web/Views/Home/Create.cshtml +++ b/samples/TagHelperSample.Web/Views/Home/Create.cshtml @@ -10,13 +10,13 @@
@* validation summary tag helper will target just
elements and append the list of errors *@ @* - i.e. this helper, like
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