diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/ChunkInheritanceUtility.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/ChunkInheritanceUtility.cs index 33bbb9675b..f2ac93f7e9 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/ChunkInheritanceUtility.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/Directives/ChunkInheritanceUtility.cs @@ -4,6 +4,7 @@ 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; @@ -20,7 +21,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives private readonly Dictionary _parsedCodeTrees; private readonly MvcRazorHost _razorHost; private readonly IFileSystem _fileSystem; - private readonly IEnumerable _defaultInheritedChunks; + private readonly IReadOnlyList _defaultInheritedChunks; /// /// Initializes a new instance of . @@ -30,7 +31,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives /// Sequence of s inherited by default. public ChunkInheritanceUtility([NotNull] MvcRazorHost razorHost, [NotNull] IFileSystem fileSystem, - [NotNull] IEnumerable defaultInheritedChunks) + [NotNull] IReadOnlyList defaultInheritedChunks) { _razorHost = razorHost; _fileSystem = fileSystem; @@ -39,14 +40,16 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives } /// - /// Gets a of containing parsed results of _ViewStart files - /// that are used for inheriting tag helpers and chunks to the page located at . + /// Gets an ordered of parsed for each _ViewStart that + /// is applicable to the page located at . The list is ordered so that the + /// for the _ViewStart closest to the in the filesystem + /// appears first. /// /// The path of the page to locate inherited chunks for. - /// A of from _ViewStart pages. - public IReadOnlyList GetInheritedChunks([NotNull] string pagePath) + /// A of parsed _ViewStart s. + public IReadOnlyList GetInheritedCodeTrees([NotNull] string pagePath) { - var inheritedChunks = new List(); + var inheritedCodeTrees = new List(); var templateEngine = new RazorTemplateEngine(_razorHost); foreach (var viewStartPath in ViewStartUtility.GetViewStartLocations(pagePath)) @@ -55,7 +58,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives if (_parsedCodeTrees.TryGetValue(viewStartPath, out codeTree)) { - inheritedChunks.AddRange(codeTree.Chunks); + inheritedCodeTrees.Add(codeTree); } else { @@ -68,25 +71,26 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives // for the current _ViewStart to succeed. codeTree = ParseViewFile(templateEngine, fileInfo, viewStartPath); _parsedCodeTrees.Add(viewStartPath, codeTree); - inheritedChunks.AddRange(codeTree.Chunks); + + inheritedCodeTrees.Add(codeTree); } } } - inheritedChunks.AddRange(_defaultInheritedChunks); - - return inheritedChunks; + return inheritedCodeTrees; } /// - /// Merges a list of chunks into the specified . + /// Merges inherited by default and instances produced by parsing + /// _ViewStart files into the specified . /// - /// The to merge. - /// The of to merge. + /// The to merge in to. + /// inherited from _ViewStart + /// files. /// The list of chunks to merge. - public void MergeInheritedChunks([NotNull] CodeTree codeTree, - [NotNull] IReadOnlyList inherited, - string defaultModel) + public void MergeInheritedCodeTrees([NotNull] CodeTree codeTree, + [NotNull] IReadOnlyList inheritedCodeTrees, + string defaultModel) { var mergerMappings = GetMergerMappings(codeTree, defaultModel); IChunkMerger merger; @@ -104,7 +108,12 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives // In the second phase we invoke IChunkMerger.Merge for each chunk that has a mapped merger. // During this phase, the merger can either add to the CodeTree or ignore the chunk based on the merging // rules. - foreach (var chunk in inherited) + // Read the chunks outside in - that is chunks from the _ViewStart closest to the page get merged in first + // and the furthest one last. This allows the merger to ignore a directive like @model that was previously + // seen. + var chunksToMerge = inheritedCodeTrees.SelectMany(tree => tree.Chunks) + .Concat(_defaultInheritedChunks); + foreach (var chunk in chunksToMerge) { if (mergerMappings.TryGetValue(chunk.GetType(), out merger)) { diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs index 450c96c5e5..be3b1a43d0 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs @@ -183,8 +183,8 @@ namespace Microsoft.AspNet.Mvc.Razor /// public override RazorParser DecorateRazorParser([NotNull] RazorParser razorParser, string sourceFileName) { - var inheritedChunks = ChunkInheritanceUtility.GetInheritedChunks(sourceFileName); - return new MvcRazorParser(razorParser, inheritedChunks); + var inheritedCodeTrees = ChunkInheritanceUtility.GetInheritedCodeTrees(sourceFileName); + return new MvcRazorParser(razorParser, inheritedCodeTrees, DefaultInheritedChunks); } /// @@ -197,9 +197,9 @@ namespace Microsoft.AspNet.Mvc.Razor public override CodeBuilder DecorateCodeBuilder([NotNull] CodeBuilder incomingBuilder, [NotNull] CodeBuilderContext context) { - var inheritedChunks = ChunkInheritanceUtility.GetInheritedChunks(context.SourceFile); + var inheritedChunks = ChunkInheritanceUtility.GetInheritedCodeTrees(context.SourceFile); - ChunkInheritanceUtility.MergeInheritedChunks(context.CodeTreeBuilder.CodeTree, + ChunkInheritanceUtility.MergeInheritedCodeTrees(context.CodeTreeBuilder.CodeTree, inheritedChunks, DefaultModel); diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorParser.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorParser.cs index f582bb8723..a3834072c3 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorParser.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorParser.cs @@ -1,6 +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 System.Linq; using Microsoft.AspNet.Razor.Generator.Compiler; @@ -8,6 +9,7 @@ using Microsoft.AspNet.Razor.Parser; using Microsoft.AspNet.Razor.Parser.SyntaxTree; using Microsoft.AspNet.Razor.Parser.TagHelpers; using Microsoft.AspNet.Razor.TagHelpers; +using Microsoft.AspNet.Razor.Text; namespace Microsoft.AspNet.Mvc.Razor { @@ -17,18 +19,23 @@ namespace Microsoft.AspNet.Mvc.Razor /// public class MvcRazorParser : RazorParser { - private readonly IReadOnlyList _viewStartChunks; + private readonly IEnumerable _viewStartDirectiveDescriptors; /// /// Initializes a new instance of . /// /// The to copy properties from. - /// The of s that are inherited - /// by parsed pages from _ViewStart files. - public MvcRazorParser(RazorParser parser, IReadOnlyList viewStartChunks) + /// The s that are inherited + /// from parsed pages from _ViewStart files. + /// The inherited by + /// default by all Razor pages in the application. + public MvcRazorParser([NotNull] RazorParser parser, + [NotNull] IReadOnlyList inheritedCodeTrees, + [NotNull] IReadOnlyList defaultInheritedChunks) : base(parser) { - _viewStartChunks = viewStartChunks; + // Construct tag helper descriptors from @addTagHelper and @removeTagHelper chunks + _viewStartDirectiveDescriptors = GetTagHelperDescriptors(inheritedCodeTrees, defaultInheritedChunks); } /// @@ -36,17 +43,47 @@ namespace Microsoft.AspNet.Mvc.Razor [NotNull] Block documentRoot, [NotNull] ParserErrorSink errorSink) { - // Grab all the @addtaghelper chunks from view starts and construct TagHelperDirectiveDescriptors - var directiveDescriptors = _viewStartChunks.OfType() - .Select(chunk => new TagHelperDirectiveDescriptor( - chunk.LookupText, - chunk.Start, - TagHelperDirectiveType.AddTagHelper)); - var visitor = new ViewStartAddRemoveTagHelperVisitor(TagHelperDescriptorResolver, - directiveDescriptors, + _viewStartDirectiveDescriptors, errorSink); - var descriptors = visitor.GetDescriptors(documentRoot); + return visitor.GetDescriptors(documentRoot); + } + + private static IEnumerable GetTagHelperDescriptors( + IReadOnlyList inheritedCodeTrees, + IReadOnlyList defaultInheritedChunks) + { + var descriptors = new List(); + + // For tag helpers, the @removeTagHelper only applies tag helpers that were added prior to it. + // Consequently we must visit tag helpers outside-in - furthest _ViewStart first and nearest one last. This + // is different from the behavior of chunk merging where we visit the nearest one first and ignore chunks + // that were previously visited. + var chunksFromViewStarts = inheritedCodeTrees.Reverse() + .SelectMany(tree => tree.Chunks); + var chunksInOrder = defaultInheritedChunks.Concat(chunksFromViewStarts); + foreach (var chunk in chunksInOrder) + { + var addHelperChunk = chunk as AddTagHelperChunk; + if (addHelperChunk != null) + { + var descriptor = new TagHelperDirectiveDescriptor(addHelperChunk.LookupText, + SourceLocation.Undefined, + TagHelperDirectiveType.AddTagHelper); + descriptors.Add(descriptor); + } + else + { + var removeHelperChunk = chunk as RemoveTagHelperChunk; + if (removeHelperChunk != null) + { + var descriptor = new TagHelperDirectiveDescriptor(removeHelperChunk.LookupText, + SourceLocation.Undefined, + TagHelperDirectiveType.RemoveTagHelper); + descriptors.Add(descriptor); + } + } + } return descriptors; } diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/TagHelpersTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/TagHelpersTest.cs index 25700cc545..8fca5826d5 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/TagHelpersTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/TagHelpersTest.cs @@ -54,17 +54,40 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests Assert.Equal(expectedContent, responseContent); } + public static IEnumerable TagHelpersAreInheritedFromViewStartPagesData + { + get + { + var expected1 = +@"root-content + + +nested-content"; + yield return new[] { "NestedViewStartTagHelper", expected1 }; + + var expected2 = +@"layout:root-content + + +nested-content"; + + yield return new[] { "ViewWithLayoutAndNestedTagHelper", expected2 }; + + var expected3 = +@"layout:root-content + + +page: +nested-content"; + yield return new[] { "ViewWithInheritedRemoveTagHelper", expected3 }; + } + } + [Theory] - [InlineData("NestedViewStartTagHelper")] - [InlineData("ViewWithLayoutAndNestedTagHelper")] - public async Task TagHelpersAreInheritedFromViewStartPages(string action) + [MemberData(nameof(TagHelpersAreInheritedFromViewStartPagesData))] + public async Task TagHelpersAreInheritedFromViewStartPages(string action, string expected) { // Arrange - var expected = string.Join(Environment.NewLine, - "root-content", - "", - "", - "nested-content"); var server = TestServer.Create(_provider, _app); var client = server.CreateClient(); diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/ChunkInheritanceUtilityTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/ChunkInheritanceUtilityTest.cs index b39ba3a8ac..48ccaa0e84 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/ChunkInheritanceUtilityTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/Directives/ChunkInheritanceUtilityTest.cs @@ -25,31 +25,41 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives } "); + var defaultChunks = new Chunk[] + { + new InjectChunk("MyTestHtmlHelper", "Html"), + new UsingChunk { Namespace = "AppNamespace.Model" }, + }; var host = new MvcRazorHost(fileSystem); - var utility = new ChunkInheritanceUtility(host, fileSystem, new Chunk[0]); + var utility = new ChunkInheritanceUtility(host, fileSystem, defaultChunks); // Act - var chunks = utility.GetInheritedChunks(@"Views\home\Index.cshtml"); + var codeTrees = utility.GetInheritedCodeTrees(@"Views\home\Index.cshtml"); // Assert - Assert.Equal(8, chunks.Count); - Assert.IsType(chunks[0]); + Assert.Equal(2, codeTrees.Count); + var viewStartChunks = codeTrees[0].Chunks; + Assert.Equal(3, viewStartChunks.Count); - var usingChunk = Assert.IsType(chunks[1]); + Assert.IsType(viewStartChunks[0]); + var usingChunk = Assert.IsType(viewStartChunks[1]); Assert.Equal("MyNamespace", usingChunk.Namespace); + Assert.IsType(viewStartChunks[2]); - Assert.IsType(chunks[2]); - Assert.IsType(chunks[3]); + viewStartChunks = codeTrees[1].Chunks; + Assert.Equal(5, viewStartChunks.Count); - var injectChunk = Assert.IsType(chunks[4]); + Assert.IsType(viewStartChunks[0]); + + var injectChunk = Assert.IsType(viewStartChunks[1]); Assert.Equal("MyHelper", injectChunk.TypeName); Assert.Equal("Helper", injectChunk.MemberName); - var setBaseTypeChunk = Assert.IsType(chunks[5]); + var setBaseTypeChunk = Assert.IsType(viewStartChunks[2]); Assert.Equal("MyBaseType", setBaseTypeChunk.TypeName); - Assert.IsType(chunks[6]); - Assert.IsType(chunks[7]); + Assert.IsType(viewStartChunks[3]); + Assert.IsType(viewStartChunks[4]); } [Fact] @@ -61,17 +71,22 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives fileSystem.AddFile(@"Views\_Layout.cshtml", string.Empty); fileSystem.AddFile(@"Views\home\_not-viewstart.cshtml", string.Empty); var host = new MvcRazorHost(fileSystem); - var utility = new ChunkInheritanceUtility(host, fileSystem, new Chunk[0]); + var defaultChunks = new Chunk[] + { + new InjectChunk("MyTestHtmlHelper", "Html"), + new UsingChunk { Namespace = "AppNamespace.Model" }, + }; + var utility = new ChunkInheritanceUtility(host, fileSystem, defaultChunks); // Act - var chunks = utility.GetInheritedChunks(@"Views\home\Index.cshtml"); + var codeTrees = utility.GetInheritedCodeTrees(@"Views\home\Index.cshtml"); // Assert - Assert.Empty(chunks); + Assert.Empty(codeTrees); } [Fact] - public void GetInheritedChunks_ReturnsDefaultInheritedChunks() + public void MergeInheritedChunks_MergesDefaultInheritedChunks() { // Arrange var fileSystem = new TestFileSystem(); @@ -83,19 +98,38 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives new InjectChunk("MyTestHtmlHelper", "Html"), new UsingChunk { Namespace = "AppNamespace.Model" }, }; + var inheritedCodeTrees = new CodeTree[] + { + new CodeTree + { + Chunks = new Chunk[] + { + new UsingChunk { Namespace = "InheritedNamespace" }, + new LiteralChunk { Text = "some text" } + } + }, + new CodeTree + { + Chunks = new Chunk[] + { + new UsingChunk { Namespace = "AppNamespace.Model" }, + } + } + }; + var utility = new ChunkInheritanceUtility(host, fileSystem, defaultChunks); + var codeTree = new CodeTree(); // Act - var chunks = utility.GetInheritedChunks(@"Views\Home\Index.cshtml"); + utility.MergeInheritedCodeTrees(codeTree, + inheritedCodeTrees, + "dynamic"); // Assert - Assert.Equal(4, chunks.Count); - var injectChunk = Assert.IsType(chunks[1]); - Assert.Equal("DifferentHelper", injectChunk.TypeName); - Assert.Equal("Html", injectChunk.MemberName); - - Assert.Same(defaultChunks[0], chunks[2]); - Assert.Same(defaultChunks[1], chunks[3]); + Assert.Equal(3, codeTree.Chunks.Count); + Assert.Same(inheritedCodeTrees[0].Chunks[0], codeTree.Chunks[0]); + Assert.Same(inheritedCodeTrees[1].Chunks[0], codeTree.Chunks[1]); + Assert.Same(defaultChunks[0], codeTree.Chunks[2]); } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Host.Test/MvcRazorParserTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/MvcRazorParserTest.cs new file mode 100644 index 0000000000..b046cb2b7c --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Host.Test/MvcRazorParserTest.cs @@ -0,0 +1,91 @@ +// 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; +using Microsoft.AspNet.Razor.Text; +using Moq; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class MvcRazorCodeParserTest + { + [Fact] + public void GetTagHelperDescriptors_ReturnsDescriptorsFromViewStart() + { + // Arrange + var builder = new BlockBuilder { Type = BlockType.Comment }; + var block = new Block(builder); + var codeTrees = new[] + { + new CodeTree + { + Chunks = new Chunk[] + { + new LiteralChunk { Text = "Hello world" }, + new AddTagHelperChunk { LookupText = "Add Tag Helper" }, + } + }, + new CodeTree + { + Chunks = new[] + { + new RemoveTagHelperChunk { LookupText = "Remove Tag Helper" }, + } + } + }; + + IList descriptors = null; + var resolver = new Mock(); + resolver.Setup(r => r.Resolve(It.IsAny())) + .Callback((TagHelperDescriptorResolutionContext context) => + { + descriptors = context.DirectiveDescriptors; + }) + .Returns(Enumerable.Empty()) + .Verifiable(); + + var baseParser = new RazorParser(new CSharpCodeParser(), + new HtmlMarkupParser(), + resolver.Object); + var parser = new TestableMvcRazorParser(baseParser, codeTrees, new Chunk[0]); + var sink = new ParserErrorSink(); + + // Act + var result = parser.GetTagHelperDescriptorsPublic(block, sink).ToArray(); + + // Assert + Assert.NotNull(descriptors); + Assert.Equal(2, descriptors.Count); + + Assert.Equal("Remove Tag Helper", descriptors[0].LookupText); + Assert.Equal(SourceLocation.Undefined, descriptors[0].Location); + + Assert.Equal("Add Tag Helper", descriptors[1].LookupText); + Assert.Equal(TagHelperDirectiveType.AddTagHelper, descriptors[1].DirectiveType); + Assert.Equal(SourceLocation.Undefined, descriptors[1].Location); + } + + private class TestableMvcRazorParser : MvcRazorParser + { + public TestableMvcRazorParser(RazorParser parser, + IReadOnlyList codeTrees, + IReadOnlyList defaultInheritedChunks) + : base(parser, codeTrees, defaultInheritedChunks) + { + } + + public IEnumerable GetTagHelperDescriptorsPublic( + Block documentRoot, + ParserErrorSink errorSink) + { + return GetTagHelperDescriptors(documentRoot, errorSink); + } + } + } +} \ No newline at end of file diff --git a/test/WebSites/TagHelpersWebSite/Controllers/HomeController.cs b/test/WebSites/TagHelpersWebSite/Controllers/HomeController.cs index 5005a39e68..d0336d685c 100644 --- a/test/WebSites/TagHelpersWebSite/Controllers/HomeController.cs +++ b/test/WebSites/TagHelpersWebSite/Controllers/HomeController.cs @@ -39,5 +39,10 @@ namespace TagHelpersWebSite.Controllers { return View(); } + + public ViewResult ViewWithInheritedRemoveTagHelper() + { + return View("/Views/RemoveTagHelperViewStart/ViewWithInheritedRemoveTagHelper.cshtml"); + } } } \ No newline at end of file diff --git a/test/WebSites/TagHelpersWebSite/Views/RemoveTagHelperViewStart/ViewWithInheritedRemoveTagHelper.cshtml b/test/WebSites/TagHelpersWebSite/Views/RemoveTagHelperViewStart/ViewWithInheritedRemoveTagHelper.cshtml new file mode 100644 index 0000000000..fd0848f854 --- /dev/null +++ b/test/WebSites/TagHelpersWebSite/Views/RemoveTagHelperViewStart/ViewWithInheritedRemoveTagHelper.cshtml @@ -0,0 +1,2 @@ +page: +some-content \ No newline at end of file diff --git a/test/WebSites/TagHelpersWebSite/Views/RemoveTagHelperViewStart/_ViewStart.cshtml b/test/WebSites/TagHelpersWebSite/Views/RemoveTagHelperViewStart/_ViewStart.cshtml new file mode 100644 index 0000000000..07c5a53fe7 --- /dev/null +++ b/test/WebSites/TagHelpersWebSite/Views/RemoveTagHelperViewStart/_ViewStart.cshtml @@ -0,0 +1,5 @@ +@{ + Layout = "~/Views/Shared/_LayoutWithRootTagHelper.cshtml"; +} +@removetaghelper "TagHelpersWebSite.TagHelpers.RootViewStartTagHelper, TagHelpersWebSite" +@addtaghelper "TagHelpersWebSite.TagHelpers.NestedViewStartTagHelper, TagHelpersWebSite" \ No newline at end of file diff --git a/test/WebSites/TagHelpersWebSite/Views/Shared/_LayoutWithRootTagHelper.cshtml b/test/WebSites/TagHelpersWebSite/Views/Shared/_LayoutWithRootTagHelper.cshtml index d08aa242dd..5d3acee440 100644 --- a/test/WebSites/TagHelpersWebSite/Views/Shared/_LayoutWithRootTagHelper.cshtml +++ b/test/WebSites/TagHelpersWebSite/Views/Shared/_LayoutWithRootTagHelper.cshtml @@ -1,2 +1,2 @@ - +layout: @RenderBody() \ No newline at end of file