diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/ValidationMessageTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/ValidationMessageTagHelper.cs new file mode 100644 index 0000000000..e10d61183d --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/ValidationMessageTagHelper.cs @@ -0,0 +1,55 @@ +// 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 Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Microsoft.AspNet.Razor.TagHelpers; + +namespace Microsoft.AspNet.Mvc.TagHelpers +{ + /// + /// implementation targeting <span> elements with validation-for attributes. + /// + [TagName("span")] + [ContentBehavior(ContentBehavior.Modify)] + public class ValidationMessageTagHelper : TagHelper + { + [Activate] + private ViewContext ViewContext { get; set; } + + [Activate] + private IHtmlGenerator Generator { get; set; } + + /// + /// Name to be validated on the current model. + /// + [HtmlAttributeName("validation-for")] + public ModelExpression For { get; set; } + + /// + public override void Process(TagHelperContext context, TagHelperOutput output) + { + if (For != null) + { + var tagBuilder = Generator.GenerateValidationMessage(ViewContext, + For.Name, + message: null, + tag: null, + htmlAttributes: null); + + if (tagBuilder != null) + { + output.MergeAttributes(tagBuilder); + + // We check for whitespace to detect scenarios such as: + // + // + if (string.IsNullOrWhiteSpace(output.Content)) + { + output.Content = tagBuilder.InnerHtml; + } + } + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ValidationMessageTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ValidationMessageTagHelperTest.cs new file mode 100644 index 0000000000..dbd8af09b6 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/ValidationMessageTagHelperTest.cs @@ -0,0 +1,211 @@ +// 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.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.Razor.Runtime.TagHelpers; +using Microsoft.AspNet.Routing; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.TagHelpers +{ + public class ValidationMessageTagHelperTest + { + [Fact] + public async Task ProcessAsync_GeneratesExpectedOutput() + { + // Arrange + var metadataProvider = new DataAnnotationsModelMetadataProvider(); + var modelExpression = CreateModelExpression("Name"); + var validationMessageTagHelper = new ValidationMessageTagHelper + { + For = modelExpression + }; + + var tagHelperContext = new TagHelperContext( + allAttributes: new Dictionary + { + { "id", "myvalidationmessage" }, + { "for", modelExpression }, + }); + var output = new TagHelperOutput( + "original tag name", + attributes: new Dictionary + { + { "id", "myvalidationmessage" } + }, + content: "Something"); + var htmlGenerator = new TestableHtmlGenerator(metadataProvider); + var viewContext = TestableHtmlGenerator.GetViewContext(model: null, + htmlGenerator: htmlGenerator, + metadataProvider: metadataProvider); + + var activator = new DefaultTagHelperActivator(); + activator.Activate(validationMessageTagHelper, viewContext); + + // Act + await validationMessageTagHelper.ProcessAsync(tagHelperContext, output); + + // Assert + Assert.Equal(4, output.Attributes.Count); + var attribute = Assert.Single(output.Attributes, kvp => kvp.Key.Equals("id")); + Assert.Equal("myvalidationmessage", attribute.Value); + attribute = Assert.Single(output.Attributes, kvp => kvp.Key.Equals("class")); + Assert.Equal("field-validation-valid", attribute.Value); + attribute = Assert.Single(output.Attributes, kvp => kvp.Key.Equals("data-valmsg-for")); + Assert.Equal("Name", attribute.Value); + attribute = Assert.Single(output.Attributes, kvp => kvp.Key.Equals("data-valmsg-replace")); + Assert.Equal("true", attribute.Value); + Assert.Equal("Something", output.Content); + Assert.Equal("original tag name", output.TagName); + } + + [Fact] + public async Task ProcessAsync_CallsIntoGenerateValidationMessageWithExpectedParameters() + { + // Arrange + var validationMessageTagHelper = new ValidationMessageTagHelper + { + For = CreateModelExpression("Hello") + }; + var output = new TagHelperOutput( + "span", + attributes: new Dictionary(), + content: "Content of validation message"); + var expectedViewContext = CreateViewContext(); + var generator = new Mock(); + generator + .Setup(mock => + mock.GenerateValidationMessage(expectedViewContext, "Hello", null, null, null)) + .Returns(new TagBuilder("span")) + .Verifiable(); + + SetViewContextAndGenerator(validationMessageTagHelper, expectedViewContext, generator.Object); + + // Act & Assert + await validationMessageTagHelper.ProcessAsync(context: null, output: output); + + generator.Verify(); + Assert.Equal("span", output.TagName); + Assert.Empty(output.Attributes); + Assert.Equal("Content of validation message", output.Content); + } + + [Theory] + [InlineData("Content of validation message", "Content of validation message")] + [InlineData("\r\n \r\n", "New HTML")] + public async Task ProcessAsync_MergesTagBuilderFromGenerateValidationMessage( + string outputContent, string expectedOutputContent) + { + // Arrange + var validationMessageTagHelper = new ValidationMessageTagHelper + { + For = CreateModelExpression("Hello") + }; + var output = new TagHelperOutput( + "span", + attributes: new Dictionary(), + content: outputContent); + var tagBuilder = new TagBuilder("span2") + { + InnerHtml = "New HTML" + }; + tagBuilder.Attributes.Add("data-foo", "bar"); + tagBuilder.Attributes.Add("data-hello", "world"); + + var expectedViewContext = CreateViewContext(); + var generator = new Mock(MockBehavior.Strict); + var setup = generator + .Setup(mock => mock.GenerateValidationMessage( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(tagBuilder); + + SetViewContextAndGenerator(validationMessageTagHelper, expectedViewContext, generator.Object); + + // Act + await validationMessageTagHelper.ProcessAsync(context: null, output: output); + + // Assert + Assert.Equal(output.TagName, "span"); + Assert.Equal(2, 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); + Assert.Equal(expectedOutputContent, output.Content); + } + + [Fact] + public async Task ProcessAsync_DoesNothingIfNullFor() + { + // Arrange + var validationMessageTagHelper = new ValidationMessageTagHelper(); + var output = new TagHelperOutput( + "span", + attributes: new Dictionary(), + content: "Content of validation message"); + var expectedViewContext = CreateViewContext(); + var generator = new Mock(MockBehavior.Strict); + + SetViewContextAndGenerator(validationMessageTagHelper, expectedViewContext, generator.Object); + + // Act + await validationMessageTagHelper.ProcessAsync(context: null, output: output); + + // Assert + Assert.Equal("span", output.TagName); + Assert.Empty(output.Attributes); + Assert.Equal("Content of validation message", output.Content); + } + + private static ModelExpression CreateModelExpression(string name) + { + return new ModelExpression( + name, + new ModelMetadata( + new Mock().Object, + containerType: null, + modelAccessor: null, + modelType: typeof(object), + propertyName: string.Empty)); + } + + private static ViewContext CreateViewContext() + { + var actionContext = new ActionContext( + new Mock().Object, + new RouteData(), + new ActionDescriptor()); + + return new ViewContext( + actionContext, + Mock.Of(), + new ViewDataDictionary( + new DataAnnotationsModelMetadataProvider()), + new StringWriter()); + } + + private static void SetViewContextAndGenerator(ITagHelper tagHelper, + ViewContext viewContext, + IHtmlGenerator generator) + { + var tagHelperType = tagHelper.GetType(); + + tagHelperType.GetProperty("ViewContext", BindingFlags.NonPublic | BindingFlags.Instance) + .SetValue(tagHelper, viewContext); + tagHelperType.GetProperty("Generator", BindingFlags.NonPublic | BindingFlags.Instance) + .SetValue(tagHelper, generator); + } + } +} \ No newline at end of file