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:
parent
e995e7a3e2
commit
082512f63c
|
|
@ -76,6 +76,7 @@ namespace Microsoft.AspNet.Mvc.Razor
|
||||||
DefaultBaseClass = BaseType + '<' + DefaultModel + '>';
|
DefaultBaseClass = BaseType + '<' + DefaultModel + '>';
|
||||||
DefaultNamespace = "Asp";
|
DefaultNamespace = "Asp";
|
||||||
GeneratedClassContext = new GeneratedClassContext(
|
GeneratedClassContext = new GeneratedClassContext(
|
||||||
|
|
||||||
executeMethodName: "ExecuteAsync",
|
executeMethodName: "ExecuteAsync",
|
||||||
writeMethodName: "Write",
|
writeMethodName: "Write",
|
||||||
writeLiteralMethodName: "WriteLiteral",
|
writeLiteralMethodName: "WriteLiteral",
|
||||||
|
|
@ -87,7 +88,9 @@ namespace Microsoft.AspNet.Mvc.Razor
|
||||||
{
|
{
|
||||||
RunnerTypeName = typeof(TagHelperRunner).FullName,
|
RunnerTypeName = typeof(TagHelperRunner).FullName,
|
||||||
ScopeManagerTypeName = typeof(TagHelperScopeManager).FullName,
|
ScopeManagerTypeName = typeof(TagHelperScopeManager).FullName,
|
||||||
ExecutionContextTypeName = typeof(TagHelpersExecutionContext).FullName
|
ExecutionContextTypeName = typeof(TagHelpersExecutionContext).FullName,
|
||||||
|
StartWritingScopeMethodName = "StartWritingScope",
|
||||||
|
EndWritingScopeMethodName = "EndWritingScope"
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
ResolveUrlMethodName = "Href",
|
ResolveUrlMethodName = "Href",
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,38 @@ namespace Microsoft.AspNet.Mvc.Razor
|
||||||
return string.Format(CultureInfo.CurrentCulture, GetString("MvcRazorCodeParser_OnlyOneModelStatementIsAllowed"), p0);
|
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>
|
/// <summary>
|
||||||
/// {0} can only be called from a layout page.
|
/// {0} can only be called from a layout page.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,16 @@ namespace Microsoft.AspNet.Mvc.Razor
|
||||||
public abstract class RazorPage : IRazorPage
|
public abstract class RazorPage : IRazorPage
|
||||||
{
|
{
|
||||||
private readonly HashSet<string> _renderedSections = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
private readonly HashSet<string> _renderedSections = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly Stack<TextWriter> _writerScopes;
|
||||||
|
private TextWriter _originalWriter;
|
||||||
private IUrlHelper _urlHelper;
|
private IUrlHelper _urlHelper;
|
||||||
private bool _renderedBody;
|
private bool _renderedBody;
|
||||||
|
|
||||||
public RazorPage()
|
public RazorPage()
|
||||||
{
|
{
|
||||||
SectionWriters = new Dictionary<string, HelperResult>(StringComparer.OrdinalIgnoreCase);
|
SectionWriters = new Dictionary<string, HelperResult>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
_writerScopes = new Stack<TextWriter>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpContext Context
|
public HttpContext Context
|
||||||
|
|
@ -107,6 +111,57 @@ namespace Microsoft.AspNet.Mvc.Razor
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public abstract Task ExecuteAsync();
|
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>
|
/// <summary>
|
||||||
/// Writes the specified <paramref name="value"/> with HTML encoding to <see cref="Output"/>.
|
/// Writes the specified <paramref name="value"/> with HTML encoding to <see cref="Output"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -395,6 +450,13 @@ namespace Microsoft.AspNet.Mvc.Razor
|
||||||
/// <returns>A <see cref="Task"/> that represents the asynchronous flush operation.</returns>
|
/// <returns>A <see cref="Task"/> that represents the asynchronous flush operation.</returns>
|
||||||
public Task FlushAsync()
|
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
|
// Calls to Flush are allowed if the page does not specify a Layout or if it is executing a section in the
|
||||||
// Layout.
|
// Layout.
|
||||||
if (!IsLayoutBeingRendered && !string.IsNullOrEmpty(Layout))
|
if (!IsLayoutBeingRendered && !string.IsNullOrEmpty(Layout))
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,12 @@
|
||||||
<data name="MvcRazorCodeParser_OnlyOneModelStatementIsAllowed" xml:space="preserve">
|
<data name="MvcRazorCodeParser_OnlyOneModelStatementIsAllowed" xml:space="preserve">
|
||||||
<value>Only one '{0}' statement is allowed in a file.</value>
|
<value>Only one '{0}' statement is allowed in a file.</value>
|
||||||
</data>
|
</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">
|
<data name="RenderBodyCannotBeCalled" xml:space="preserve">
|
||||||
<value>{0} can only be called from a layout page.</value>
|
<value>{0} can only be called from a layout page.</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,117 @@ namespace Microsoft.AspNet.Mvc.Razor
|
||||||
{
|
{
|
||||||
public class RazorPageTest
|
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]
|
[Fact]
|
||||||
public async Task DefineSection_ThrowsIfSectionIsAlreadyDefined()
|
public async Task DefineSection_ThrowsIfSectionIsAlreadyDefined()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue