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;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using Microsoft.AspNet.FileSystems; using Microsoft.AspNet.FileSystems;
using Microsoft.AspNet.Razor; using Microsoft.AspNet.Razor;
using Microsoft.AspNet.Razor.Generator.Compiler; using Microsoft.AspNet.Razor.Generator.Compiler;
@ -13,63 +12,57 @@ using Microsoft.AspNet.Razor.Parser;
namespace Microsoft.AspNet.Mvc.Razor.Directives namespace Microsoft.AspNet.Mvc.Razor.Directives
{ {
/// <summary> /// <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> /// </summary>
public class ChunkInheritanceUtility 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> /// <summary>
/// Instantiates a new instance of <see cref="ChunkInheritanceUtility"/>. /// Initializes 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.
/// </summary> /// </summary>
/// <param name="razorHost">The <see cref="MvcRazorHost"/> used to parse _ViewStart pages.</param> /// <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="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> /// <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> /// <returns>A <see cref="IReadOnlyList{T}"/> of <see cref="Chunk"/> from _ViewStart pages.</returns>
public List<Chunk> GetInheritedChunks([NotNull] MvcRazorHost razorHost, public IReadOnlyList<Chunk> GetInheritedChunks([NotNull] string pagePath)
[NotNull] IFileSystem fileSystem,
[NotNull] string pagePath)
{ {
var inheritedChunks = new List<Chunk>(); var inheritedChunks = new List<Chunk>();
var templateEngine = new RazorTemplateEngine(razorHost); var templateEngine = new RazorTemplateEngine(_razorHost);
foreach (var viewStart in ViewStartUtility.GetViewStartLocations(fileSystem, pagePath)) foreach (var viewStart in ViewStartUtility.GetViewStartLocations(_fileSystem, pagePath))
{ {
CodeTree codeTree;
IFileInfo fileInfo; IFileInfo fileInfo;
if (fileSystem.TryGetFileInfo(viewStart, out fileInfo))
if (_parsedCodeTrees.TryGetValue(viewStart, out codeTree))
{ {
var parsedTree = ParseViewFile(templateEngine, fileInfo); inheritedChunks.AddRange(codeTree.Chunks);
var chunksToAdd = parsedTree.Chunks }
.Where(chunk => ChunkMergers.ContainsKey(chunk.GetType())); else if (_fileSystem.TryGetFileInfo(viewStart, out fileInfo))
inheritedChunks.AddRange(chunksToAdd); {
codeTree = ParseViewFile(templateEngine, fileInfo);
_parsedCodeTrees.Add(viewStart, codeTree);
inheritedChunks.AddRange(codeTree.Chunks);
} }
} }
@ -79,19 +72,22 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives
} }
/// <summary> /// <summary>
/// Merges a list of chunks into the <see cref="CodeTree"/> instance. /// Merges a list of chunks into the specified <paramref name="codeTree/>.
/// </summary> /// </summary>
/// <param name="codeTree">The <see cref="CodeTree"/> to mere</param>
/// <param name="inherited">The list of chunks to merge.</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; IChunkMerger merger;
// We merge chunks into the codeTree in two passes. In the first pass, we traverse the CodeTree visiting // 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. // 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); merger.VisitChunk(chunk);
} }
@ -102,11 +98,11 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives
// rules. // rules.
foreach (var chunk in inherited) 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 // 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 // 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) // TODO: This needs to be cached (#1016)
private CodeTree ParseViewFile(RazorTemplateEngine engine, private static CodeTree ParseViewFile(RazorTemplateEngine engine,
IFileInfo fileInfo) IFileInfo fileInfo)
{ {
using (var stream = fileInfo.CreateReadStream()) using (var stream = fileInfo.CreateReadStream())
{ {
using (var streamReader = new StreamReader(stream)) 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 className = ParserHelpers.SanitizeClassName(fileInfo.Name);
var language = engine.Host.CodeLanguage; var language = engine.Host.CodeLanguage;
var codeGenerator = language.CreateCodeGenerator(className, var codeGenerator = language.CreateCodeGenerator(className,
@ -138,6 +134,7 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives
fileInfo.PhysicalPath, fileInfo.PhysicalPath,
engine.Host); engine.Host);
codeGenerator.Visit(parseResults); codeGenerator.Visit(parseResults);
return codeGenerator.Context.CodeTreeBuilder.CodeTree; return codeGenerator.Context.CodeTreeBuilder.CodeTree;
} }
} }

View File

@ -36,6 +36,7 @@ namespace Microsoft.AspNet.Mvc.Razor
// CodeGenerationContext.DefaultBaseClass is set to MyBaseType<dynamic>. // CodeGenerationContext.DefaultBaseClass is set to MyBaseType<dynamic>.
// This field holds the type name without the generic decoration (MyBaseType) // This field holds the type name without the generic decoration (MyBaseType)
private readonly string _baseType; private readonly string _baseType;
private ChunkInheritanceUtility _chunkInheritanceUtility;
#if NET45 #if NET45
/// <summary> /// <summary>
@ -154,6 +155,20 @@ namespace Microsoft.AspNet.Mvc.Razor
get { return "CreateModelExpression"; } 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 /> /// <inheritdoc />
public GeneratorResults GenerateCode(string rootRelativePath, Stream inputStream) public GeneratorResults GenerateCode(string rootRelativePath, Stream inputStream)
{ {
@ -163,6 +178,13 @@ namespace Microsoft.AspNet.Mvc.Razor
return engine.GenerateCode(inputStream, className, DefaultNamespace, rootRelativePath); 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 /> /// <inheritdoc />
public override ParserBase DecorateCodeParser([NotNull] ParserBase incomingCodeParser) public override ParserBase DecorateCodeParser([NotNull] ParserBase incomingCodeParser)
{ {
@ -173,10 +195,14 @@ namespace Microsoft.AspNet.Mvc.Razor
public override CodeBuilder DecorateCodeBuilder([NotNull] CodeBuilder incomingBuilder, public override CodeBuilder DecorateCodeBuilder([NotNull] CodeBuilder incomingBuilder,
[NotNull] CodeBuilderContext context) [NotNull] CodeBuilderContext context)
{ {
UpdateCodeBuilder(context); var inheritedChunks = ChunkInheritanceUtility.GetInheritedChunks(context.SourceFile);
ChunkInheritanceUtility.MergeInheritedChunks(context.CodeTreeBuilder.CodeTree,
inheritedChunks,
DefaultModel);
return new MvcCSharpCodeBuilder(context, return new MvcCSharpCodeBuilder(context,
DefaultModel, DefaultModel,
ActivateAttribute, ActivateAttribute,
new GeneratedTagHelperAttributeContext new GeneratedTagHelperAttributeContext
{ {
@ -184,14 +210,5 @@ namespace Microsoft.AspNet.Mvc.Razor
CreateModelExpressionMethodName = CreateModelExpressionMethod 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(expectedMediaType, response.Content.Headers.ContentType);
Assert.Equal(expectedContent, responseContent); 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> <div>
<p>Hello, you've reached the about page.</p> <p>Hello, you've reached the about page.</p>

View File

@ -33,6 +33,7 @@
<div> <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> <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> </div>

View File

@ -41,6 +41,7 @@
<div> <div>
<p>This website has <strong style="font-size: 1.25em; <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> 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. // 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. // 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 Microsoft.AspNet.Razor.Generator.Compiler;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNet.Mvc.Razor.Directives namespace Microsoft.AspNet.Mvc.Razor.Directives
@ -29,24 +26,30 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives
"); ");
var host = new MvcRazorHost(fileSystem); 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 // Act
var chunks = utility.GetInheritedChunks(host, var chunks = utility.GetInheritedChunks(@"x:\myapproot\views\home\Index.cshtml");
fileSystem,
@"x:\myapproot\views\home\Index.cshtml");
// Assert // Assert
Assert.Equal(3, chunks.Count); Assert.Equal(8, chunks.Count);
var usingChunk = Assert.IsType<UsingChunk>(chunks[0]); Assert.IsType<LiteralChunk>(chunks[0]);
var usingChunk = Assert.IsType<UsingChunk>(chunks[1]);
Assert.Equal("MyNamespace", usingChunk.Namespace); 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("MyHelper<TModel>", injectChunk.TypeName);
Assert.Equal("Helper", injectChunk.MemberName); 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.Equal("MyBaseType", setBaseTypeChunk.TypeName);
Assert.IsType<StatementChunk>(chunks[6]);
Assert.IsType<LiteralChunk>(chunks[7]);
} }
[Fact] [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\_Layout.cshtml", string.Empty);
fileSystem.AddFile(@"x:\myapproot\views\home\_not-viewstart.cshtml", string.Empty); fileSystem.AddFile(@"x:\myapproot\views\home\_not-viewstart.cshtml", string.Empty);
var host = new MvcRazorHost(fileSystem); 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 // Act
var chunks = utility.GetInheritedChunks(host, var chunks = utility.GetInheritedChunks(@"x:\myapproot\views\home\Index.cshtml");
fileSystem,
@"x:\myapproot\views\home\Index.cshtml");
// Assert // Assert
Assert.Empty(chunks); Assert.Empty(chunks);
@ -75,73 +76,26 @@ namespace Microsoft.AspNet.Mvc.Razor.Directives
// Arrange // Arrange
var fileSystem = new TestFileSystem(); var fileSystem = new TestFileSystem();
fileSystem.AddFile(@"x:\myapproot\views\_viewstart.cshtml", fileSystem.AddFile(@"x:\myapproot\views\_viewstart.cshtml",
@"@inject DifferentHelper<TModel> Html "@inject DifferentHelper<TModel> Html");
@using AppNamespace.Models
@{
Layout = ""test.cshtml"";
}
");
var host = new MvcRazorHost(fileSystem); var host = new MvcRazorHost(fileSystem);
var defaultChunks = new Chunk[] var defaultChunks = new Chunk[]
{ {
new InjectChunk("MyTestHtmlHelper", "Html"), new InjectChunk("MyTestHtmlHelper", "Html"),
new UsingChunk { Namespace = "AppNamespace.Model" }, new UsingChunk { Namespace = "AppNamespace.Model" },
}; };
var utility = new ChunkInheritanceUtility(new CodeTree(), defaultChunks, "dynamic"); var utility = new ChunkInheritanceUtility(host, fileSystem, defaultChunks);
// Act // Act
var chunks = utility.GetInheritedChunks(host, var chunks = utility.GetInheritedChunks(@"x:\myapproot\views\home\Index.cshtml");
fileSystem,
@"x:\myapproot\views\home\Index.cshtml");
// Assert // Assert
Assert.Equal(4, chunks.Count); 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("DifferentHelper<TModel>", injectChunk.TypeName);
Assert.Equal("Html", injectChunk.MemberName); Assert.Equal("Html", injectChunk.MemberName);
var usingChunk = Assert.IsType<UsingChunk>(chunks[1]); Assert.Same(defaultChunks[0], chunks[2]);
Assert.Equal("AppNamespace.Models", usingChunk.Namespace); Assert.Same(defaultChunks[1], chunks[3]);
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();
} }
} }
} }

View File

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

View File

@ -29,5 +29,15 @@ namespace TagHelpersWebSite.Controllers
{ {
return View(); 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"; Layout = "/Views/Shared/_Layout.cshtml";
} }
@addtaghelper "TagHelpersWebSite.TagHelpers.RootViewStartTagHelper, TagHelpersWebSite"