Add ValidationSummaryTagHelper.

- Tested ValidationSummaryTagHelper behavior.
- Updated sample to utilize new ValidationSummaryTagHelper format.

#1251
This commit is contained in:
NTaylorMullen 2014-10-12 14:44:45 -07:00 committed by Doug Bunting
parent 70e695b665
commit 9c07055ac7
8 changed files with 418 additions and 4 deletions

View File

@ -10,13 +10,13 @@
<div class="form-horizontal">
@* validation summary tag helper will target just <div/> elements and append the list of errors *@
@* - i.e. this helper, like <select/> helper has ContentBehavior.Append *@
@* validation-model-errors-only="true" implies validation-summary="true" *@
@* helper does nothing if model is valid and (client-side validation is disabled or validation-model-errors-only="true") *@
@* helper does nothing if model is valid and (client-side validation is disabled or validation-summary="ModelOnly") *@
@* don't need a bound attribute to match Html.ValidationSummary()'s headerTag parameter; users wrap message as they wish *@
@* initially at least, will not remove the <div/> if list isn't generated *@
@* - should helper remove the <div/> if list isn't generated? *@
@* - (Html.ValidationSummary returns empty string despite non-empty message parameter) *@
<div validation-summary="true" validation-model-errors-only="true">
@* Acceptable values are: "None", "ModelOnly" and "All" *@
<div validation-summary="ModelOnly">
<span style="color:red">This is my message</span>
</div>

View File

@ -6,7 +6,7 @@
<form>
<div class="form-horizontal">
<div validation-summary="true"/>
<div validation-summary="All"/>
<input type="hidden" for="Id" />
<div class="form-group">

View File

@ -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")]

View File

@ -58,6 +58,22 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
return string.Format(CultureInfo.CurrentCulture, GetString("FormTagHelper_CannotDetermineAction"), p0, p1, p2, p3);
}
/// <summary>
/// Cannot parse '{1}' value '{2}' for {0}. Acceptable values are '{3}', '{4}' and '{5}'.
/// </summary>
internal static string ValidationSummaryTagHelper_InvalidValidationSummaryValue
{
get { return GetString("ValidationSummaryTagHelper_InvalidValidationSummaryValue"); }
}
/// <summary>
/// Cannot parse '{1}' value '{2}' for {0}. Acceptable values are '{3}', '{4}' and '{5}'.
/// </summary>
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);

View File

@ -126,4 +126,7 @@
<data name="FormTagHelper_CannotDetermineAction" xml:space="preserve">
<value>Cannot determine an {1} for {0}. A {0} with a URL-based {1} must not have attributes starting with {3} or a {2} attribute.</value>
</data>
<data name="ValidationSummaryTagHelper_InvalidValidationSummaryValue" xml:space="preserve">
<value>Cannot parse '{1}' value '{2}' for {0}. Acceptable values are '{3}', '{4}' and '{5}'.</value>
</data>
</root>

View File

@ -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
{
/// <summary>
/// Acceptable validation summary rendering modes.
/// </summary>
public enum ValidationSummary
{
/// <summary>
/// No validation summary.
/// </summary>
None,
/// <summary>
/// Validation summary with model-level errors only (excludes all property errors).
/// </summary>
ModelOnly,
/// <summary>
/// Validation summary with all errors.
/// </summary>
All
}
}

View File

@ -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
{
/// <summary>
/// <see cref="ITagHelper"/> implementation targeting &lt;div&gt; elements with a <c>validation-summary</c>
/// attribute.
/// </summary>
[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.
/// <summary>
/// If <c>All</c> or <c>ModelOnly</c>, appends a validation summary. Acceptable values are defined by the
/// <see cref="ValidationSummary"/> enum.
/// </summary>
[HtmlAttributeName("validation-summary")]
public string ValidationSummaryValue { get; set; }
/// <inheritdoc />
/// Does nothing if <see cref="ValidationSummaryValue"/> is <c>null</c>, 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(
"<div>",
"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;
}
}
}
}
}

View File

@ -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<string, object>());
var output = new TagHelperOutput(
"div",
attributes: new Dictionary<string, string>
{
{ "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<ul><li style=\"display:none\"></li>" + Environment.NewLine + "</ul>",
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<string, string>(),
content: "Content of validation summary");
var expectedViewContext = CreateViewContext();
var generator = new Mock<IHtmlGenerator>();
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<string, string>(),
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<IHtmlGenerator>(MockBehavior.Strict);
generator
.Setup(mock => mock.GenerateValidationSummary(
It.IsAny<ViewContext>(),
It.IsAny<bool>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<object>()))
.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<string, string>(),
content: "Content of validation message");
var generator = new Mock<IHtmlGenerator>(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<string, string>(),
content: "Content of validation message");
var tagBuilder = new TagBuilder("span2")
{
InnerHtml = "New HTML"
};
var generator = new Mock<IHtmlGenerator>();
generator
.Setup(mock => mock.GenerateValidationSummary(
It.IsAny<ViewContext>(),
It.IsAny<bool>(),
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<object>()))
.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<string, string>(),
content: "Content of validation message");
var tagBuilder = new TagBuilder("span2")
{
InnerHtml = "New HTML"
};
var generator = new Mock<IHtmlGenerator>(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<string, string>(),
content: "Content of validation message");
var expectedViewContext = CreateViewContext();
var expectedMessage = "Cannot parse 'validation-summary' value 'Hello World' for <div>. Acceptable values " +
"are 'All', 'ModelOnly' and 'None'.";
// Act
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() =>
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<IView>(),
new ViewDataDictionary(
new EmptyModelMetadataProvider()),
TextWriter.Null);
}
private class Model
{
public string Text { get; set; }
}
}
}