Change Script, Link and Image `TagHelper`s to work better with other `TagHelper`s.

- `ScriptTagHelper`, `LinkTagHelper` and `ImageTagHelper` now default to using `output.Attributes["href|src"]` if it's present when they run. This enables other `TagHelper`s to run prior and add those attributes.
- Added unit tests to validate this behavior.
- Updated `ImageTagHelper` functional test resources. Now that we're always defaulting to `output.Attributes["src"]` for `ImageTagHelper.Src` we're properly copying attributes back into the `output.Attributes` collection in the correct order (isntead of appending to the end).

#2902
This commit is contained in:
N. Taylor Mullen 2015-08-19 17:06:15 -07:00
parent 052479af6b
commit c3e2e6fa0a
7 changed files with 192 additions and 40 deletions

View File

@ -75,23 +75,20 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
/// <inheritdoc /> /// <inheritdoc />
public override void Process(TagHelperContext context, TagHelperOutput output) public override void Process(TagHelperContext context, TagHelperOutput output)
{ {
output.CopyHtmlAttribute(SrcAttributeName, context);
ProcessUrlAttribute(SrcAttributeName, output);
if (AppendVersion) if (AppendVersion)
{ {
EnsureFileVersionProvider(); EnsureFileVersionProvider();
string resolvedUrl; // Retrieve the TagHelperOutput variation of the "src" attribute in case other TagHelpers in the
if (TryResolveUrl(Src, encodeWebRoot: false, resolvedUrl: out resolvedUrl)) // pipeline have touched the value. If the value is already encoded this ImageTagHelper may
{ // not function properly.
Src = resolvedUrl; Src = output.Attributes[SrcAttributeName].Value as string;
}
output.Attributes[SrcAttributeName] = _fileVersionProvider.AddFileVersionToPath(Src); output.Attributes[SrcAttributeName] = _fileVersionProvider.AddFileVersionToPath(Src);
} }
else
{
// Pass through attribute that is also a well-known HTML attribute.
output.CopyHtmlAttribute(SrcAttributeName, context);
ProcessUrlAttribute(SrcAttributeName, output);
}
} }
private void EnsureFileVersionProvider() private void EnsureFileVersionProvider()

View File

@ -211,16 +211,16 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
if (Href != null) if (Href != null)
{ {
output.CopyHtmlAttribute(HrefAttributeName, context); output.CopyHtmlAttribute(HrefAttributeName, context);
// Resolve any application relative URLs (~/) now so they can be used in comparisons later.
if (TryResolveUrl(Href, encodeWebRoot: false, resolvedUrl: out resolvedUrl))
{
Href = resolvedUrl;
}
ProcessUrlAttribute(HrefAttributeName, output);
} }
// If there's no "href" attribute in output.Attributes this will noop.
ProcessUrlAttribute(HrefAttributeName, output);
// Retrieve the TagHelperOutput variation of the "href" attribute in case other TagHelpers in the
// pipeline have touched the value. If the value is already encoded this LinkTagHelper may
// not function properly.
Href = output.Attributes[HrefAttributeName]?.Value as string;
var modeResult = AttributeMatcher.DetermineMode(context, ModeDetails); var modeResult = AttributeMatcher.DetermineMode(context, ModeDetails);
modeResult.LogDetails(Logger, this, context.UniqueId, ViewContext.View.Path); modeResult.LogDetails(Logger, this, context.UniqueId, ViewContext.View.Path);
@ -238,11 +238,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{ {
EnsureFileVersionProvider(); EnsureFileVersionProvider();
var attributeStringValue = output.Attributes[HrefAttributeName]?.Value as string; if (Href != null)
if (attributeStringValue != null)
{ {
output.Attributes[HrefAttributeName].Value = output.Attributes[HrefAttributeName].Value = _fileVersionProvider.AddFileVersionToPath(Href);
_fileVersionProvider.AddFileVersionToPath(attributeStringValue);
} }
} }

View File

@ -179,15 +179,16 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
if (Src != null) if (Src != null)
{ {
output.CopyHtmlAttribute(SrcAttributeName, context); output.CopyHtmlAttribute(SrcAttributeName, context);
if (TryResolveUrl(Src, encodeWebRoot: false, resolvedUrl: out resolvedUrl))
{
Src = resolvedUrl;
}
ProcessUrlAttribute(SrcAttributeName, output);
} }
// If there's no "src" attribute in output.Attributes this will noop.
ProcessUrlAttribute(SrcAttributeName, output);
// Retrieve the TagHelperOutput variation of the "src" attribute in case other TagHelpers in the
// pipeline have touched the value. If the value is already encoded this ScriptTagHelper may
// not function properly.
Src = output.Attributes[SrcAttributeName]?.Value as string;
var modeResult = AttributeMatcher.DetermineMode(context, ModeDetails); var modeResult = AttributeMatcher.DetermineMode(context, ModeDetails);
modeResult.LogDetails(Logger, this, context.UniqueId, ViewContext.View.Path); modeResult.LogDetails(Logger, this, context.UniqueId, ViewContext.View.Path);
@ -205,11 +206,9 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{ {
EnsureFileVersionProvider(); EnsureFileVersionProvider();
var attributeStringValue = output.Attributes[SrcAttributeName]?.Value as string; if (Src != null)
if (attributeStringValue != null)
{ {
output.Attributes[SrcAttributeName].Value = output.Attributes[SrcAttributeName].Value = _fileVersionProvider.AddFileVersionToPath(Src);
_fileVersionProvider.AddFileVersionToPath(attributeStringValue);
} }
} }

View File

@ -12,24 +12,24 @@
<img src="/images/red.png" alt="Red block" title="&lt;the title>"> <img src="/images/red.png" alt="Red block" title="&lt;the title>">
<!-- Plain image tag with file version --> <!-- Plain image tag with file version -->
<img alt="Red versioned" title="Red versioned" src="/images/red.png?v=W2F5D366_nQ2fQqUk3URdgWy2ZekXjHzHJaY5yaiOOk" /> <img src="/images/red.png?v=W2F5D366_nQ2fQqUk3URdgWy2ZekXjHzHJaY5yaiOOk" alt="Red versioned" title="Red versioned" />
<!-- Plain image tag with file version set to false --> <!-- Plain image tag with file version set to false -->
<img src="/images/red.png" alt="Red explicitly not versioned" title="Red versioned"> <img src="/images/red.png" alt="Red explicitly not versioned" title="Red versioned">
<!-- Plain image tag with absolute path and file version --> <!-- Plain image tag with absolute path and file version -->
<img alt="Absolute path versioned" src="http://contoso.com/hello/world"> <img src="http://contoso.com/hello/world" alt="Absolute path versioned">
<!-- Plain image tag with file version and path to file that does not exist --> <!-- Plain image tag with file version and path to file that does not exist -->
<img alt="Path to non existing file" src="/images/fake.png" /> <img src="/images/fake.png" alt="Path to non existing file" />
<!-- Plain image tag with file version and path containing query string --> <!-- Plain image tag with file version and path containing query string -->
<img alt="Path with query string" src="/images/red.png?abc=def&amp;v=W2F5D366_nQ2fQqUk3URdgWy2ZekXjHzHJaY5yaiOOk"> <img src="/images/red.png?abc=def&amp;v=W2F5D366_nQ2fQqUk3URdgWy2ZekXjHzHJaY5yaiOOk" alt="Path with query string">
<!-- Plain image tag with file version and path containing fragment --> <!-- Plain image tag with file version and path containing fragment -->
<img alt="Path with query string" src="/images/red.png?v=W2F5D366_nQ2fQqUk3URdgWy2ZekXjHzHJaY5yaiOOk#abc" /> <img src="/images/red.png?v=W2F5D366_nQ2fQqUk3URdgWy2ZekXjHzHJaY5yaiOOk#abc" alt="Path with query string" />
<!-- Plain image tag with file version and path linking to some action --> <!-- Plain image tag with file version and path linking to some action -->
<img alt="Path linking to some action" src="/controller/action"> <img src="/controller/action" alt="Path linking to some action">
</body> </body>
</html> </html>

View File

@ -25,6 +25,56 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{ {
public class ImageTagHelperTest public class ImageTagHelperTest
{ {
[Theory]
[InlineData(null, "test.jpg", "test.jpg")]
[InlineData("abcd.jpg", "test.jpg", "test.jpg")]
[InlineData(null, "~/test.jpg", "/virtualRoot/test.jpg")]
[InlineData("abcd.jpg", "~/test.jpg", "/virtualRoot/test.jpg")]
public void Process_SrcDefaultsToTagHelperOutputSrcAttributeAddedByOtherTagHelper(
string src,
string srcOutput,
string expectedSrcPrefix)
{
// Arrange
var allAttributes = new TagHelperAttributeList(
new TagHelperAttributeList
{
{ "alt", new HtmlString("Testing") },
{ "asp-append-version", true },
});
var context = MakeTagHelperContext(allAttributes);
var outputAttributes = new TagHelperAttributeList
{
{ "alt", new HtmlString("Testing") },
{ "src", srcOutput },
};
var output = new TagHelperOutput("img", outputAttributes);
var hostingEnvironment = MakeHostingEnvironment();
var viewContext = MakeViewContext();
var urlHelper = new Mock<IUrlHelper>();
urlHelper
.Setup(urlhelper => urlhelper.Content(It.IsAny<string>()))
.Returns(new Func<string, string>(url => url.Replace("~/", "/virtualRoot/")));
var helper = new ImageTagHelper(
hostingEnvironment,
MakeCache(),
new CommonTestEncoder(),
urlHelper.Object)
{
ViewContext = viewContext,
AppendVersion = true,
Src = src,
};
// Act
helper.Process(context, output);
// Assert
Assert.Equal(
expectedSrcPrefix + "?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk",
(string)output.Attributes["src"].Value,
StringComparer.Ordinal);
}
[Fact] [Fact]
public void PreservesOrderOfSourceAttributesWhenRun() public void PreservesOrderOfSourceAttributesWhenRun()

View File

@ -28,6 +28,60 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{ {
public class LinkTagHelperTest public class LinkTagHelperTest
{ {
[Theory]
[InlineData(null, "test.css", "test.css")]
[InlineData("abcd.css", "test.css", "test.css")]
[InlineData(null, "~/test.css", "/virtualRoot/test.css")]
[InlineData("abcd.css", "~/test.css", "/virtualRoot/test.css")]
public void Process_HrefDefaultsToTagHelperOutputHrefAttributeAddedByOtherTagHelper(
string href,
string hrefOutput,
string expectedHrefPrefix)
{
// Arrange
var allAttributes = new TagHelperAttributeList(
new TagHelperAttributeList
{
{ "rel", new HtmlString("stylesheet") },
{ "asp-append-version", true },
});
var context = MakeTagHelperContext(allAttributes);
var outputAttributes = new TagHelperAttributeList
{
{ "rel", new HtmlString("stylesheet") },
{ "href", hrefOutput },
};
var output = MakeTagHelperOutput("link", outputAttributes);
var logger = new Mock<ILogger<LinkTagHelper>>();
var hostingEnvironment = MakeHostingEnvironment();
var viewContext = MakeViewContext();
var urlHelper = new Mock<IUrlHelper>();
urlHelper
.Setup(urlhelper => urlhelper.Content(It.IsAny<string>()))
.Returns(new Func<string, string>(url => url.Replace("~/", "/virtualRoot/")));
var helper = new LinkTagHelper(
logger.Object,
hostingEnvironment,
MakeCache(),
new CommonTestEncoder(),
new CommonTestEncoder(),
urlHelper.Object)
{
ViewContext = viewContext,
AppendVersion = true,
Href = href,
};
// Act
helper.Process(context, output);
// Assert
Assert.Equal(
expectedHrefPrefix + "?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk",
(string)output.Attributes["href"].Value,
StringComparer.Ordinal);
}
public static TheoryData MultiAttributeSameNameData public static TheoryData MultiAttributeSameNameData
{ {
get get

View File

@ -29,6 +29,60 @@ namespace Microsoft.AspNet.Mvc.TagHelpers
{ {
public class ScriptTagHelperTest public class ScriptTagHelperTest
{ {
[Theory]
[InlineData(null, "test.js", "test.js")]
[InlineData("abcd.js", "test.js", "test.js")]
[InlineData(null, "~/test.js", "/virtualRoot/test.js")]
[InlineData("abcd.js", "~/test.js", "/virtualRoot/test.js")]
public void Process_SrcDefaultsToTagHelperOutputSrcAttributeAddedByOtherTagHelper(
string src,
string srcOutput,
string expectedSrcPrefix)
{
// Arrange
var allAttributes = new TagHelperAttributeList(
new TagHelperAttributeList
{
{ "type", new HtmlString("text/javascript") },
{ "asp-append-version", true },
});
var context = MakeTagHelperContext(allAttributes);
var outputAttributes = new TagHelperAttributeList
{
{ "type", new HtmlString("text/javascript") },
{ "src", srcOutput },
};
var output = MakeTagHelperOutput("script", outputAttributes);
var logger = new Mock<ILogger<ScriptTagHelper>>();
var hostingEnvironment = MakeHostingEnvironment();
var viewContext = MakeViewContext();
var urlHelper = new Mock<IUrlHelper>();
urlHelper
.Setup(urlhelper => urlhelper.Content(It.IsAny<string>()))
.Returns(new Func<string, string>(url => url.Replace("~/", "/virtualRoot/")));
var helper = new ScriptTagHelper(
logger.Object,
hostingEnvironment,
MakeCache(),
new CommonTestEncoder(),
new CommonTestEncoder(),
urlHelper.Object)
{
ViewContext = viewContext,
AppendVersion = true,
Src = src,
};
// Act
helper.Process(context, output);
// Assert
Assert.Equal(
expectedSrcPrefix + "?v=f4OxZX_x_FO5LcGBSKHWXfwtSx-j1ncoSt3SABJtkGk",
(string)output.Attributes["src"].Value,
StringComparer.Ordinal);
}
[Theory] [Theory]
[MemberData(nameof(LinkTagHelperTest.MultiAttributeSameNameData), MemberType = typeof(LinkTagHelperTest))] [MemberData(nameof(LinkTagHelperTest.MultiAttributeSameNameData), MemberType = typeof(LinkTagHelperTest))]
public async Task HandlesMultipleAttributesSameNameCorrectly(TagHelperAttributeList outputAttributes) public async Task HandlesMultipleAttributesSameNameCorrectly(TagHelperAttributeList outputAttributes)