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
This commit is contained in:
N. Taylor Mullen 2014-09-25 17:45:44 -07:00
parent e995e7a3e2
commit 082512f63c
5 changed files with 215 additions and 1 deletions

View File

@ -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",

View File

@ -154,6 +154,38 @@ namespace Microsoft.AspNet.Mvc.Razor
return string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_OnlyOneModelStatementIsAllowed"), p0);
}
/// <summary>
/// There is no active writing scope to end.
/// </summary>
internal static string RazorPage_ThereIsNoActiveWritingScopeToEnd
{
get { return GetString("RazorPage_ThereIsNoActiveWritingScopeToEnd"); }
}
/// <summary>
/// There is no active writing scope to end.
/// </summary>
internal static string FormatRazorPage_ThereIsNoActiveWritingScopeToEnd()
{
return GetString("RazorPage_ThereIsNoActiveWritingScopeToEnd");
}
/// <summary>
/// You cannot flush while inside a writing scope.
/// </summary>
internal static string RazorPage_YouCannotFlushWhileInAWritingScope
{
get { return GetString("RazorPage_YouCannotFlushWhileInAWritingScope"); }
}
/// <summary>
/// You cannot flush while inside a writing scope.
/// </summary>
internal static string FormatRazorPage_YouCannotFlushWhileInAWritingScope()
{
return GetString("RazorPage_YouCannotFlushWhileInAWritingScope");
}
/// <summary>
/// {0} can only be called from a layout page.
/// </summary>

View File

@ -21,12 +21,16 @@ namespace Microsoft.AspNet.Mvc.Razor
public abstract class RazorPage : IRazorPage
{
private readonly HashSet<string> _renderedSections = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private readonly Stack<TextWriter> _writerScopes;
private TextWriter _originalWriter;
private IUrlHelper _urlHelper;
private bool _renderedBody;
public RazorPage()
{
SectionWriters = new Dictionary<string, HelperResult>(StringComparer.OrdinalIgnoreCase);
_writerScopes = new Stack<TextWriter>();
}
public HttpContext Context
@ -107,6 +111,57 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <inheritdoc />
public abstract Task ExecuteAsync();
/// <summary>
/// Starts a new writing scope.
/// </summary>
/// <remarks>
/// All writes to the <see cref="Output"/> or <see cref="ViewContext.Writer"/> after calling this method will
/// be buffered until <see cref="EndWritingScope"/> is called.
/// </remarks>
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);
}
/// <summary>
/// Ends the current writing scope that was started by calling <see cref="StartWritingScope"/>.
/// </summary>
/// <returns>The <see cref="TextWriter"/> that contains the content written to the <see cref="Output"/> or
/// <see cref="ViewContext.Writer"/> during the writing scope.</returns>
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;
}
/// <summary>
/// Writes the specified <paramref name="value"/> with HTML encoding to <see cref="Output"/>.
/// </summary>
@ -395,6 +450,13 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <returns>A <see cref="Task"/> that represents the asynchronous flush operation.</returns>
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))

View File

@ -144,6 +144,12 @@
<data name="MvcRazorCodeParser_OnlyOneModelStatementIsAllowed" xml:space="preserve">
<value>Only one '{0}' statement is allowed in a file.</value>
</data>
<data name="RazorPage_ThereIsNoActiveWritingScopeToEnd" xml:space="preserve">
<value>There is no active writing scope to end.</value>
</data>
<data name="RazorPage_YouCannotFlushWhileInAWritingScope" xml:space="preserve">
<value>You cannot flush while inside a writing scope.</value>
</data>
<data name="RenderBodyCannotBeCalled" xml:space="preserve">
<value>{0} can only be called from a layout page.</value>
</data>

View File

@ -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<InvalidOperationException>(
() => 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<InvalidOperationException>(
() => page.ExecuteAsync());
// Assert
Assert.Equal("There is no active writing scope to end.", ex.Message);
}
[Fact]
public async Task DefineSection_ThrowsIfSectionIsAlreadyDefined()
{