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()
{