Adding support for inheriting tag helpers from _ViewStart files

Fixes #1166
This commit is contained in:
Pranav K 2014-10-12 11:59:00 -07:00
parent 0ad959e236
commit b2a01e7b45
17 changed files with 236 additions and 134 deletions

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.AspNet.FileSystems;
using Microsoft.AspNet.Razor;
using Microsoft.AspNet.Razor.Generator.Compiler;
@ -13,63 +12,57 @@ using Microsoft.AspNet.Razor.Parser;
namespace Microsoft.AspNet.Mvc.Razor.Directives
{
/// <summary>
/// A utility type for supporting inheritance of chunks into a page from _ViewStart pages that apply to it.
/// A utility type for supporting inheritance of tag helpers and chunks into a page from applicable _ViewStart
/// pages.
/// </summary>
public class ChunkInheritanceUtility
{
private readonly IReadOnlyList<Chunk> _defaultInheritedChunks;
private readonly Dictionary<string, CodeTree> _parsedCodeTrees;
private readonly MvcRazorHost _razorHost;
private readonly IFileSystem _fileSystem;
private readonly IEnumerable<Chunk> _defaultInheritedChunks;
/// <summary>
/// Instantiates a new instance of <see cref="ChunkInheritanceUtility"/>.
/// </summary>
/// <param name="codeTree">The <see cref="CodeTree"/> instance to add <see cref="Chunk"/>s to.</param>
/// <param name="defaultInheritedChunks">The list of <see cref="Chunk"/>s inherited by default.</param>
/// <param name="defaultModel">The model type used in the event no model is specified via the
/// <c>@model</c> keyword.</param>
public ChunkInheritanceUtility([NotNull] CodeTree codeTree,
[NotNull] IReadOnlyList<Chunk> defaultInheritedChunks,
[NotNull] string defaultModel)
{
CodeTree = codeTree;
_defaultInheritedChunks = defaultInheritedChunks;
ChunkMergers = GetMergerMappings(codeTree, defaultModel);
}
/// <summary>
/// Gets the CodeTree to add inherited <see cref="Chunk"/> instances to.
/// </summary>
public CodeTree CodeTree { get; private set; }
/// <summary>
/// Gets a dictionary mapping <see cref="Chunk"/> type to the <see cref="IChunkMerger"/> used to merge
/// chunks of that type.
/// </summary>
public IDictionary<Type, IChunkMerger> ChunkMergers { get; private set; }
/// <summary>
/// Gets the list of chunks that are to be inherited by a specified page.
/// Chunks are inherited from _ViewStarts that are applicable to the page.
/// Initializes a new instance of <see cref="ChunkInheritanceUtility"/>.
/// </summary>
/// <param name="razorHost">The <see cref="MvcRazorHost"/> used to parse _ViewStart pages.</param>
/// <param name="fileSystem">The filesystem that represents the application.</param>
/// <param name="defaultInheritedChunks">Sequence of <see cref="Chunk"/>s inherited by default.</param>
public ChunkInheritanceUtility([NotNull] MvcRazorHost razorHost,
[NotNull] IFileSystem fileSystem,
[NotNull] IEnumerable<Chunk> defaultInheritedChunks)
{
_razorHost = razorHost;
_fileSystem = fileSystem;
_defaultInheritedChunks = defaultInheritedChunks;
_parsedCodeTrees = new Dictionary<string, CodeTree>(StringComparer.Ordinal);
}
/// <summary>
/// Gets a <see cref="IReadOnlyList{T}"/> of <see cref="Chunk"/> containing parsed results of _ViewStart files
/// that are used for inheriting tag helpers and chunks to the page located at <paramref name="pagePath"/>.
/// </summary>
/// <param name="pagePath">The path of the page to locate inherited chunks for.</param>
/// <returns>A list of chunks that are applicable to the given page.</returns>
public List<Chunk> GetInheritedChunks([NotNull] MvcRazorHost razorHost,
[NotNull] IFileSystem fileSystem,
[NotNull] string pagePath)
/// <returns>A <see cref="IReadOnlyList{T}"/> of <see cref="Chunk"/> from _ViewStart pages.</returns>
public IReadOnlyList<Chunk> GetInheritedChunks([NotNull] string pagePath)
{
var inheritedChunks = new List<Chunk>();
var templateEngine = new RazorTemplateEngine(razorHost);
foreach (var viewStart in ViewStartUtility.GetViewStartLocations(fileSystem, pagePath))
var templateEngine = new RazorTemplateEngine(_razorHost);
foreach (var viewStart in ViewStartUtility.GetViewStartLocations(_fileSystem, pagePath))
{
CodeTree codeTree;
IFileInfo fileInfo;
if (fileSystem.TryGetFileInfo(viewStart, out fileInfo))
if (_parsedCodeTrees.TryGetValue(viewStart, out codeTree))
{
var parsedTree = ParseViewFile(templateEngine, fileInfo);
var chunksToAdd = parsedTree.Chunks
.Where(chunk => ChunkMergers.ContainsKey(chunk.GetType()));
inheritedChunks.AddRange(chunksToAdd);
inheritedChunks.AddRange(codeTree.Chunks);
}
else if (_fileSystem.TryGetFileInfo(viewStart, out fileInfo))
{
codeTree = ParseViewFile(templateEngine, fileInfo);
_parsedCodeTrees.Add(viewStart, codeTree);
inheritedChunks.AddRange(codeTree.Chunks);
}
}
@ -79,19 +72,22 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives
}
/// <summary>
/// Merges a list of chunks into the <see cref="CodeTree"/> instance.
/// Merges a list of chunks into the specified <paramref name="codeTree/>.
/// </summary>
/// <param name="codeTree">The <see cref="CodeTree"/> to mere</param>
/// <param name="inherited">The list of chunks to merge.</param>
public void MergeInheritedChunks(List<Chunk> inherited)
public void MergeInheritedChunks([NotNull] CodeTree codeTree,
[NotNull] IReadOnlyList<Chunk> inherited,
string defaultModel)
{
var current = CodeTree.Chunks;
var mergerMappings = GetMergerMappings(codeTree, defaultModel);
IChunkMerger merger;
// We merge chunks into the codeTree in two passes. In the first pass, we traverse the CodeTree visiting
// a mapped IChunkMerger for types that are registered.
foreach (var chunk in current)
foreach (var chunk in codeTree.Chunks)
{
if (ChunkMergers.TryGetValue(chunk.GetType(), out merger))
if (mergerMappings.TryGetValue(chunk.GetType(), out merger))
{
merger.VisitChunk(chunk);
}
@ -102,11 +98,11 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives
// rules.
foreach (var chunk in inherited)
{
if (ChunkMergers.TryGetValue(chunk.GetType(), out merger))
if (mergerMappings.TryGetValue(chunk.GetType(), out merger))
{
// TODO: When mapping chunks, we should remove mapping information since it would be incorrect
// to generate it in the page that inherits it. Tracked by #945
merger.Merge(CodeTree, chunk);
merger.Merge(codeTree, chunk);
}
}
}
@ -123,14 +119,14 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives
}
// TODO: This needs to be cached (#1016)
private CodeTree ParseViewFile(RazorTemplateEngine engine,
IFileInfo fileInfo)
private static CodeTree ParseViewFile(RazorTemplateEngine engine,
IFileInfo fileInfo)
{
using (var stream = fileInfo.CreateReadStream())
{
using (var streamReader = new StreamReader(stream))
{
var parseResults = engine.ParseTemplate(streamReader);
var parseResults = engine.ParseTemplate(streamReader, fileInfo.PhysicalPath);
var className = ParserHelpers.SanitizeClassName(fileInfo.Name);
var language = engine.Host.CodeLanguage;
var codeGenerator = language.CreateCodeGenerator(className,
@ -138,6 +134,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives
fileInfo.PhysicalPath,
engine.Host);
codeGenerator.Visit(parseResults);
return codeGenerator.Context.CodeTreeBuilder.CodeTree;
}
}

View File

@ -36,6 +36,7 @@ namespace Microsoft.AspNet.Mvc.Razor
// CodeGenerationContext.DefaultBaseClass is set to MyBaseType<dynamic>.
// This field holds the type name without the generic decoration (MyBaseType)
private readonly string _baseType;
private ChunkInheritanceUtility _chunkInheritanceUtility;
#if NET45
/// <summary>
@ -154,6 +155,20 @@ namespace Microsoft.AspNet.Mvc.Razor
get { return "CreateModelExpression"; }
}
private ChunkInheritanceUtility ChunkInheritanceUtility
{
get
{
if (_chunkInheritanceUtility == null)
{
// This needs to be lazily evaluated to support DefaultInheritedChunks being virtual.
_chunkInheritanceUtility = new ChunkInheritanceUtility(this, _fileSystem, DefaultInheritedChunks);
}
return _chunkInheritanceUtility;
}
}
/// <inheritdoc />
public GeneratorResults GenerateCode(string rootRelativePath, Stream inputStream)
{
@ -163,6 +178,13 @@ namespace Microsoft.AspNet.Mvc.Razor
return engine.GenerateCode(inputStream, className, DefaultNamespace, rootRelativePath);
}
/// <inheritdoc />
public override RazorParser DecorateRazorParser([NotNull] RazorParser razorParser, string sourceFileName)
{
var inheritedChunks = ChunkInheritanceUtility.GetInheritedChunks(sourceFileName);
return new MvcRazorParser(razorParser, inheritedChunks);
}
/// <inheritdoc />
public override ParserBase DecorateCodeParser([NotNull] ParserBase incomingCodeParser)
{
@ -173,10 +195,14 @@ namespace Microsoft.AspNet.Mvc.Razor
public override CodeBuilder DecorateCodeBuilder([NotNull] CodeBuilder incomingBuilder,
[NotNull] CodeBuilderContext context)
{
UpdateCodeBuilder(context);
var inheritedChunks = ChunkInheritanceUtility.GetInheritedChunks(context.SourceFile);
ChunkInheritanceUtility.MergeInheritedChunks(context.CodeTreeBuilder.CodeTree,
inheritedChunks,
DefaultModel);
return new MvcCSharpCodeBuilder(context,
DefaultModel,
DefaultModel,
ActivateAttribute,
new GeneratedTagHelperAttributeContext
{
@ -184,14 +210,5 @@ namespace Microsoft.AspNet.Mvc.Razor
CreateModelExpressionMethodName = CreateModelExpressionMethod
});
}
private void UpdateCodeBuilder(CodeGeneratorContext context)
{
var chunkUtility = new ChunkInheritanceUtility(context.CodeTreeBuilder.CodeTree,
DefaultInheritedChunks,
DefaultModel);
var inheritedChunks = chunkUtility.GetInheritedChunks(this, _fileSystem, context.SourceFile);
chunkUtility.MergeInheritedChunks(inheritedChunks);
}
}
}

View File

@ -0,0 +1,48 @@
// 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.Linq;
using Microsoft.AspNet.Razor.Generator.Compiler;
using Microsoft.AspNet.Razor.Parser;
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
using Microsoft.AspNet.Razor.TagHelpers;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// A subtype of <see cref="RazorParser"/> that <see cref="MvcRazorHost"/> uses to support inheritance of tag
/// helpers from _viewstart files.
/// </summary>
public class MvcRazorParser : RazorParser
{
private readonly IReadOnlyList<Chunk> _viewStartChunks;
/// <summary>
/// Initializes a new instance of <see cref="MvcRazorParser"/>.
/// </summary>
/// <param name="parser">The <see cref="RazorParser"/> to copy properties from.</param>
/// <param name="viewStartChunks">The <see cref="IReadOnlyList{T}"/> of <see cref="Chunk"/>s that are inherited
/// by parsed pages from _ViewStart files.</param>
public MvcRazorParser(RazorParser parser, IReadOnlyList<Chunk> viewStartChunks)
: base(parser)
{
_viewStartChunks = viewStartChunks;
}
/// <inheritdoc />
protected override IEnumerable<TagHelperDescriptor> GetTagHelperDescriptors([NotNull] Block documentRoot)
{
var descriptors = base.GetTagHelperDescriptors(documentRoot);
// TODO: https://github.com/aspnet/Razor/issues/112 Needs to support RemvoeHelperChunks too.
// Grab all the @addTagHelper chunks from view starts
var viewStartDescriptors = _viewStartChunks.OfType<AddTagHelperChunk>()
.Select(c => c.LookupText)
.SelectMany(TagHelperDescriptorResolver.Resolve);
return descriptors.Concat(viewStartDescriptors);
}
}
}

View File

@ -51,5 +51,26 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
Assert.Equal(expectedMediaType, response.Content.Headers.ContentType);
Assert.Equal(expectedContent, responseContent);
}
[Theory]
[InlineData("NestedViewStartTagHelper")]
[InlineData("ViewWithLayoutAndNestedTagHelper")]
public async Task TagHelpersAreInheritedFromViewStartPages(string action)
{
// Arrange
var expected = string.Join(Environment.NewLine,
"<root>root-content</root>",
"",
"",
"<nested>nested-content</nested>");
var server = TestServer.Create(_provider, _app);
var client = server.CreateClient();
// Act
var result = await client.GetStringAsync("http://localhost/Home/" + action);
// Assert
Assert.Equal(expected, result.Trim());
}
}
}

View File

@ -34,6 +34,7 @@
<div>
<p>Hello, you've reached the about page.</p>

View File

@ -33,6 +33,7 @@
<div>
<p>Hello, you've reached the help page. If you're having troubles try visiting our <a href="/?approved=true">My Approved Home Page</a></p>
</div>

View File

@ -41,6 +41,7 @@
<div>
<p>This website has <strong style="font-size: 1.25em;
text-decoration: underline;">not</strong> been approved yet. Visit <strong><a target="_blank" href="http://www.contoso.com">www.contoso.com</a></strong> for <strong>more</strong> information.</p>

View File

@ -1,10 +1,7 @@
// 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 Microsoft.AspNet.Razor.Generator.Compiler;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.Razor.Directives
@ -29,24 +26,30 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives
");
var host = new MvcRazorHost(fileSystem);
var utility = new ChunkInheritanceUtility(new CodeTree(), new Chunk[0], "dynamic");
var utility = new ChunkInheritanceUtility(host, fileSystem, new Chunk[0]);
// Act
var chunks = utility.GetInheritedChunks(host,
fileSystem,
@"x:\myapproot\views\home\Index.cshtml");
var chunks = utility.GetInheritedChunks(@"x:\myapproot\views\home\Index.cshtml");
// Assert
Assert.Equal(3, chunks.Count);
var usingChunk = Assert.IsType<UsingChunk>(chunks[0]);
Assert.Equal(8, chunks.Count);
Assert.IsType<LiteralChunk>(chunks[0]);
var usingChunk = Assert.IsType<UsingChunk>(chunks[1]);
Assert.Equal("MyNamespace", usingChunk.Namespace);
var injectChunk = Assert.IsType<InjectChunk>(chunks[1]);
Assert.IsType<LiteralChunk>(chunks[2]);
Assert.IsType<LiteralChunk>(chunks[3]);
var injectChunk = Assert.IsType<InjectChunk>(chunks[4]);
Assert.Equal("MyHelper<TModel>", injectChunk.TypeName);
Assert.Equal("Helper", injectChunk.MemberName);
var setBaseTypeChunk = Assert.IsType<SetBaseTypeChunk>(chunks[2]);
var setBaseTypeChunk = Assert.IsType<SetBaseTypeChunk>(chunks[5]);
Assert.Equal("MyBaseType", setBaseTypeChunk.TypeName);
Assert.IsType<StatementChunk>(chunks[6]);
Assert.IsType<LiteralChunk>(chunks[7]);
}
[Fact]
@ -58,12 +61,10 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives
fileSystem.AddFile(@"x:\myapproot\views\_Layout.cshtml", string.Empty);
fileSystem.AddFile(@"x:\myapproot\views\home\_not-viewstart.cshtml", string.Empty);
var host = new MvcRazorHost(fileSystem);
var utility = new ChunkInheritanceUtility(new CodeTree(), new Chunk[0], "dynamic");
var utility = new ChunkInheritanceUtility(host, fileSystem, new Chunk[0]);
// Act
var chunks = utility.GetInheritedChunks(host,
fileSystem,
@"x:\myapproot\views\home\Index.cshtml");
var chunks = utility.GetInheritedChunks(@"x:\myapproot\views\home\Index.cshtml");
// Assert
Assert.Empty(chunks);
@ -75,73 +76,26 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives
// Arrange
var fileSystem = new TestFileSystem();
fileSystem.AddFile(@"x:\myapproot\views\_viewstart.cshtml",
@"@inject DifferentHelper<TModel> Html
@using AppNamespace.Models
@{
Layout = ""test.cshtml"";
}
");
"@inject DifferentHelper<TModel> Html");
var host = new MvcRazorHost(fileSystem);
var defaultChunks = new Chunk[]
{
new InjectChunk("MyTestHtmlHelper", "Html"),
new UsingChunk { Namespace = "AppNamespace.Model" },
};
var utility = new ChunkInheritanceUtility(new CodeTree(), defaultChunks, "dynamic");
var utility = new ChunkInheritanceUtility(host, fileSystem, defaultChunks);
// Act
var chunks = utility.GetInheritedChunks(host,
fileSystem,
@"x:\myapproot\views\home\Index.cshtml");
var chunks = utility.GetInheritedChunks(@"x:\myapproot\views\home\Index.cshtml");
// Assert
Assert.Equal(4, chunks.Count);
var injectChunk = Assert.IsType<InjectChunk>(chunks[0]);
var injectChunk = Assert.IsType<InjectChunk>(chunks[1]);
Assert.Equal("DifferentHelper<TModel>", injectChunk.TypeName);
Assert.Equal("Html", injectChunk.MemberName);
var usingChunk = Assert.IsType<UsingChunk>(chunks[1]);
Assert.Equal("AppNamespace.Models", usingChunk.Namespace);
injectChunk = Assert.IsType<InjectChunk>(chunks[2]);
Assert.Equal("MyTestHtmlHelper", injectChunk.TypeName);
Assert.Equal("Html", injectChunk.MemberName);
usingChunk = Assert.IsType<UsingChunk>(chunks[3]);
Assert.Equal("AppNamespace.Model", usingChunk.Namespace);
}
[Fact]
public void MergeChunks_VisitsChunksPriorToMerging()
{
// Arrange
var codeTree = new CodeTree();
codeTree.Chunks.Add(new LiteralChunk());
codeTree.Chunks.Add(new ExpressionBlockChunk());
codeTree.Chunks.Add(new ExpressionBlockChunk());
var merger = new Mock<IChunkMerger>();
var mockSequence = new MockSequence();
merger.InSequence(mockSequence)
.Setup(m => m.VisitChunk(It.IsAny<LiteralChunk>()))
.Verifiable();
merger.InSequence(mockSequence)
.Setup(m => m.Merge(codeTree, It.IsAny<LiteralChunk>()))
.Verifiable();
var inheritedChunks = new List<Chunk>
{
new CodeAttributeChunk(),
new LiteralChunk()
};
var utility = new ChunkInheritanceUtility(codeTree, inheritedChunks, "dynamic");
// Act
utility.ChunkMergers[typeof(LiteralChunk)] = merger.Object;
utility.MergeInheritedChunks(inheritedChunks);
// Assert
merger.Verify();
Assert.Same(defaultChunks[0], chunks[2]);
Assert.Same(defaultChunks[1], chunks[3]);
}
}
}

View File

@ -24,7 +24,7 @@ namespace Microsoft.AspNet.Mvc.Razor
{
var fileInfo = new Mock<IFileInfo>();
fileInfo.Setup(f => f.CreateReadStream())
.Returns(new MemoryStream(Encoding.UTF8.GetBytes(contents)));
.Returns(() => new MemoryStream(Encoding.UTF8.GetBytes(contents)));
fileInfo.SetupGet(f => f.PhysicalPath)
.Returns(path);
fileInfo.SetupGet(f => f.Name)

View File

@ -29,5 +29,15 @@ namespace TagHelpersWebSite.Controllers
{
return View();
}
public ViewResult NestedViewStartTagHelper()
{
return View();
}
public ViewResult ViewWithLayoutAndNestedTagHelper()
{
return View();
}
}
}

View File

@ -0,0 +1,18 @@
// 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.Razor.Runtime.TagHelpers;
using Microsoft.AspNet.Razor.TagHelpers;
namespace TagHelpersWebSite.TagHelpers
{
[TagName("nested")]
[ContentBehavior(ContentBehavior.Modify)]
public class NestedViewStartTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Content = "nested-content";
}
}
}

View File

@ -0,0 +1,18 @@
// 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.Razor.Runtime.TagHelpers;
using Microsoft.AspNet.Razor.TagHelpers;
namespace TagHelpersWebSite.TagHelpers
{
[TagName("root")]
[ContentBehavior(ContentBehavior.Replace)]
public class RootViewStartTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Content = "root-content";
}
}
}

View File

@ -0,0 +1,7 @@
@{
Layout = null;
}
<root />
<nested>some-content</nested>

View File

@ -0,0 +1 @@
@addtaghelper "TagHelpersWebSite.TagHelpers.NestedViewStartTagHelper, TagHelpersWebSite"

View File

@ -0,0 +1,5 @@
@{
Layout = "~/Views/Shared/_LayoutWithRootTagHelper.cshtml";
}
@addtaghelper "TagHelpersWebSite.TagHelpers.NestedViewStartTagHelper, TagHelpersWebSite"
<nested>some-content</nested>

View File

@ -0,0 +1,2 @@
<root />
@RenderBody()

View File

@ -1,3 +1,4 @@
@{
Layout = "/Views/Shared/_Layout.cshtml";
}
}
@addtaghelper "TagHelpersWebSite.TagHelpers.RootViewStartTagHelper, TagHelpersWebSite"