Add ValidationSummaryTagHelper.
- Tested ValidationSummaryTagHelper behavior. - Updated sample to utilize new ValidationSummaryTagHelper format. #1251
This commit is contained in:
parent
70e695b665
commit
9c07055ac7
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <div> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue