From 082512f63c639381f79cb02589383fdf43bd80fd Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Thu, 25 Sep 2014 17:45:44 -0700 Subject: [PATCH] Add writing scopes to RazorPage. - RazorPage now has the ability to use writing scopes to control where things are written. - This enables RazorPages to use these writing scopes with TagHelpers. TagHelpers use them to buffer attributes that have C# contained within them and to also buffer content of TagHelpers whos ContentBehavior is Modify. - Added RazorPage tests to validate their functionality. #1102 --- .../MvcRazorHost.cs | 5 +- .../Properties/Resources.Designer.cs | 32 +++++ src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs | 62 ++++++++++ src/Microsoft.AspNet.Mvc.Razor/Resources.resx | 6 + .../RazorPageTest.cs | 111 ++++++++++++++++++ 5 files changed, 215 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs index 089d3b5c47..33c90019e4 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs @@ -76,6 +76,7 @@ namespace Microsoft.AspNet.Mvc.Razor DefaultBaseClass = BaseType + '<' + DefaultModel + '>'; DefaultNamespace = "Asp"; GeneratedClassContext = new GeneratedClassContext( + executeMethodName: "ExecuteAsync", writeMethodName: "Write", writeLiteralMethodName: "WriteLiteral", @@ -87,7 +88,9 @@ namespace Microsoft.AspNet.Mvc.Razor { RunnerTypeName = typeof(TagHelperRunner).FullName, ScopeManagerTypeName = typeof(TagHelperScopeManager).FullName, - ExecutionContextTypeName = typeof(TagHelpersExecutionContext).FullName + ExecutionContextTypeName = typeof(TagHelpersExecutionContext).FullName, + StartWritingScopeMethodName = "StartWritingScope", + EndWritingScopeMethodName = "EndWritingScope" }) { ResolveUrlMethodName = "Href", diff --git a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs index b897a77c8f..848fe0a25a 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs @@ -154,6 +154,38 @@ namespace Microsoft.AspNet.Mvc.Razor return string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_OnlyOneModelStatementIsAllowed"), p0); } + /// + /// There is no active writing scope to end. + /// + internal static string RazorPage_ThereIsNoActiveWritingScopeToEnd + { + get { return GetString("RazorPage_ThereIsNoActiveWritingScopeToEnd"); } + } + + /// + /// There is no active writing scope to end. + /// + internal static string FormatRazorPage_ThereIsNoActiveWritingScopeToEnd() + { + return GetString("RazorPage_ThereIsNoActiveWritingScopeToEnd"); + } + + /// + /// You cannot flush while inside a writing scope. + /// + internal static string RazorPage_YouCannotFlushWhileInAWritingScope + { + get { return GetString("RazorPage_YouCannotFlushWhileInAWritingScope"); } + } + + /// + /// You cannot flush while inside a writing scope. + /// + internal static string FormatRazorPage_YouCannotFlushWhileInAWritingScope() + { + return GetString("RazorPage_YouCannotFlushWhileInAWritingScope"); + } + /// /// {0} can only be called from a layout page. /// diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs index 4e6abbcf78..a0f4e47b19 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs @@ -21,12 +21,16 @@ namespace Microsoft.AspNet.Mvc.Razor public abstract class RazorPage : IRazorPage { private readonly HashSet _renderedSections = new HashSet(StringComparer.OrdinalIgnoreCase); + private readonly Stack _writerScopes; + private TextWriter _originalWriter; private IUrlHelper _urlHelper; private bool _renderedBody; public RazorPage() { SectionWriters = new Dictionary(StringComparer.OrdinalIgnoreCase); + + _writerScopes = new Stack(); } public HttpContext Context @@ -107,6 +111,57 @@ namespace Microsoft.AspNet.Mvc.Razor /// public abstract Task ExecuteAsync(); + /// + /// Starts a new writing scope. + /// + /// + /// All writes to the or after calling this method will + /// be buffered until is called. + /// + public void StartWritingScope() + { + // If there isn't a base writer take the ViewContext.Writer + if (_originalWriter == null) + { + _originalWriter = ViewContext.Writer; + } + + // We need to replace the ViewContext's Writer to ensure that all content (including content written + // from HTML helpers) is redirected. + ViewContext.Writer = new StringWriter(); + + _writerScopes.Push(ViewContext.Writer); + } + + /// + /// Ends the current writing scope that was started by calling . + /// + /// The that contains the content written to the or + /// during the writing scope. + public TextWriter EndWritingScope() + { + if (_writerScopes.Count == 0) + { + throw new InvalidOperationException(Resources.RazorPage_ThereIsNoActiveWritingScopeToEnd); + } + + var writer = _writerScopes.Pop(); + + if (_writerScopes.Count > 0) + { + ViewContext.Writer = _writerScopes.Peek(); + } + else + { + ViewContext.Writer = _originalWriter; + + // No longer a base writer + _originalWriter = null; + } + + return writer; + } + /// /// Writes the specified with HTML encoding to . /// @@ -395,6 +450,13 @@ namespace Microsoft.AspNet.Mvc.Razor /// A that represents the asynchronous flush operation. public Task FlushAsync() { + // If there are active writing scopes then we should throw. Cannot flush content that has the potential to + // change. + if (_writerScopes.Count > 0) + { + throw new InvalidOperationException(Resources.RazorPage_YouCannotFlushWhileInAWritingScope); + } + // Calls to Flush are allowed if the page does not specify a Layout or if it is executing a section in the // Layout. if (!IsLayoutBeingRendered && !string.IsNullOrEmpty(Layout)) diff --git a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx index e2cfc45b7b..68439f48c4 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx @@ -144,6 +144,12 @@ Only one '{0}' statement is allowed in a file. + + There is no active writing scope to end. + + + You cannot flush while inside a writing scope. + {0} can only be called from a layout page. diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs index accaa77f29..1b44b1098a 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorPageTest.cs @@ -16,6 +16,117 @@ namespace Microsoft.AspNet.Mvc.Razor { public class RazorPageTest { + [Fact] + public async Task WritingScopesRedirectContentWrittenToViewContextWriter() + { + // Arrange + var viewContext = CreateViewContext(); + var page = CreatePage(v => + { + v.Write("Hello Prefix"); + v.StartWritingScope(); + v.Write("Hello from Output"); + v.ViewContext.Writer.Write("Hello from view context writer"); + var scopeValue = v.EndWritingScope(); + v.Write("From Scope: " + scopeValue.ToString()); + }); + + // Act + await page.ExecuteAsync(); + var pageOutput = page.Output.ToString(); + + // Assert + Assert.Equal("Hello PrefixFrom Scope: Hello from OutputHello from view context writer", pageOutput); + } + + [Fact] + public async Task WritingScopesRedirectsContentWrittenToOutput() + { + // Arrange + var viewContext = CreateViewContext(); + var page = CreatePage(v => + { + v.Write("Hello Prefix"); + v.StartWritingScope(); + v.Write("Hello In Scope"); + var scopeValue = v.EndWritingScope(); + v.Write("From Scope: " + scopeValue.ToString()); + }); + + // Act + await page.ExecuteAsync(); + var pageOutput = page.Output.ToString(); + + // Assert + Assert.Equal("Hello PrefixFrom Scope: Hello In Scope", pageOutput); + } + + [Fact] + public async Task WritingScopesCanNest() + { + // Arrange + var viewContext = CreateViewContext(); + var page = CreatePage(v => + { + v.Write("Hello Prefix"); + v.StartWritingScope(); + v.Write("Hello In Scope Pre Nest"); + + v.StartWritingScope(); + v.Write("Hello In Nested Scope"); + var scopeValue1 = v.EndWritingScope(); + + v.Write("Hello In Scope Post Nest"); + var scopeValue2 = v.EndWritingScope(); + + v.Write("From Scopes: " + scopeValue2.ToString() + scopeValue1.ToString()); + }); + + // Act + await page.ExecuteAsync(); + var pageOutput = page.Output.ToString(); + + // Assert + Assert.Equal("Hello PrefixFrom Scopes: Hello In Scope Pre NestHello In Scope Post NestHello In Nested Scope", pageOutput); + } + + [Fact] + public async Task StartNewWritingScope_CannotFlushInWritingScope() + { + // Arrange + var viewContext = CreateViewContext(); + var page = CreatePage(v => + { + v.StartWritingScope(); + v.FlushAsync(); + }); + + // Act + var ex = await Assert.ThrowsAsync( + () => page.ExecuteAsync()); + + // Assert + Assert.Equal("You cannot flush while inside a writing scope.", ex.Message); + } + + [Fact] + public async Task StartNewWritingScope_CannotEndWritingScopeWhenNoWritingScope() + { + // Arrange + var viewContext = CreateViewContext(); + var page = CreatePage(v => + { + v.EndWritingScope(); + }); + + // Act + var ex = await Assert.ThrowsAsync( + () => page.ExecuteAsync()); + + // Assert + Assert.Equal("There is no active writing scope to end.", ex.Message); + } + [Fact] public async Task DefineSection_ThrowsIfSectionIsAlreadyDefined() {