Implement MoveTo semantics in Mvc
- Simplify things that used to rely on HtmlTextWriter. This behavior is now the default. - Simplify a whole mess of Razor TextWriter code. - Integration test for merging of TagHelper buffers back into the 'main' buffer.
This commit is contained in:
parent
e2fd41e416
commit
0b7035ddcf
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"projects": ["src", "test/WebSites", "samples"]
|
"projects": ["src", "test/WebSites", "samples", "d:\\k\\HtmlAbstractions\\src", "d:\\k\\Razor\\src"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -100,15 +100,7 @@ namespace Microsoft.AspNetCore.Mvc.Localization
|
||||||
throw new ArgumentNullException(nameof(encoder));
|
throw new ArgumentNullException(nameof(encoder));
|
||||||
}
|
}
|
||||||
|
|
||||||
var htmlTextWriter = writer as HtmlTextWriter;
|
FormatValue(writer, encoder, Value, _arguments);
|
||||||
if (htmlTextWriter == null)
|
|
||||||
{
|
|
||||||
FormatValue(writer, encoder, Value, _arguments);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
htmlTextWriter.Write(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void FormatValue(
|
private static void FormatValue(
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
private bool _renderedBody;
|
private bool _renderedBody;
|
||||||
private AttributeInfo _attributeInfo;
|
private AttributeInfo _attributeInfo;
|
||||||
private TagHelperAttributeInfo _tagHelperAttributeInfo;
|
private TagHelperAttributeInfo _tagHelperAttributeInfo;
|
||||||
private HtmlContentWrapperTextWriter _valueBuffer;
|
private StringWriter _valueBuffer;
|
||||||
private IViewBufferScope _bufferScope;
|
private IViewBufferScope _bufferScope;
|
||||||
private bool _ignoreBody;
|
private bool _ignoreBody;
|
||||||
private HashSet<string> _ignoredSections;
|
private HashSet<string> _ignoredSections;
|
||||||
|
|
@ -214,7 +214,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public void StartTagHelperWritingScope(HtmlEncoder encoder)
|
public void StartTagHelperWritingScope(HtmlEncoder encoder)
|
||||||
{
|
{
|
||||||
_tagHelperScopes.Push(new TagHelperScopeInfo(HtmlEncoder, ViewContext.Writer));
|
var buffer = new ViewBuffer(BufferScope, Path, ViewBuffer.TagHelperPageSize);
|
||||||
|
_tagHelperScopes.Push(new TagHelperScopeInfo(buffer, HtmlEncoder, ViewContext.Writer));
|
||||||
|
|
||||||
// If passed an HtmlEncoder, override the property.
|
// If passed an HtmlEncoder, override the property.
|
||||||
if (encoder != null)
|
if (encoder != null)
|
||||||
|
|
@ -224,9 +225,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
|
|
||||||
// We need to replace the ViewContext's Writer to ensure that all content (including content written
|
// We need to replace the ViewContext's Writer to ensure that all content (including content written
|
||||||
// from HTML helpers) is redirected.
|
// from HTML helpers) is redirected.
|
||||||
var buffer = new ViewBuffer(BufferScope, Path);
|
ViewContext.Writer = new ViewBufferTextWriter(buffer, ViewContext.Writer.Encoding);
|
||||||
var writer = new HtmlContentWrapperTextWriter(buffer, ViewContext.Writer.Encoding);
|
|
||||||
ViewContext.Writer = writer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -240,13 +239,13 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
throw new InvalidOperationException(Resources.RazorPage_ThereIsNoActiveWritingScopeToEnd);
|
throw new InvalidOperationException(Resources.RazorPage_ThereIsNoActiveWritingScopeToEnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scopeInfo = _tagHelperScopes.Pop();
|
||||||
|
|
||||||
// Get the content written during the current scope.
|
// Get the content written during the current scope.
|
||||||
var writer = ViewContext.Writer as HtmlContentWrapperTextWriter;
|
|
||||||
var tagHelperContent = new DefaultTagHelperContent();
|
var tagHelperContent = new DefaultTagHelperContent();
|
||||||
tagHelperContent.AppendHtml(writer?.ContentBuilder);
|
tagHelperContent.AppendHtml(scopeInfo.Buffer);
|
||||||
|
|
||||||
// Restore previous scope.
|
// Restore previous scope.
|
||||||
var scopeInfo = _tagHelperScopes.Pop();
|
|
||||||
HtmlEncoder = scopeInfo.Encoder;
|
HtmlEncoder = scopeInfo.Encoder;
|
||||||
ViewContext.Writer = scopeInfo.Writer;
|
ViewContext.Writer = scopeInfo.Writer;
|
||||||
|
|
||||||
|
|
@ -317,16 +316,25 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
var htmlContent = value as IHtmlContent;
|
var htmlContent = value as IHtmlContent;
|
||||||
if (htmlContent != null)
|
if (htmlContent != null)
|
||||||
{
|
{
|
||||||
var htmlTextWriter = writer as HtmlTextWriter;
|
var bufferedWriter = writer as ViewBufferTextWriter;
|
||||||
if (htmlTextWriter == null)
|
if (bufferedWriter == null || !bufferedWriter.IsBuffering)
|
||||||
{
|
{
|
||||||
htmlContent.WriteTo(writer, encoder);
|
htmlContent.WriteTo(writer, encoder);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// This special case allows us to keep buffering as IHtmlContent until we get to the 'final'
|
var htmlContentContainer = value as IHtmlContentContainer;
|
||||||
// TextWriter.
|
if (htmlContentContainer != null)
|
||||||
htmlTextWriter.Write(htmlContent);
|
{
|
||||||
|
// This is likely another ViewBuffer.
|
||||||
|
htmlContentContainer.MoveTo(bufferedWriter.Buffer);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Perf: This is the common case for IHtmlContent, ViewBufferTextWriter is inefficient
|
||||||
|
// for writing character by character.
|
||||||
|
bufferedWriter.Buffer.AppendHtml(htmlContent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
@ -354,7 +362,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(value))
|
if (!string.IsNullOrEmpty(value))
|
||||||
{
|
{
|
||||||
encoder.Encode(writer, value);
|
// Perf: Encode right away instead of writing it character-by-character.
|
||||||
|
// character-by-character isn't efficient when using a writer backed by a ViewBuffer.
|
||||||
|
var encoded = encoder.Encode(value);
|
||||||
|
writer.Write(encoded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -529,7 +540,6 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
int attributeValuesCount)
|
int attributeValuesCount)
|
||||||
{
|
{
|
||||||
_tagHelperAttributeInfo = new TagHelperAttributeInfo(executionContext, attributeName, attributeValuesCount);
|
_tagHelperAttributeInfo = new TagHelperAttributeInfo(executionContext, attributeName, attributeValuesCount);
|
||||||
_valueBuffer = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddHtmlAttributeValue(
|
public void AddHtmlAttributeValue(
|
||||||
|
|
@ -567,10 +577,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
|
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
|
// Perf: We'll use this buffer for all of the attribute values and then clear it to
|
||||||
|
// reduce allocations.
|
||||||
if (_valueBuffer == null)
|
if (_valueBuffer == null)
|
||||||
{
|
{
|
||||||
var buffer = new ViewBuffer(BufferScope, Path);
|
_valueBuffer = new StringWriter();
|
||||||
_valueBuffer = new HtmlContentWrapperTextWriter(buffer, Output.Encoding);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(prefix))
|
if (!string.IsNullOrEmpty(prefix))
|
||||||
|
|
@ -586,10 +597,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
{
|
{
|
||||||
if (!_tagHelperAttributeInfo.Suppressed)
|
if (!_tagHelperAttributeInfo.Suppressed)
|
||||||
{
|
{
|
||||||
executionContext.AddHtmlAttribute(
|
// Perf: _valueBuffer might be null if nothing was written. If it is set, clear it so
|
||||||
_tagHelperAttributeInfo.Name,
|
// it is reset for the next value.
|
||||||
(IHtmlContent)_valueBuffer?.ContentBuilder ?? HtmlString.Empty);
|
var content = _valueBuffer == null ? HtmlString.Empty : new HtmlString(_valueBuffer.ToString());
|
||||||
_valueBuffer = null;
|
_valueBuffer?.GetStringBuilder().Clear();
|
||||||
|
|
||||||
|
executionContext.AddHtmlAttribute(_tagHelperAttributeInfo.Name, content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1053,12 +1066,15 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
|
|
||||||
private struct TagHelperScopeInfo
|
private struct TagHelperScopeInfo
|
||||||
{
|
{
|
||||||
public TagHelperScopeInfo(HtmlEncoder encoder, TextWriter writer)
|
public TagHelperScopeInfo(ViewBuffer buffer, HtmlEncoder encoder, TextWriter writer)
|
||||||
{
|
{
|
||||||
|
Buffer = buffer;
|
||||||
Encoder = encoder;
|
Encoder = encoder;
|
||||||
Writer = writer;
|
Writer = writer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ViewBuffer Buffer { get; }
|
||||||
|
|
||||||
public HtmlEncoder Encoder { get; }
|
public HtmlEncoder Encoder { get; }
|
||||||
|
|
||||||
public TextWriter Writer { get; }
|
public TextWriter Writer { get; }
|
||||||
|
|
|
||||||
|
|
@ -1,209 +0,0 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Encodings.Web;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Html;
|
|
||||||
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Razor
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// An <see cref="HtmlTextWriter"/> that is backed by a unbuffered writer (over the Response stream) and a buffered
|
|
||||||
/// <see cref="IHtmlContentBuilder"/>. When <c>Flush</c> or <c>FlushAsync</c> is invoked, the writer
|
|
||||||
/// copies all content from the buffered writer to the unbuffered one and switches to writing to the unbuffered
|
|
||||||
/// writer for all further write operations.
|
|
||||||
/// </summary>
|
|
||||||
public class RazorTextWriter : HtmlTextWriter
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a new instance of <see cref="RazorTextWriter"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="unbufferedWriter">The <see cref="TextWriter"/> to write output to when this instance
|
|
||||||
/// is no longer buffering.</param>
|
|
||||||
/// <param name="buffer">The <see cref="ViewBuffer"/> to buffer output to.</param>
|
|
||||||
/// <param name="encoder">The HTML encoder.</param>
|
|
||||||
public RazorTextWriter(TextWriter unbufferedWriter, ViewBuffer buffer, HtmlEncoder encoder)
|
|
||||||
{
|
|
||||||
UnbufferedWriter = unbufferedWriter;
|
|
||||||
Buffer = buffer;
|
|
||||||
HtmlEncoder = encoder;
|
|
||||||
|
|
||||||
BufferedWriter = new HtmlContentWrapperTextWriter(buffer, unbufferedWriter.Encoding);
|
|
||||||
TargetWriter = BufferedWriter;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override Encoding Encoding
|
|
||||||
{
|
|
||||||
get { return BufferedWriter.Encoding; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool IsBuffering { get; private set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the buffered content.
|
|
||||||
/// </summary>
|
|
||||||
public ViewBuffer Buffer { get; }
|
|
||||||
|
|
||||||
// Internal for unit testing
|
|
||||||
internal HtmlContentWrapperTextWriter BufferedWriter { get; }
|
|
||||||
|
|
||||||
private TextWriter UnbufferedWriter { get; }
|
|
||||||
|
|
||||||
private TextWriter TargetWriter { get; set; }
|
|
||||||
|
|
||||||
private HtmlEncoder HtmlEncoder { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(char value)
|
|
||||||
{
|
|
||||||
TargetWriter.Write(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(char[] buffer, int index, int count)
|
|
||||||
{
|
|
||||||
if (buffer == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(buffer));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index < 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(index));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count < 0 || (index + count > buffer.Length))
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(count));
|
|
||||||
}
|
|
||||||
|
|
||||||
TargetWriter.Write(buffer, index, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(string value)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(value))
|
|
||||||
{
|
|
||||||
TargetWriter.Write(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(IHtmlContent value)
|
|
||||||
{
|
|
||||||
// Perf: We don't special case 'TargetWriter is HtmlTextWriter' here, because want to delegate to the
|
|
||||||
// IHtmlContent if it wants to 'flatten' itself or not. This is an important optimization for TagHelpers.
|
|
||||||
value.WriteTo(TargetWriter, HtmlEncoder);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override Task WriteAsync(char value)
|
|
||||||
{
|
|
||||||
return TargetWriter.WriteAsync(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override Task WriteAsync(char[] buffer, int index, int count)
|
|
||||||
{
|
|
||||||
if (buffer == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(buffer));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index < 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(index));
|
|
||||||
}
|
|
||||||
if (count < 0 || (buffer.Length - index < count))
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(count));
|
|
||||||
}
|
|
||||||
|
|
||||||
return TargetWriter.WriteAsync(buffer, index, count);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override Task WriteAsync(string value)
|
|
||||||
{
|
|
||||||
return TargetWriter.WriteAsync(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void WriteLine()
|
|
||||||
{
|
|
||||||
TargetWriter.WriteLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void WriteLine(string value)
|
|
||||||
{
|
|
||||||
TargetWriter.WriteLine(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override Task WriteLineAsync(char value)
|
|
||||||
{
|
|
||||||
return TargetWriter.WriteLineAsync(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override Task WriteLineAsync(char[] value, int start, int offset)
|
|
||||||
{
|
|
||||||
return TargetWriter.WriteLineAsync(value, start, offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override Task WriteLineAsync(string value)
|
|
||||||
{
|
|
||||||
return TargetWriter.WriteLineAsync(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override Task WriteLineAsync()
|
|
||||||
{
|
|
||||||
return TargetWriter.WriteLineAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Copies the buffered content to the unbuffered writer and invokes flush on it.
|
|
||||||
/// Additionally causes this instance to no longer buffer and direct all write operations
|
|
||||||
/// to the unbuffered writer.
|
|
||||||
/// </summary>
|
|
||||||
public override void Flush()
|
|
||||||
{
|
|
||||||
if (IsBuffering)
|
|
||||||
{
|
|
||||||
IsBuffering = false;
|
|
||||||
TargetWriter = UnbufferedWriter;
|
|
||||||
Buffer.WriteTo(UnbufferedWriter, HtmlEncoder);
|
|
||||||
}
|
|
||||||
|
|
||||||
UnbufferedWriter.Flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Copies the buffered content to the unbuffered writer and invokes flush on it.
|
|
||||||
/// Additionally causes this instance to no longer buffer and direct all write operations
|
|
||||||
/// to the unbuffered writer.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A <see cref="Task"/> that represents the asynchronous copy and flush operations.</returns>
|
|
||||||
public override Task FlushAsync()
|
|
||||||
{
|
|
||||||
if (IsBuffering)
|
|
||||||
{
|
|
||||||
IsBuffering = false;
|
|
||||||
TargetWriter = UnbufferedWriter;
|
|
||||||
Buffer.WriteTo(UnbufferedWriter, HtmlEncoder);
|
|
||||||
}
|
|
||||||
|
|
||||||
return UnbufferedWriter.FlushAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
// 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;
|
||||||
using System.Buffers;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
@ -103,20 +102,35 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
await RenderLayoutAsync(context, bodyWriter);
|
await RenderLayoutAsync(context, bodyWriter);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<RazorTextWriter> RenderPageAsync(
|
private async Task<ViewBufferTextWriter> RenderPageAsync(
|
||||||
IRazorPage page,
|
IRazorPage page,
|
||||||
ViewContext context,
|
ViewContext context,
|
||||||
bool invokeViewStarts)
|
bool invokeViewStarts)
|
||||||
{
|
{
|
||||||
Debug.Assert(_bufferScope != null);
|
var writer = context.Writer as ViewBufferTextWriter;
|
||||||
var buffer = new ViewBuffer(_bufferScope, page.Path);
|
if (writer == null)
|
||||||
var razorTextWriter = new RazorTextWriter(context.Writer, buffer, _htmlEncoder);
|
{
|
||||||
|
Debug.Assert(_bufferScope != null);
|
||||||
|
|
||||||
|
// If we get here, this is likely the top-level page (not a partial) - this means
|
||||||
|
// that context.Writer is wrapping the output stream. We need to buffer, so create a buffered writer.
|
||||||
|
var buffer = new ViewBuffer(_bufferScope, page.Path, ViewBuffer.ViewPageSize);
|
||||||
|
writer = new ViewBufferTextWriter(buffer, context.Writer.Encoding, _htmlEncoder, context.Writer);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// This means we're writing something like a partial, where the output needs to be buffered.
|
||||||
|
// Create a new buffer, but without the ability to flush.
|
||||||
|
var buffer = new ViewBuffer(_bufferScope, page.Path, ViewBuffer.ViewPageSize);
|
||||||
|
writer = new ViewBufferTextWriter(buffer, context.Writer.Encoding);
|
||||||
|
}
|
||||||
|
|
||||||
// The writer for the body is passed through the ViewContext, allowing things like HtmlHelpers
|
// The writer for the body is passed through the ViewContext, allowing things like HtmlHelpers
|
||||||
// and ViewComponents to reference it.
|
// and ViewComponents to reference it.
|
||||||
var oldWriter = context.Writer;
|
var oldWriter = context.Writer;
|
||||||
var oldFilePath = context.ExecutingFilePath;
|
var oldFilePath = context.ExecutingFilePath;
|
||||||
context.Writer = razorTextWriter;
|
|
||||||
|
context.Writer = writer;
|
||||||
context.ExecutingFilePath = page.Path;
|
context.ExecutingFilePath = page.Path;
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
@ -128,13 +142,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
}
|
}
|
||||||
|
|
||||||
await RenderPageCoreAsync(page, context);
|
await RenderPageCoreAsync(page, context);
|
||||||
return razorTextWriter;
|
return writer;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
context.Writer = oldWriter;
|
context.Writer = oldWriter;
|
||||||
context.ExecutingFilePath = oldFilePath;
|
context.ExecutingFilePath = oldFilePath;
|
||||||
razorTextWriter.Dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,12 +197,16 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
|
|
||||||
private async Task RenderLayoutAsync(
|
private async Task RenderLayoutAsync(
|
||||||
ViewContext context,
|
ViewContext context,
|
||||||
RazorTextWriter bodyWriter)
|
ViewBufferTextWriter bodyWriter)
|
||||||
{
|
{
|
||||||
// A layout page can specify another layout page. We'll need to continue
|
// A layout page can specify another layout page. We'll need to continue
|
||||||
// looking for layout pages until they're no longer specified.
|
// looking for layout pages until they're no longer specified.
|
||||||
var previousPage = RazorPage;
|
var previousPage = RazorPage;
|
||||||
var renderedLayouts = new List<IRazorPage>();
|
var renderedLayouts = new List<IRazorPage>();
|
||||||
|
|
||||||
|
// This loop will execute Layout pages from the inside to the outside. With each
|
||||||
|
// iteration, bodyWriter is replaced with the aggregate of all the "body" content
|
||||||
|
// (including the layout page we just rendered).
|
||||||
while (!string.IsNullOrEmpty(previousPage.Layout))
|
while (!string.IsNullOrEmpty(previousPage.Layout))
|
||||||
{
|
{
|
||||||
if (!bodyWriter.IsBuffering)
|
if (!bodyWriter.IsBuffering)
|
||||||
|
|
@ -225,6 +242,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
previousPage = layoutPage;
|
previousPage = layoutPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Now we've reached and rendered the outer-most layout page. Nothing left to execute.
|
||||||
|
|
||||||
// Ensure all defined sections were rendered or RenderBody was invoked for page without defined sections.
|
// Ensure all defined sections were rendered or RenderBody was invoked for page without defined sections.
|
||||||
foreach (var layoutPage in renderedLayouts)
|
foreach (var layoutPage in renderedLayouts)
|
||||||
{
|
{
|
||||||
|
|
@ -233,10 +252,24 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
|
|
||||||
if (bodyWriter.IsBuffering)
|
if (bodyWriter.IsBuffering)
|
||||||
{
|
{
|
||||||
// Only copy buffered content to the Output if we're currently buffering.
|
// If IsBuffering - then we've got a bunch of content in the view buffer. How to best deal with it
|
||||||
using (var writer = _bufferScope.CreateWriter(context.Writer))
|
// really depends on whether or not we're writing directly to the output or if we're writing to
|
||||||
|
// another buffer.
|
||||||
|
var viewBufferTextWriter = context.Writer as ViewBufferTextWriter;
|
||||||
|
if (viewBufferTextWriter == null || !viewBufferTextWriter.IsBuffering)
|
||||||
{
|
{
|
||||||
await bodyWriter.Buffer.WriteToAsync(writer, _htmlEncoder);
|
// This means we're writing to a 'real' writer, probably to the actual output stream.
|
||||||
|
// We're using PagedBufferedTextWriter here to 'smooth' synchronous writes of IHtmlContent values.
|
||||||
|
using (var writer = _bufferScope.CreateWriter(context.Writer))
|
||||||
|
{
|
||||||
|
await bodyWriter.Buffer.WriteToAsync(writer, _htmlEncoder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// This means we're writing to another buffer. Use MoveTo to combine them.
|
||||||
|
bodyWriter.Buffer.MoveTo(viewBufferTextWriter.Buffer);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -354,16 +354,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.TagHelpers
|
||||||
|
|
||||||
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
|
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
|
||||||
{
|
{
|
||||||
var htmlTextWriter = writer as HtmlTextWriter;
|
encoder.Encode(writer, _firstSegment, 0, _firstSegmentLength);
|
||||||
if (htmlTextWriter != null)
|
writer.Write(_secondSegment);
|
||||||
{
|
|
||||||
htmlTextWriter.Write(this);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
encoder.Encode(writer, _firstSegment, 0, _firstSegmentLength);
|
|
||||||
writer.Write(_secondSegment);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -400,13 +400,6 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
|
||||||
|
|
||||||
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
|
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
|
||||||
{
|
{
|
||||||
var htmlTextWriter = writer as HtmlTextWriter;
|
|
||||||
if (htmlTextWriter != null)
|
|
||||||
{
|
|
||||||
htmlTextWriter.Write(this);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < _builder.Length; i++)
|
for (var i = 0; i < _builder.Length; i++)
|
||||||
{
|
{
|
||||||
writer.Write(_builder[i]);
|
writer.Write(_builder[i]);
|
||||||
|
|
|
||||||
|
|
@ -1,176 +0,0 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Html;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Internal;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// <see cref="HtmlTextWriter"/> implementation which writes to an <see cref="IHtmlContentBuilder"/> instance.
|
|
||||||
/// </summary>
|
|
||||||
public class HtmlContentWrapperTextWriter : HtmlTextWriter
|
|
||||||
{
|
|
||||||
private const int MaxCharToStringLength = 1024;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="HtmlContentWrapperTextWriter"/> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="contentBuilder">The <see cref="IHtmlContentBuilder"/> to write to.</param>
|
|
||||||
/// <param name="encoding">The <see cref="System.Text.Encoding"/> in which output is written.</param>
|
|
||||||
public HtmlContentWrapperTextWriter(IHtmlContentBuilder contentBuilder, Encoding encoding)
|
|
||||||
{
|
|
||||||
if (contentBuilder == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(contentBuilder));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (encoding == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(encoding));
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentBuilder = contentBuilder;
|
|
||||||
Encoding = encoding;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The <see cref="IHtmlContentBuilder"/> this <see cref="HtmlContentWrapperTextWriter"/> writes to.
|
|
||||||
/// </summary>
|
|
||||||
public IHtmlContentBuilder ContentBuilder { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override Encoding Encoding { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(char value)
|
|
||||||
{
|
|
||||||
Write(value.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(char[] buffer, int index, int count)
|
|
||||||
{
|
|
||||||
if (buffer == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(buffer));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index < 0)
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(index));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count < 0 || (index + count > buffer.Length))
|
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(count));
|
|
||||||
}
|
|
||||||
|
|
||||||
while (count > 0)
|
|
||||||
{
|
|
||||||
// Split large char arrays into 1KB strings.
|
|
||||||
var currentCount = count;
|
|
||||||
if (MaxCharToStringLength < currentCount)
|
|
||||||
{
|
|
||||||
currentCount = MaxCharToStringLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
Write(new string(buffer, index, currentCount));
|
|
||||||
index += currentCount;
|
|
||||||
count -= currentCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(string value)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(value))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentBuilder.Append(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(IHtmlContent value)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentBuilder.AppendHtml(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override Task WriteAsync(char value)
|
|
||||||
{
|
|
||||||
Write(value.ToString());
|
|
||||||
return TaskCache.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override Task WriteAsync(char[] buffer, int index, int count)
|
|
||||||
{
|
|
||||||
if (buffer == null)
|
|
||||||
{
|
|
||||||
throw new ArgumentNullException(nameof(buffer));
|
|
||||||
}
|
|
||||||
|
|
||||||
Write(buffer, index, count);
|
|
||||||
return TaskCache.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override Task WriteAsync(string value)
|
|
||||||
{
|
|
||||||
Write(value);
|
|
||||||
return TaskCache.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void WriteLine()
|
|
||||||
{
|
|
||||||
Write(Environment.NewLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void WriteLine(string value)
|
|
||||||
{
|
|
||||||
Write(value);
|
|
||||||
WriteLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override Task WriteLineAsync(char value)
|
|
||||||
{
|
|
||||||
WriteLine(value);
|
|
||||||
return TaskCache.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override Task WriteLineAsync(char[] value, int start, int offset)
|
|
||||||
{
|
|
||||||
WriteLine(value, start, offset);
|
|
||||||
return TaskCache.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override Task WriteLineAsync(string value)
|
|
||||||
{
|
|
||||||
WriteLine(value);
|
|
||||||
return TaskCache.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override Task WriteLineAsync()
|
|
||||||
{
|
|
||||||
WriteLine();
|
|
||||||
return TaskCache.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -13,8 +13,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a <see cref="T:ViewBufferValue[]"/>.
|
/// Gets a <see cref="T:ViewBufferValue[]"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="pageSize">The minimum size of the segment.</param>
|
||||||
/// <returns>The <see cref="T:ViewBufferValue[]"/>.</returns>
|
/// <returns>The <see cref="T:ViewBufferValue[]"/>.</returns>
|
||||||
ViewBufferValue[] GetSegment();
|
ViewBufferValue[] GetPage(int pageSize);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a <see cref="T:ViewBufferValue[]"/> that can be reused.
|
/// Returns a <see cref="T:ViewBufferValue[]"/> that can be reused.
|
||||||
|
|
@ -23,11 +24,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
void ReturnSegment(ViewBufferValue[] segment);
|
void ReturnSegment(ViewBufferValue[] segment);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a <see cref="ViewBufferTextWriter"/> that will delegate to the provided
|
/// Creates a <see cref="PagedBufferedTextWriter"/> that will delegate to the provided
|
||||||
/// <paramref name="writer"/>.
|
/// <paramref name="writer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="writer">The <see cref="TextWriter"/>.</param>
|
/// <param name="writer">The <see cref="TextWriter"/>.</param>
|
||||||
/// <returns>A <see cref="ViewBufferTextWriter"/>.</returns>
|
/// <returns>A <see cref="PagedBufferedTextWriter"/>.</returns>
|
||||||
ViewBufferTextWriter CreateWriter(TextWriter writer);
|
PagedBufferedTextWriter CreateWriter(TextWriter writer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MemoryPoolViewBufferScope : IViewBufferScope, IDisposable
|
public class MemoryPoolViewBufferScope : IViewBufferScope, IDisposable
|
||||||
{
|
{
|
||||||
public static readonly int SegmentSize = 512;
|
public static readonly int MinimumSize = 16;
|
||||||
private readonly ArrayPool<ViewBufferValue> _viewBufferPool;
|
private readonly ArrayPool<ViewBufferValue> _viewBufferPool;
|
||||||
private readonly ArrayPool<char> _charPool;
|
private readonly ArrayPool<char> _charPool;
|
||||||
private List<ViewBufferValue[]> _available;
|
private List<ViewBufferValue[]> _available;
|
||||||
|
|
@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
/// The <see cref="ArrayPool{ViewBufferValue}"/> for creating <see cref="ViewBufferValue"/> instances.
|
/// The <see cref="ArrayPool{ViewBufferValue}"/> for creating <see cref="ViewBufferValue"/> instances.
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="charPool">
|
/// <param name="charPool">
|
||||||
/// The <see cref="ArrayPool{Char}"/> for creating <see cref="ViewBufferTextWriter"/> instances.
|
/// The <see cref="ArrayPool{Char}"/> for creating <see cref="PagedBufferedTextWriter"/> instances.
|
||||||
/// </param>
|
/// </param>
|
||||||
public MemoryPoolViewBufferScope(ArrayPool<ViewBufferValue> viewBufferPool, ArrayPool<char> charPool)
|
public MemoryPoolViewBufferScope(ArrayPool<ViewBufferValue> viewBufferPool, ArrayPool<char> charPool)
|
||||||
{
|
{
|
||||||
|
|
@ -36,8 +36,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ViewBufferValue[] GetSegment()
|
public ViewBufferValue[] GetPage(int pageSize)
|
||||||
{
|
{
|
||||||
|
if (pageSize <= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
{
|
{
|
||||||
throw new ObjectDisposedException(typeof(MemoryPoolViewBufferScope).FullName);
|
throw new ObjectDisposedException(typeof(MemoryPoolViewBufferScope).FullName);
|
||||||
|
|
@ -60,7 +65,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
segment = _viewBufferPool.Rent(SegmentSize);
|
segment = _viewBufferPool.Rent(Math.Max(pageSize, MinimumSize));
|
||||||
_leased.Add(segment);
|
_leased.Add(segment);
|
||||||
}
|
}
|
||||||
catch when (segment != null)
|
catch when (segment != null)
|
||||||
|
|
@ -91,14 +96,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ViewBufferTextWriter CreateWriter(TextWriter writer)
|
public PagedBufferedTextWriter CreateWriter(TextWriter writer)
|
||||||
{
|
{
|
||||||
if (writer == null)
|
if (writer == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(writer));
|
throw new ArgumentNullException(nameof(writer));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ViewBufferTextWriter(_charPool, writer);
|
return new PagedBufferedTextWriter(_charPool, writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
|
{
|
||||||
|
public class PagedBufferedTextWriter : TextWriter
|
||||||
|
{
|
||||||
|
public const int PageSize = 1024;
|
||||||
|
|
||||||
|
private readonly TextWriter _inner;
|
||||||
|
private readonly List<char[]> _pages;
|
||||||
|
private readonly ArrayPool<char> _pool;
|
||||||
|
|
||||||
|
private int _currentPage;
|
||||||
|
private int _currentIndex; // The next 'free' character
|
||||||
|
|
||||||
|
public PagedBufferedTextWriter(ArrayPool<char> pool, TextWriter inner)
|
||||||
|
{
|
||||||
|
_pool = pool;
|
||||||
|
_inner = inner;
|
||||||
|
_pages = new List<char[]>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Encoding Encoding => _inner.Encoding;
|
||||||
|
|
||||||
|
public override void Flush()
|
||||||
|
{
|
||||||
|
// Don't do anything. We'll call FlushAsync.
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task FlushAsync()
|
||||||
|
{
|
||||||
|
if (_pages.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i <= _currentPage; i++)
|
||||||
|
{
|
||||||
|
var page = _pages[i];
|
||||||
|
|
||||||
|
var count = i == _currentPage ? _currentIndex : page.Length;
|
||||||
|
if (count > 0)
|
||||||
|
{
|
||||||
|
await _inner.WriteAsync(page, 0, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return all but one of the pages. This way if someone writes a large chunk of
|
||||||
|
// content, we can return those buffers and avoid holding them for the whole
|
||||||
|
// page's lifetime.
|
||||||
|
for (var i = _pages.Count - 1; i > 0; i--)
|
||||||
|
{
|
||||||
|
var page = _pages[i];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_pages.RemoveAt(i);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_pool.Return(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentPage = 0;
|
||||||
|
_currentIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(char value)
|
||||||
|
{
|
||||||
|
var page = GetCurrentPage();
|
||||||
|
page[_currentIndex++] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(char[] buffer)
|
||||||
|
{
|
||||||
|
Write(buffer, 0, buffer.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(char[] buffer, int index, int count)
|
||||||
|
{
|
||||||
|
while (count > 0)
|
||||||
|
{
|
||||||
|
var page = GetCurrentPage();
|
||||||
|
var copyLength = Math.Min(count, page.Length - _currentIndex);
|
||||||
|
Debug.Assert(copyLength > 0);
|
||||||
|
|
||||||
|
Array.Copy(
|
||||||
|
buffer,
|
||||||
|
index,
|
||||||
|
page,
|
||||||
|
_currentIndex,
|
||||||
|
copyLength);
|
||||||
|
|
||||||
|
_currentIndex += copyLength;
|
||||||
|
index += copyLength;
|
||||||
|
|
||||||
|
count -= copyLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(string value)
|
||||||
|
{
|
||||||
|
var index = 0;
|
||||||
|
var count = value.Length;
|
||||||
|
|
||||||
|
while (count > 0)
|
||||||
|
{
|
||||||
|
var page = GetCurrentPage();
|
||||||
|
var copyLength = Math.Min(count, page.Length - _currentIndex);
|
||||||
|
Debug.Assert(copyLength > 0);
|
||||||
|
|
||||||
|
value.CopyTo(
|
||||||
|
index,
|
||||||
|
page,
|
||||||
|
_currentIndex,
|
||||||
|
copyLength);
|
||||||
|
|
||||||
|
_currentIndex += copyLength;
|
||||||
|
index += copyLength;
|
||||||
|
|
||||||
|
count -= copyLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task WriteAsync(char value)
|
||||||
|
{
|
||||||
|
return _inner.WriteAsync(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task WriteAsync(char[] buffer, int index, int count)
|
||||||
|
{
|
||||||
|
return _inner.WriteAsync(buffer, index, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Task WriteAsync(string value)
|
||||||
|
{
|
||||||
|
return _inner.WriteAsync(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private char[] GetCurrentPage()
|
||||||
|
{
|
||||||
|
char[] page = null;
|
||||||
|
if (_pages.Count == 0)
|
||||||
|
{
|
||||||
|
Debug.Assert(_currentPage == 0);
|
||||||
|
Debug.Assert(_currentIndex == 0);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
page = _pool.Rent(PageSize);
|
||||||
|
_pages.Add(page);
|
||||||
|
}
|
||||||
|
catch when (page != null)
|
||||||
|
{
|
||||||
|
_pool.Return(page);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Assert(_pages.Count > _currentPage);
|
||||||
|
page = _pages[_currentPage];
|
||||||
|
|
||||||
|
if (_currentIndex == page.Length)
|
||||||
|
{
|
||||||
|
// Current page is full.
|
||||||
|
_currentPage++;
|
||||||
|
_currentIndex = 0;
|
||||||
|
|
||||||
|
if (_pages.Count == _currentPage)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
page = _pool.Rent(PageSize);
|
||||||
|
_pages.Add(page);
|
||||||
|
}
|
||||||
|
catch when (page != null)
|
||||||
|
{
|
||||||
|
_pool.Return(page);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
|
||||||
|
for (var i = 0; i < _pages.Count; i++)
|
||||||
|
{
|
||||||
|
_pool.Return(_pages[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
_pages.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,15 +2,12 @@
|
||||||
// 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;
|
||||||
using System.Buffers;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Html;
|
using Microsoft.AspNetCore.Html;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
{
|
{
|
||||||
|
|
@ -20,23 +17,36 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
[DebuggerDisplay("{DebuggerToString()}")]
|
[DebuggerDisplay("{DebuggerToString()}")]
|
||||||
public class ViewBuffer : IHtmlContentBuilder
|
public class ViewBuffer : IHtmlContentBuilder
|
||||||
{
|
{
|
||||||
|
public static readonly int PartialViewPageSize = 32;
|
||||||
|
public static readonly int TagHelperPageSize = 32;
|
||||||
|
public static readonly int ViewComponentPageSize = 32;
|
||||||
|
public static readonly int ViewPageSize = 256;
|
||||||
|
|
||||||
private readonly IViewBufferScope _bufferScope;
|
private readonly IViewBufferScope _bufferScope;
|
||||||
private readonly string _name;
|
private readonly string _name;
|
||||||
|
private readonly int _pageSize;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of <see cref="ViewBuffer"/>.
|
/// Initializes a new instance of <see cref="ViewBuffer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="bufferScope">The <see cref="IViewBufferScope"/>.</param>
|
/// <param name="bufferScope">The <see cref="IViewBufferScope"/>.</param>
|
||||||
/// <param name="name">A name to identify this instance.</param>
|
/// <param name="name">A name to identify this instance.</param>
|
||||||
public ViewBuffer(IViewBufferScope bufferScope, string name)
|
/// <param name="pageSize">The size of buffer pages.</param>
|
||||||
|
public ViewBuffer(IViewBufferScope bufferScope, string name, int pageSize)
|
||||||
{
|
{
|
||||||
if (bufferScope == null)
|
if (bufferScope == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(bufferScope));
|
throw new ArgumentNullException(nameof(bufferScope));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pageSize <= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
_bufferScope = bufferScope;
|
_bufferScope = bufferScope;
|
||||||
_name = name;
|
_name = name;
|
||||||
|
_pageSize = pageSize;
|
||||||
|
|
||||||
Pages = new List<ViewBufferPage>();
|
Pages = new List<ViewBufferPage>();
|
||||||
}
|
}
|
||||||
|
|
@ -54,7 +64,9 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
AppendValue(new ViewBufferValue(unencoded));
|
// Text that needs encoding is the uncommon case in views, which is why it
|
||||||
|
// creates a wrapper and pre-encoded text does not.
|
||||||
|
AppendValue(new ViewBufferValue(new EncodingWrapper(unencoded)));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,48 +78,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perf: special case ViewBuffers so we can 'combine' them.
|
AppendValue(new ViewBufferValue(content));
|
||||||
var otherBuffer = content as ViewBuffer;
|
|
||||||
if (otherBuffer == null)
|
|
||||||
{
|
|
||||||
AppendValue(new ViewBufferValue(content));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < otherBuffer.Pages.Count; i++)
|
|
||||||
{
|
|
||||||
var otherPage = otherBuffer.Pages[i];
|
|
||||||
var currentPage = Pages.Count == 0 ? null : Pages[Pages.Count - 1];
|
|
||||||
|
|
||||||
// If the other page is less or equal to than half full, let's copy it's to the current page if
|
|
||||||
// possible.
|
|
||||||
var isLessThanHalfFull = 2 * otherPage.Count <= otherPage.Capacity;
|
|
||||||
if (isLessThanHalfFull &&
|
|
||||||
currentPage != null &&
|
|
||||||
currentPage.Capacity - currentPage.Count >= otherPage.Count)
|
|
||||||
{
|
|
||||||
// We have room, let's copy the items.
|
|
||||||
Array.Copy(
|
|
||||||
sourceArray: otherPage.Buffer,
|
|
||||||
sourceIndex: 0,
|
|
||||||
destinationArray: currentPage.Buffer,
|
|
||||||
destinationIndex: currentPage.Count,
|
|
||||||
length: otherPage.Count);
|
|
||||||
|
|
||||||
currentPage.Count += otherPage.Count;
|
|
||||||
|
|
||||||
// Now we can return this page, and it can be reused in the scope of this request.
|
|
||||||
_bufferScope.ReturnSegment(otherPage.Buffer);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Otherwise, let's just take the the page from the other buffer.
|
|
||||||
Pages.Add(otherPage);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
otherBuffer.Clear();
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -119,8 +90,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
var value = new HtmlString(encoded);
|
AppendValue(new ViewBufferValue(encoded));
|
||||||
AppendValue(new ViewBufferValue(value));
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,7 +105,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
ViewBufferPage page;
|
ViewBufferPage page;
|
||||||
if (Pages.Count == 0)
|
if (Pages.Count == 0)
|
||||||
{
|
{
|
||||||
page = new ViewBufferPage(_bufferScope.GetSegment());
|
page = new ViewBufferPage(_bufferScope.GetPage(_pageSize));
|
||||||
Pages.Add(page);
|
Pages.Add(page);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
@ -143,7 +113,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
page = Pages[Pages.Count - 1];
|
page = Pages[Pages.Count - 1];
|
||||||
if (page.IsFull)
|
if (page.IsFull)
|
||||||
{
|
{
|
||||||
page = new ViewBufferPage(_bufferScope.GetSegment());
|
page = new ViewBufferPage(_bufferScope.GetPage(_pageSize));
|
||||||
Pages.Add(page);
|
Pages.Add(page);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -165,15 +135,18 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
|
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
|
||||||
{
|
{
|
||||||
if (Pages == null)
|
if (writer == null)
|
||||||
{
|
{
|
||||||
return;
|
throw new ArgumentNullException(nameof(writer));
|
||||||
}
|
}
|
||||||
|
|
||||||
var htmlTextWriter = writer as HtmlTextWriter;
|
if (encoder == null)
|
||||||
if (htmlTextWriter != null)
|
{
|
||||||
|
throw new ArgumentNullException(nameof(encoder));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Pages == null)
|
||||||
{
|
{
|
||||||
htmlTextWriter.Write(this);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,15 +182,18 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
/// <returns>A <see cref="Task"/> which will complete once content has been written.</returns>
|
/// <returns>A <see cref="Task"/> which will complete once content has been written.</returns>
|
||||||
public async Task WriteToAsync(TextWriter writer, HtmlEncoder encoder)
|
public async Task WriteToAsync(TextWriter writer, HtmlEncoder encoder)
|
||||||
{
|
{
|
||||||
if (Pages == null)
|
if (writer == null)
|
||||||
{
|
{
|
||||||
return;
|
throw new ArgumentNullException(nameof(writer));
|
||||||
}
|
}
|
||||||
|
|
||||||
var htmlTextWriter = writer as HtmlTextWriter;
|
if (encoder == null)
|
||||||
if (htmlTextWriter != null)
|
{
|
||||||
|
throw new ArgumentNullException(nameof(encoder));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Pages == null)
|
||||||
{
|
{
|
||||||
htmlTextWriter.Write(this);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -254,5 +230,153 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
private string DebuggerToString() => _name;
|
private string DebuggerToString() => _name;
|
||||||
|
|
||||||
|
public void CopyTo(IHtmlContentBuilder destination)
|
||||||
|
{
|
||||||
|
if (destination == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(destination));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Pages == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < Pages.Count; i++)
|
||||||
|
{
|
||||||
|
var page = Pages[i];
|
||||||
|
for (var j = 0; j < page.Count; j++)
|
||||||
|
{
|
||||||
|
var value = page.Buffer[j];
|
||||||
|
|
||||||
|
string valueAsString;
|
||||||
|
IHtmlContentContainer valueAsContainer;
|
||||||
|
if ((valueAsString = value.Value as string) != null)
|
||||||
|
{
|
||||||
|
destination.AppendHtml(valueAsString);
|
||||||
|
}
|
||||||
|
else if ((valueAsContainer = value.Value as IHtmlContentContainer) != null)
|
||||||
|
{
|
||||||
|
valueAsContainer.CopyTo(destination);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
destination.AppendHtml((IHtmlContent)value.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void MoveTo(IHtmlContentBuilder destination)
|
||||||
|
{
|
||||||
|
if (destination == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(destination));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Pages == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perf: We have an efficient implementation when the destination is another view buffer,
|
||||||
|
// we can just insert our pages as-is.
|
||||||
|
var other = destination as ViewBuffer;
|
||||||
|
if (other != null)
|
||||||
|
{
|
||||||
|
MoveTo(other);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < Pages.Count; i++)
|
||||||
|
{
|
||||||
|
var page = Pages[i];
|
||||||
|
for (var j = 0; j < page.Count; j++)
|
||||||
|
{
|
||||||
|
var value = page.Buffer[j];
|
||||||
|
|
||||||
|
string valueAsString;
|
||||||
|
IHtmlContentContainer valueAsContainer;
|
||||||
|
if ((valueAsString = value.Value as string) != null)
|
||||||
|
{
|
||||||
|
destination.AppendHtml(valueAsString);
|
||||||
|
}
|
||||||
|
else if ((valueAsContainer = value.Value as IHtmlContentContainer) != null)
|
||||||
|
{
|
||||||
|
valueAsContainer.MoveTo(destination);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
destination.AppendHtml((IHtmlContent)value.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < Pages.Count; i++)
|
||||||
|
{
|
||||||
|
var page = Pages[i];
|
||||||
|
Array.Clear(page.Buffer, 0, page.Count);
|
||||||
|
_bufferScope.ReturnSegment(page.Buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
Pages.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MoveTo(ViewBuffer destination)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < Pages.Count; i++)
|
||||||
|
{
|
||||||
|
var page = Pages[i];
|
||||||
|
|
||||||
|
var destinationPage = destination.Pages.Count == 0 ? null : destination.Pages[destination.Pages.Count - 1];
|
||||||
|
|
||||||
|
// If the source page is less or equal to than half full, let's copy it's content to the destination
|
||||||
|
// page if possible.
|
||||||
|
var isLessThanHalfFull = 2 * page.Count <= page.Capacity;
|
||||||
|
if (isLessThanHalfFull &&
|
||||||
|
destinationPage != null &&
|
||||||
|
destinationPage.Capacity - destinationPage.Count >= page.Count)
|
||||||
|
{
|
||||||
|
// We have room, let's copy the items.
|
||||||
|
Array.Copy(
|
||||||
|
sourceArray: page.Buffer,
|
||||||
|
sourceIndex: 0,
|
||||||
|
destinationArray: destinationPage.Buffer,
|
||||||
|
destinationIndex: destinationPage.Count,
|
||||||
|
length: page.Count);
|
||||||
|
|
||||||
|
destinationPage.Count += page.Count;
|
||||||
|
|
||||||
|
// Now we can return the source page, and it can be reused in the scope of this request.
|
||||||
|
Array.Clear(page.Buffer, 0, page.Count);
|
||||||
|
_bufferScope.ReturnSegment(page.Buffer);
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Otherwise, let's just add the source page to the other buffer.
|
||||||
|
destination.Pages.Add(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Pages.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class EncodingWrapper : IHtmlContent
|
||||||
|
{
|
||||||
|
private readonly string _unencoded;
|
||||||
|
|
||||||
|
public EncodingWrapper(string unencoded)
|
||||||
|
{
|
||||||
|
_unencoded = unencoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
|
||||||
|
{
|
||||||
|
encoder.Encode(writer, _unencoded);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,211 +1,437 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. 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;
|
||||||
using System.Buffers;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Html;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <para>
|
||||||
|
/// A <see cref="TextWriter"/> that is backed by a unbuffered writer (over the Response stream) and/or a
|
||||||
|
/// <see cref="ViewBuffer"/>
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// When <c>Flush</c> or <c>FlushAsync</c> is invoked, the writer copies all content from the buffer to
|
||||||
|
/// the writer and switches to writing to the unbuffered writer for all further write operations.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
public class ViewBufferTextWriter : TextWriter
|
public class ViewBufferTextWriter : TextWriter
|
||||||
{
|
{
|
||||||
public const int PageSize = 1024;
|
|
||||||
|
|
||||||
private readonly TextWriter _inner;
|
private readonly TextWriter _inner;
|
||||||
private readonly List<char[]> _pages;
|
private readonly HtmlEncoder _htmlEncoder;
|
||||||
private readonly ArrayPool<char> _pool;
|
|
||||||
|
|
||||||
private int _currentPage;
|
/// <summary>
|
||||||
private int _currentIndex; // The next 'free' character
|
/// Creates a new instance of <see cref="ViewBufferTextWriter"/>.
|
||||||
|
/// </summary>
|
||||||
public ViewBufferTextWriter(ArrayPool<char> pool, TextWriter inner)
|
/// <param name="buffer">The <see cref="ViewBuffer"/> for buffered output.</param>
|
||||||
|
/// <param name="encoding">The <see cref="System.Text.Encoding"/>.</param>
|
||||||
|
public ViewBufferTextWriter(ViewBuffer buffer, Encoding encoding)
|
||||||
{
|
{
|
||||||
_pool = pool;
|
if (buffer == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoding == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(encoding));
|
||||||
|
}
|
||||||
|
|
||||||
|
Buffer = buffer;
|
||||||
|
Encoding = encoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new instance of <see cref="ViewBufferTextWriter"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="buffer">The <see cref="ViewBuffer"/> for buffered output.</param>
|
||||||
|
/// <param name="encoding">The <see cref="System.Text.Encoding"/>.</param>
|
||||||
|
/// <param name="htmlEncoder">The HTML encoder.</param>
|
||||||
|
/// <param name="inner">
|
||||||
|
/// The inner <see cref="TextWriter"/> to write output to when this instance is no longer buffering.
|
||||||
|
/// </param>
|
||||||
|
public ViewBufferTextWriter(ViewBuffer buffer, Encoding encoding, HtmlEncoder htmlEncoder, TextWriter inner)
|
||||||
|
{
|
||||||
|
if (buffer == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoding == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(encoding));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (htmlEncoder == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(htmlEncoder));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inner == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(inner));
|
||||||
|
}
|
||||||
|
|
||||||
|
Buffer = buffer;
|
||||||
|
Encoding = encoding;
|
||||||
|
_htmlEncoder = htmlEncoder;
|
||||||
_inner = inner;
|
_inner = inner;
|
||||||
_pages = new List<char[]>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Encoding Encoding => _inner.Encoding;
|
/// <inheritdoc />
|
||||||
|
public override Encoding Encoding { get; }
|
||||||
|
|
||||||
public override void Flush()
|
/// <inheritdoc />
|
||||||
|
public bool IsBuffering { get; private set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the <see cref="ViewBuffer"/>.
|
||||||
|
/// </summary>
|
||||||
|
public ViewBuffer Buffer { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(char value)
|
||||||
{
|
{
|
||||||
// Don't do anything. We'll call FlushAsync.
|
if (IsBuffering)
|
||||||
|
{
|
||||||
|
Buffer.AppendHtml(value.ToString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_inner.Write(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task FlushAsync()
|
/// <inheritdoc />
|
||||||
|
public override void Write(char[] buffer, int index, int count)
|
||||||
{
|
{
|
||||||
if (_pages.Count == 0)
|
if (buffer == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0 || index >= buffer.Length)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count < 0 || (buffer.Length - index < count))
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(count));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsBuffering)
|
||||||
|
{
|
||||||
|
Buffer.AppendHtml(new string(buffer, index, count));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_inner.Write(buffer, index, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i <= _currentPage; i++)
|
if (IsBuffering)
|
||||||
{
|
{
|
||||||
var page = _pages[i];
|
Buffer.AppendHtml(value);
|
||||||
|
|
||||||
var count = i == _currentPage ? _currentIndex : page.Length;
|
|
||||||
if (count > 0)
|
|
||||||
{
|
|
||||||
await _inner.WriteAsync(page, 0, count);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
// Return all but one of the pages. This way if someone writes a large chunk of
|
|
||||||
// content, we can return those buffers and avoid holding them for the whole
|
|
||||||
// page's lifetime.
|
|
||||||
for (var i = _pages.Count - 1; i > 0; i--)
|
|
||||||
{
|
{
|
||||||
var page = _pages[i];
|
_inner.Write(value);
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_pages.RemoveAt(i);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_pool.Return(page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentPage = 0;
|
|
||||||
_currentIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Write(char value)
|
|
||||||
{
|
|
||||||
var page = GetCurrentPage();
|
|
||||||
page[_currentIndex++] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Write(char[] buffer)
|
|
||||||
{
|
|
||||||
Write(buffer, 0, buffer.Length);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Write(char[] buffer, int index, int count)
|
|
||||||
{
|
|
||||||
while (count > 0)
|
|
||||||
{
|
|
||||||
var page = GetCurrentPage();
|
|
||||||
var copyLength = Math.Min(count, page.Length - _currentIndex);
|
|
||||||
Debug.Assert(copyLength > 0);
|
|
||||||
|
|
||||||
Array.Copy(
|
|
||||||
buffer,
|
|
||||||
index,
|
|
||||||
page,
|
|
||||||
_currentIndex,
|
|
||||||
copyLength);
|
|
||||||
|
|
||||||
_currentIndex += copyLength;
|
|
||||||
index += copyLength;
|
|
||||||
|
|
||||||
count -= copyLength;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Write(string value)
|
/// <inheritdoc />
|
||||||
|
public override void Write(object value)
|
||||||
{
|
{
|
||||||
var index = 0;
|
if (value == null)
|
||||||
var count = value.Length;
|
|
||||||
|
|
||||||
while (count > 0)
|
|
||||||
{
|
{
|
||||||
var page = GetCurrentPage();
|
return;
|
||||||
var copyLength = Math.Min(count, page.Length - _currentIndex);
|
}
|
||||||
Debug.Assert(copyLength > 0);
|
|
||||||
|
|
||||||
value.CopyTo(
|
IHtmlContentContainer container;
|
||||||
index,
|
IHtmlContent content;
|
||||||
page,
|
if ((container = value as IHtmlContentContainer) != null)
|
||||||
_currentIndex,
|
{
|
||||||
copyLength);
|
Write(container);
|
||||||
|
}
|
||||||
_currentIndex += copyLength;
|
else if ((content = value as IHtmlContent) != null)
|
||||||
index += copyLength;
|
{
|
||||||
|
Write(content);
|
||||||
count -= copyLength;
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Write(value.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes an <see cref="IHtmlContent"/> value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The <see cref="IHtmlContent"/> value.</param>
|
||||||
|
public void Write(IHtmlContent value)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsBuffering)
|
||||||
|
{
|
||||||
|
Buffer.AppendHtml(value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
value.WriteTo(_inner, _htmlEncoder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes an <see cref="IHtmlContentContainer"/> value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The <see cref="IHtmlContentContainer"/> value.</param>
|
||||||
|
public void Write(IHtmlContentContainer value)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsBuffering)
|
||||||
|
{
|
||||||
|
value.MoveTo(Buffer);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
value.WriteTo(_inner, _htmlEncoder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void WriteLine(object value)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IHtmlContentContainer container;
|
||||||
|
IHtmlContent content;
|
||||||
|
if ((container = value as IHtmlContentContainer) != null)
|
||||||
|
{
|
||||||
|
Write(container);
|
||||||
|
Write(NewLine);
|
||||||
|
}
|
||||||
|
else if ((content = value as IHtmlContent) != null)
|
||||||
|
{
|
||||||
|
Write(content);
|
||||||
|
Write(NewLine);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Write(value.ToString());
|
||||||
|
Write(NewLine);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override Task WriteAsync(char value)
|
public override Task WriteAsync(char value)
|
||||||
{
|
{
|
||||||
return _inner.WriteAsync(value);
|
if (IsBuffering)
|
||||||
|
{
|
||||||
|
Buffer.AppendHtml(value.ToString());
|
||||||
|
return TaskCache.CompletedTask;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return _inner.WriteAsync(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override Task WriteAsync(char[] buffer, int index, int count)
|
public override Task WriteAsync(char[] buffer, int index, int count)
|
||||||
{
|
{
|
||||||
return _inner.WriteAsync(buffer, index, count);
|
if (buffer == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index < 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(index));
|
||||||
|
}
|
||||||
|
if (count < 0 || (buffer.Length - index < count))
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(count));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsBuffering)
|
||||||
|
{
|
||||||
|
Buffer.AppendHtml(new string(buffer, index, count));
|
||||||
|
return TaskCache.CompletedTask;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return _inner.WriteAsync(buffer, index, count);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public override Task WriteAsync(string value)
|
public override Task WriteAsync(string value)
|
||||||
{
|
{
|
||||||
return _inner.WriteAsync(value);
|
if (IsBuffering)
|
||||||
|
{
|
||||||
|
Buffer.AppendHtml(value);
|
||||||
|
return TaskCache.CompletedTask;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return _inner.WriteAsync(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private char[] GetCurrentPage()
|
/// <inheritdoc />
|
||||||
|
public override void WriteLine()
|
||||||
{
|
{
|
||||||
char[] page = null;
|
if (IsBuffering)
|
||||||
if (_pages.Count == 0)
|
|
||||||
{
|
{
|
||||||
Debug.Assert(_currentPage == 0);
|
Buffer.AppendHtml(NewLine);
|
||||||
Debug.Assert(_currentIndex == 0);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
page = _pool.Rent(PageSize);
|
|
||||||
_pages.Add(page);
|
|
||||||
}
|
|
||||||
catch when (page != null)
|
|
||||||
{
|
|
||||||
_pool.Return(page);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
|
|
||||||
return page;
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
Debug.Assert(_pages.Count > _currentPage);
|
|
||||||
page = _pages[_currentPage];
|
|
||||||
|
|
||||||
if (_currentIndex == page.Length)
|
|
||||||
{
|
{
|
||||||
// Current page is full.
|
_inner.WriteLine();
|
||||||
_currentPage++;
|
|
||||||
_currentIndex = 0;
|
|
||||||
|
|
||||||
if (_pages.Count == _currentPage)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
page = _pool.Rent(PageSize);
|
|
||||||
_pages.Add(page);
|
|
||||||
}
|
|
||||||
catch when (page != null)
|
|
||||||
{
|
|
||||||
_pool.Return(page);
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return page;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
/// <inheritdoc />
|
||||||
|
public override void WriteLine(string value)
|
||||||
{
|
{
|
||||||
base.Dispose(disposing);
|
if (IsBuffering)
|
||||||
|
|
||||||
for (var i = 0; i < _pages.Count; i++)
|
|
||||||
{
|
{
|
||||||
_pool.Return(_pages[i]);
|
Buffer.AppendHtml(value);
|
||||||
|
Buffer.AppendHtml(NewLine);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_inner.WriteLine(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task WriteLineAsync(char value)
|
||||||
|
{
|
||||||
|
if (IsBuffering)
|
||||||
|
{
|
||||||
|
Buffer.AppendHtml(value.ToString());
|
||||||
|
Buffer.AppendHtml(NewLine);
|
||||||
|
return TaskCache.CompletedTask;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return _inner.WriteLineAsync(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task WriteLineAsync(char[] value, int start, int offset)
|
||||||
|
{
|
||||||
|
if (IsBuffering)
|
||||||
|
{
|
||||||
|
Buffer.AppendHtml(new string(value, start, offset));
|
||||||
|
Buffer.AppendHtml(NewLine);
|
||||||
|
return TaskCache.CompletedTask;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return _inner.WriteLineAsync(value, start, offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task WriteLineAsync(string value)
|
||||||
|
{
|
||||||
|
if (IsBuffering)
|
||||||
|
{
|
||||||
|
Buffer.AppendHtml(value);
|
||||||
|
Buffer.AppendHtml(NewLine);
|
||||||
|
return TaskCache.CompletedTask;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return _inner.WriteLineAsync(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Task WriteLineAsync()
|
||||||
|
{
|
||||||
|
if (IsBuffering)
|
||||||
|
{
|
||||||
|
Buffer.AppendHtml(NewLine);
|
||||||
|
return TaskCache.CompletedTask;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return _inner.WriteLineAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copies the buffered content to the unbuffered writer and invokes flush on it.
|
||||||
|
/// Additionally causes this instance to no longer buffer and direct all write operations
|
||||||
|
/// to the unbuffered writer.
|
||||||
|
/// </summary>
|
||||||
|
public override void Flush()
|
||||||
|
{
|
||||||
|
if (_inner == null || _inner is ViewBufferTextWriter)
|
||||||
|
{
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_pages.Clear();
|
if (IsBuffering)
|
||||||
|
{
|
||||||
|
IsBuffering = false;
|
||||||
|
Buffer.WriteTo(_inner, _htmlEncoder);
|
||||||
|
Buffer.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
_inner.Flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copies the buffered content to the unbuffered writer and invokes flush on it.
|
||||||
|
/// Additionally causes this instance to no longer buffer and direct all write operations
|
||||||
|
/// to the unbuffered writer.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A <see cref="Task"/> that represents the asynchronous copy and flush operations.</returns>
|
||||||
|
public override async Task FlushAsync()
|
||||||
|
{
|
||||||
|
if (_inner == null || _inner is ViewBufferTextWriter)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsBuffering)
|
||||||
|
{
|
||||||
|
IsBuffering = false;
|
||||||
|
await Buffer.WriteToAsync(_inner, _htmlEncoder);
|
||||||
|
Buffer.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _inner.FlushAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3,9 +3,8 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using Microsoft.AspNetCore.Html;
|
|
||||||
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
using Microsoft.AspNetCore.Mvc.ViewFeatures;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Rendering
|
namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
{
|
{
|
||||||
|
|
@ -15,20 +14,29 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
public class MvcForm : IDisposable
|
public class MvcForm : IDisposable
|
||||||
{
|
{
|
||||||
private readonly ViewContext _viewContext;
|
private readonly ViewContext _viewContext;
|
||||||
|
private readonly HtmlEncoder _htmlEncoder;
|
||||||
|
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of <see cref="MvcForm"/>.
|
/// Initializes a new instance of <see cref="MvcForm"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="viewContext">The <see cref="ViewContext"/>.</param>
|
/// <param name="viewContext">The <see cref="ViewContext"/>.</param>
|
||||||
public MvcForm(ViewContext viewContext)
|
/// <param name="htmlEncoder">The <see cref="HtmlEncoder"/>.</param>
|
||||||
|
public MvcForm(ViewContext viewContext, HtmlEncoder htmlEncoder)
|
||||||
{
|
{
|
||||||
if (viewContext == null)
|
if (viewContext == null)
|
||||||
{
|
{
|
||||||
throw new ArgumentNullException(nameof(viewContext));
|
throw new ArgumentNullException(nameof(viewContext));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (htmlEncoder == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(htmlEncoder));
|
||||||
|
}
|
||||||
|
|
||||||
_viewContext = viewContext;
|
_viewContext = viewContext;
|
||||||
|
_htmlEncoder = htmlEncoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -63,27 +71,24 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
private void RenderEndOfFormContent()
|
private void RenderEndOfFormContent()
|
||||||
{
|
{
|
||||||
var formContext = _viewContext.FormContext;
|
var formContext = _viewContext.FormContext;
|
||||||
if (formContext.HasEndOfFormContent)
|
if (!formContext.HasEndOfFormContent)
|
||||||
{
|
{
|
||||||
var writer = _viewContext.Writer;
|
return;
|
||||||
var htmlWriter = writer as HtmlTextWriter;
|
}
|
||||||
|
|
||||||
HtmlEncoder htmlEncoder = null;
|
|
||||||
if (htmlWriter == null)
|
|
||||||
{
|
|
||||||
htmlEncoder = _viewContext.HttpContext.RequestServices.GetRequiredService<HtmlEncoder>();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
var viewBufferWriter = _viewContext.Writer as ViewBufferTextWriter;
|
||||||
|
if (viewBufferWriter == null)
|
||||||
|
{
|
||||||
foreach (var content in formContext.EndOfFormContent)
|
foreach (var content in formContext.EndOfFormContent)
|
||||||
{
|
{
|
||||||
if (htmlWriter == null)
|
content.WriteTo(_viewContext.Writer, _htmlEncoder);
|
||||||
{
|
}
|
||||||
content.WriteTo(writer, htmlEncoder);
|
}
|
||||||
}
|
else
|
||||||
else
|
{
|
||||||
{
|
foreach (var content in formContext.EndOfFormContent)
|
||||||
htmlWriter.Write(content);
|
{
|
||||||
}
|
viewBufferWriter.Write(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -251,13 +251,14 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
|
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
|
||||||
{
|
{
|
||||||
var htmlTextWriter = writer as HtmlTextWriter;
|
if (writer == null)
|
||||||
if (htmlTextWriter != null)
|
|
||||||
{
|
{
|
||||||
// As a perf optimization, we can buffer this output rather than writing it
|
throw new ArgumentNullException(nameof(writer));
|
||||||
// out character by character.
|
}
|
||||||
htmlTextWriter.Write(this);
|
|
||||||
return;
|
if (encoder == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(encoder));
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (TagRenderMode)
|
switch (TagRenderMode)
|
||||||
|
|
|
||||||
|
|
@ -134,8 +134,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||||
ViewComponentDescriptor descriptor,
|
ViewComponentDescriptor descriptor,
|
||||||
object arguments)
|
object arguments)
|
||||||
{
|
{
|
||||||
var viewBuffer = new ViewBuffer(_viewBufferScope, descriptor.FullName);
|
var viewBuffer = new ViewBuffer(_viewBufferScope, descriptor.FullName, ViewBuffer.ViewComponentPageSize);
|
||||||
using (var writer = new HtmlContentWrapperTextWriter(viewBuffer, _viewContext.Writer.Encoding))
|
using (var writer = new ViewBufferTextWriter(viewBuffer, _viewContext.Writer.Encoding))
|
||||||
{
|
{
|
||||||
var context = new ViewComponentContext(
|
var context = new ViewComponentContext(
|
||||||
descriptor,
|
descriptor,
|
||||||
|
|
@ -152,7 +152,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||||
}
|
}
|
||||||
|
|
||||||
await invoker.InvokeAsync(context);
|
await invoker.InvokeAsync(context);
|
||||||
return writer.ContentBuilder;
|
return viewBuffer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,15 +46,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewComponents
|
||||||
throw new ArgumentNullException(nameof(context));
|
throw new ArgumentNullException(nameof(context));
|
||||||
}
|
}
|
||||||
|
|
||||||
var htmlWriter = context.Writer as HtmlTextWriter;
|
context.Writer.Write(EncodedContent);
|
||||||
if (htmlWriter == null)
|
|
||||||
{
|
|
||||||
EncodedContent.WriteTo(context.Writer, context.HtmlEncoder);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
htmlWriter.Write(EncodedContent);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -57,14 +57,6 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
// _fieldName containing almost any character.
|
// _fieldName containing almost any character.
|
||||||
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
|
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
|
||||||
{
|
{
|
||||||
var htmlTextWriter = writer as HtmlTextWriter;
|
|
||||||
if (htmlTextWriter != null)
|
|
||||||
{
|
|
||||||
// If possible, defer encoding until we're writing to the response.
|
|
||||||
htmlTextWriter.Write(this);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.Write("<input name=\"");
|
writer.Write("<input name=\"");
|
||||||
encoder.Encode(writer, _fieldName);
|
encoder.Encode(writer, _fieldName);
|
||||||
writer.Write("\" type=\"hidden\" value=\"");
|
writer.Write("\" type=\"hidden\" value=\"");
|
||||||
|
|
|
||||||
|
|
@ -505,11 +505,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
throw new ArgumentNullException(nameof(partialViewName));
|
throw new ArgumentNullException(nameof(partialViewName));
|
||||||
}
|
}
|
||||||
|
|
||||||
var viewBuffer = new ViewBuffer(_bufferScope, partialViewName);
|
var viewBuffer = new ViewBuffer(_bufferScope, partialViewName, ViewBuffer.PartialViewPageSize);
|
||||||
using (var writer = new HtmlContentWrapperTextWriter(viewBuffer, Encoding.UTF8))
|
using (var writer = new ViewBufferTextWriter(viewBuffer, Encoding.UTF8))
|
||||||
{
|
{
|
||||||
await RenderPartialCoreAsync(partialViewName, model, viewData, writer);
|
await RenderPartialCoreAsync(partialViewName, model, viewData, writer);
|
||||||
return writer.ContentBuilder;
|
return viewBuffer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -742,7 +742,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
/// <returns>A new <see cref="MvcForm"/> instance.</returns>
|
/// <returns>A new <see cref="MvcForm"/> instance.</returns>
|
||||||
protected virtual MvcForm CreateForm()
|
protected virtual MvcForm CreateForm()
|
||||||
{
|
{
|
||||||
return new MvcForm(ViewContext);
|
return new MvcForm(ViewContext, _htmlEncoder);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual IHtmlContent GenerateCheckBox(
|
protected virtual IHtmlContent GenerateCheckBox(
|
||||||
|
|
|
||||||
|
|
@ -39,17 +39,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
|
||||||
throw new ArgumentNullException(nameof(encoder));
|
throw new ArgumentNullException(nameof(encoder));
|
||||||
}
|
}
|
||||||
|
|
||||||
var htmlTextWriter = writer as HtmlTextWriter;
|
encoder.Encode(writer, _input);
|
||||||
if (htmlTextWriter != null)
|
|
||||||
{
|
|
||||||
// As a perf optimization, we can buffer this output rather than writing it
|
|
||||||
// out character by character.
|
|
||||||
htmlTextWriter.Write(this);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
encoder.Encode(writer, _input);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string DebuggerToString()
|
private string DebuggerToString()
|
||||||
|
|
|
||||||
|
|
@ -126,8 +126,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
|
|
||||||
if (viewEngineResult.Success)
|
if (viewEngineResult.Success)
|
||||||
{
|
{
|
||||||
var viewBuffer = new ViewBuffer(_bufferScope, viewName);
|
var viewBuffer = new ViewBuffer(_bufferScope, viewName, ViewBuffer.PartialViewPageSize);
|
||||||
using (var writer = new HtmlContentWrapperTextWriter(viewBuffer, _viewContext.Writer.Encoding))
|
using (var writer = new ViewBufferTextWriter(viewBuffer, _viewContext.Writer.Encoding))
|
||||||
{
|
{
|
||||||
// Forcing synchronous behavior so users don't have to await templates.
|
// Forcing synchronous behavior so users don't have to await templates.
|
||||||
var view = viewEngineResult.View;
|
var view = viewEngineResult.View;
|
||||||
|
|
@ -136,7 +136,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
var viewContext = new ViewContext(_viewContext, viewEngineResult.View, _viewData, writer);
|
var viewContext = new ViewContext(_viewContext, viewEngineResult.View, _viewData, writer);
|
||||||
var renderTask = viewEngineResult.View.RenderAsync(viewContext);
|
var renderTask = viewEngineResult.View.RenderAsync(viewContext);
|
||||||
renderTask.GetAwaiter().GetResult();
|
renderTask.GetAwaiter().GetResult();
|
||||||
return writer.ContentBuilder;
|
return viewBuffer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Encodings.Web;
|
using System.Text.Encodings.Web;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Html;
|
using Microsoft.AspNetCore.Html;
|
||||||
|
|
@ -52,7 +53,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await page.ExecuteAsync();
|
await page.ExecuteAsync();
|
||||||
var pageOutput = page.Output.ToString();
|
var pageOutput = page.RenderedContent;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
|
|
@ -79,7 +80,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await page.ExecuteAsync();
|
await page.ExecuteAsync();
|
||||||
var pageOutput = page.Output.ToString();
|
var pageOutput = page.RenderedContent;
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("HtmlEncode[[Hello Prefix]]HtmlEncode[[From Scope: ]]HtmlEncode[[Hello In Scope]]", pageOutput);
|
Assert.Equal("HtmlEncode[[Hello Prefix]]HtmlEncode[[From Scope: ]]HtmlEncode[[Hello In Scope]]", pageOutput);
|
||||||
|
|
@ -113,7 +114,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
await page.ExecuteAsync();
|
await page.ExecuteAsync();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var pageOutput = page.Output.ToString();
|
var pageOutput = page.RenderedContent;
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
"HtmlEncode[[Hello Prefix]]HtmlEncode[[From Scopes: ]]HtmlEncode[[Hello In Scope Pre Nest]]" +
|
"HtmlEncode[[Hello Prefix]]HtmlEncode[[From Scopes: ]]HtmlEncode[[Hello In Scope Pre Nest]]" +
|
||||||
"HtmlEncode[[Hello In Scope Post Nest]]HtmlEncode[[Hello In Nested Scope]]",
|
"HtmlEncode[[Hello In Scope Post Nest]]HtmlEncode[[Hello In Nested Scope]]",
|
||||||
|
|
@ -224,6 +225,138 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
await page.ExecuteAsync();
|
await page.ExecuteAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is an integration test for ensuring that ViewBuffer segments used by
|
||||||
|
// TagHelpers can be merged back into the 'main' segment where possible.
|
||||||
|
[Fact]
|
||||||
|
public async Task TagHelperScopes_ViewBuffersCanCombine()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var bufferScope = new TestViewBufferScope();
|
||||||
|
var viewContext = CreateViewContext(bufferScope: bufferScope);
|
||||||
|
|
||||||
|
var page = CreatePage(async v =>
|
||||||
|
{
|
||||||
|
Assert.Equal(0, bufferScope.CreatedBuffers.Count);
|
||||||
|
v.Write("Level:0"); // Creates a 'top-level' buffer.
|
||||||
|
Assert.Equal(1, bufferScope.CreatedBuffers.Count);
|
||||||
|
|
||||||
|
// Run a TagHelper
|
||||||
|
{
|
||||||
|
v.StartTagHelperWritingScope(encoder: null);
|
||||||
|
|
||||||
|
Assert.Equal(1, bufferScope.CreatedBuffers.Count);
|
||||||
|
Assert.Equal(0, bufferScope.ReturnedBuffers.Count);
|
||||||
|
v.Write("Level:1-A"); // Creates a new buffer for the taghelper.
|
||||||
|
Assert.Equal(2, bufferScope.CreatedBuffers.Count);
|
||||||
|
Assert.Equal(0, bufferScope.ReturnedBuffers.Count);
|
||||||
|
|
||||||
|
TagHelperContent innerContentLevel1 = null;
|
||||||
|
var outputLevel1 = new TagHelperOutput("t1", new TagHelperAttributeList(), (_, encoder) =>
|
||||||
|
{
|
||||||
|
return Task.FromResult(innerContentLevel1);
|
||||||
|
});
|
||||||
|
|
||||||
|
innerContentLevel1 = v.EndTagHelperWritingScope();
|
||||||
|
outputLevel1.Content = await outputLevel1.GetChildContentAsync();
|
||||||
|
|
||||||
|
Assert.Equal(2, bufferScope.CreatedBuffers.Count);
|
||||||
|
Assert.Equal(0, bufferScope.ReturnedBuffers.Count);
|
||||||
|
v.Write(outputLevel1); // Writing the taghelper to output returns a buffer.
|
||||||
|
Assert.Equal(2, bufferScope.CreatedBuffers.Count);
|
||||||
|
Assert.Equal(1, bufferScope.ReturnedBuffers.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(2, bufferScope.CreatedBuffers.Count);
|
||||||
|
Assert.Equal(1, bufferScope.ReturnedBuffers.Count);
|
||||||
|
v.Write("Level:0"); // Already have a buffer for this scope.
|
||||||
|
Assert.Equal(2, bufferScope.CreatedBuffers.Count);
|
||||||
|
Assert.Equal(1, bufferScope.ReturnedBuffers.Count);
|
||||||
|
|
||||||
|
// Run another TagHelper
|
||||||
|
{
|
||||||
|
v.StartTagHelperWritingScope(encoder: null);
|
||||||
|
|
||||||
|
Assert.Equal(2, bufferScope.CreatedBuffers.Count);
|
||||||
|
Assert.Equal(1, bufferScope.ReturnedBuffers.Count);
|
||||||
|
v.Write("Level:1-B"); // Creates a new buffer for the taghelper.
|
||||||
|
Assert.Equal(3, bufferScope.CreatedBuffers.Count);
|
||||||
|
Assert.Equal(1, bufferScope.ReturnedBuffers.Count);
|
||||||
|
|
||||||
|
TagHelperContent innerContentLevel1 = null;
|
||||||
|
var outputLevel1 = new TagHelperOutput("t2", new TagHelperAttributeList(), (_, encoder) =>
|
||||||
|
{
|
||||||
|
return Task.FromResult(innerContentLevel1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Run a nested TagHelper
|
||||||
|
{
|
||||||
|
v.StartTagHelperWritingScope(encoder: null);
|
||||||
|
|
||||||
|
Assert.Equal(3, bufferScope.CreatedBuffers.Count);
|
||||||
|
Assert.Equal(1, bufferScope.ReturnedBuffers.Count);
|
||||||
|
v.Write("Level:2"); // Creates a new buffer for the taghelper.
|
||||||
|
Assert.Equal(4, bufferScope.CreatedBuffers.Count);
|
||||||
|
Assert.Equal(1, bufferScope.ReturnedBuffers.Count);
|
||||||
|
|
||||||
|
TagHelperContent innerContentLevel2 = null;
|
||||||
|
var outputLevel2 = new TagHelperOutput("t3", new TagHelperAttributeList(), (_, encoder) =>
|
||||||
|
{
|
||||||
|
return Task.FromResult(innerContentLevel2);
|
||||||
|
});
|
||||||
|
|
||||||
|
innerContentLevel2 = v.EndTagHelperWritingScope();
|
||||||
|
outputLevel2.Content = await outputLevel2.GetChildContentAsync();
|
||||||
|
|
||||||
|
Assert.Equal(4, bufferScope.CreatedBuffers.Count);
|
||||||
|
Assert.Equal(1, bufferScope.ReturnedBuffers.Count);
|
||||||
|
v.Write(outputLevel2); // Writing the taghelper to output returns a buffer.
|
||||||
|
Assert.Equal(4, bufferScope.CreatedBuffers.Count);
|
||||||
|
Assert.Equal(2, bufferScope.ReturnedBuffers.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(4, bufferScope.CreatedBuffers.Count);
|
||||||
|
Assert.Equal(2, bufferScope.ReturnedBuffers.Count);
|
||||||
|
v.Write("Level:1-B"); // Already have a buffer for this scope.
|
||||||
|
Assert.Equal(4, bufferScope.CreatedBuffers.Count);
|
||||||
|
Assert.Equal(2, bufferScope.ReturnedBuffers.Count);
|
||||||
|
|
||||||
|
innerContentLevel1 = v.EndTagHelperWritingScope();
|
||||||
|
outputLevel1.Content = await outputLevel1.GetChildContentAsync();
|
||||||
|
|
||||||
|
Assert.Equal(4, bufferScope.CreatedBuffers.Count);
|
||||||
|
Assert.Equal(2, bufferScope.ReturnedBuffers.Count);
|
||||||
|
v.Write(outputLevel1); // Writing the taghelper to output returns a buffer.
|
||||||
|
Assert.Equal(4, bufferScope.CreatedBuffers.Count);
|
||||||
|
Assert.Equal(3, bufferScope.ReturnedBuffers.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Equal(4, bufferScope.CreatedBuffers.Count);
|
||||||
|
Assert.Equal(3, bufferScope.ReturnedBuffers.Count);
|
||||||
|
v.Write("Level:0"); // Already have a buffer for this scope.
|
||||||
|
Assert.Equal(4, bufferScope.CreatedBuffers.Count);
|
||||||
|
Assert.Equal(3, bufferScope.ReturnedBuffers.Count);
|
||||||
|
|
||||||
|
}, viewContext);
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
await page.ExecuteAsync();
|
||||||
|
Assert.Equal(
|
||||||
|
"HtmlEncode[[Level:0]]" +
|
||||||
|
"<t1>" +
|
||||||
|
"HtmlEncode[[Level:1-A]]" +
|
||||||
|
"</t1>" +
|
||||||
|
"HtmlEncode[[Level:0]]" +
|
||||||
|
"<t2>" +
|
||||||
|
"HtmlEncode[[Level:1-B]]" +
|
||||||
|
"<t3>" +
|
||||||
|
"HtmlEncode[[Level:2]]" +
|
||||||
|
"</t3>" +
|
||||||
|
"HtmlEncode[[Level:1-B]]" +
|
||||||
|
"</t2>" +
|
||||||
|
"HtmlEncode[[Level:0]]",
|
||||||
|
page.RenderedContent);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task DefineSection_ThrowsIfSectionIsAlreadyDefined()
|
public async Task DefineSection_ThrowsIfSectionIsAlreadyDefined()
|
||||||
{
|
{
|
||||||
|
|
@ -1237,8 +1370,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
public async Task Write_WithHtmlString_WritesValueWithoutEncoding()
|
public async Task Write_WithHtmlString_WritesValueWithoutEncoding()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var buffer = new ViewBuffer(new TestViewBufferScope(), string.Empty);
|
var buffer = new ViewBuffer(new TestViewBufferScope(), string.Empty, pageSize: 32);
|
||||||
var writer = new RazorTextWriter(TextWriter.Null, buffer, new HtmlTestEncoder());
|
var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8);
|
||||||
|
|
||||||
var page = CreatePage(p =>
|
var page = CreatePage(p =>
|
||||||
{
|
{
|
||||||
|
|
@ -1250,7 +1383,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
await page.ExecuteAsync();
|
await page.ExecuteAsync();
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("Hello world", HtmlContentUtilities.HtmlContentToString(writer.Buffer));
|
Assert.Equal("Hello world", HtmlContentUtilities.HtmlContentToString(buffer));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TestableRazorPage CreatePage(
|
private static TestableRazorPage CreatePage(
|
||||||
|
|
@ -1284,12 +1417,18 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
return view.Object;
|
return view.Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ViewContext CreateViewContext(TextWriter writer = null, string viewPath = null)
|
private static ViewContext CreateViewContext(
|
||||||
|
TextWriter writer = null,
|
||||||
|
IViewBufferScope bufferScope = null,
|
||||||
|
string viewPath = null)
|
||||||
{
|
{
|
||||||
writer = writer ?? new StringWriter();
|
bufferScope = bufferScope ?? new TestViewBufferScope();
|
||||||
|
var buffer = new ViewBuffer(bufferScope, viewPath ?? "TEST", 32);
|
||||||
|
writer = writer ?? new ViewBufferTextWriter(buffer, Encoding.UTF8);
|
||||||
|
|
||||||
var httpContext = new DefaultHttpContext();
|
var httpContext = new DefaultHttpContext();
|
||||||
var serviceProvider = new ServiceCollection()
|
var serviceProvider = new ServiceCollection()
|
||||||
.AddSingleton<IViewBufferScope, TestViewBufferScope>()
|
.AddSingleton<IViewBufferScope>(bufferScope)
|
||||||
.BuildServiceProvider();
|
.BuildServiceProvider();
|
||||||
httpContext.RequestServices = serviceProvider;
|
httpContext.RequestServices = serviceProvider;
|
||||||
var actionContext = new ActionContext(
|
var actionContext = new ActionContext(
|
||||||
|
|
@ -1321,8 +1460,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var writer = Assert.IsType<StringWriter>(Output);
|
var bufferedWriter = Assert.IsType<ViewBufferTextWriter>(Output);
|
||||||
return writer.ToString();
|
using (var stringWriter = new StringWriter())
|
||||||
|
{
|
||||||
|
bufferedWriter.Buffer.WriteTo(stringWriter, HtmlEncoder);
|
||||||
|
return stringWriter.ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.NotSame(expected, actual);
|
Assert.NotSame(expected, actual);
|
||||||
Assert.IsType<RazorTextWriter>(actual);
|
Assert.IsType<ViewBufferTextWriter>(actual);
|
||||||
Assert.Equal("HtmlEncode[[Hello world]]", viewContext.Writer.ToString());
|
Assert.Equal("HtmlEncode[[Hello world]]", viewContext.Writer.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,7 +248,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor
|
||||||
await view.RenderAsync(viewContext);
|
await view.RenderAsync(viewContext);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.IsType<RazorTextWriter>(actual);
|
Assert.IsType<ViewBufferTextWriter>(actual);
|
||||||
Assert.NotSame(original, actual);
|
Assert.NotSame(original, actual);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ using System.Linq;
|
||||||
using Microsoft.AspNetCore.Razor;
|
using Microsoft.AspNetCore.Razor;
|
||||||
using Microsoft.AspNetCore.Razor.Chunks.Generators;
|
using Microsoft.AspNetCore.Razor.Chunks.Generators;
|
||||||
using Microsoft.AspNetCore.Razor.Editor;
|
using Microsoft.AspNetCore.Razor.Editor;
|
||||||
using Microsoft.AspNetCore.Razor.Generator;
|
|
||||||
using Microsoft.AspNetCore.Razor.Parser.SyntaxTree;
|
using Microsoft.AspNetCore.Razor.Parser.SyntaxTree;
|
||||||
using Microsoft.AspNetCore.Razor.Text;
|
using Microsoft.AspNetCore.Razor.Text;
|
||||||
using Microsoft.AspNetCore.Razor.Tokenizer;
|
using Microsoft.AspNetCore.Razor.Tokenizer;
|
||||||
|
|
|
||||||
|
|
@ -1,186 +0,0 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
|
||||||
// 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 System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Encodings.Web;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Microsoft.AspNetCore.Html;
|
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
|
||||||
{
|
|
||||||
public class HtmlContentWrapperTextWriterTest
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public async Task Write_WritesCharBuffer()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var input1 = new ArraySegment<char>(new char[] { 'a', 'b', 'c', 'd' }, 1, 3);
|
|
||||||
var input2 = new ArraySegment<char>(new char[] { 'e', 'f' }, 0, 2);
|
|
||||||
var input3 = new ArraySegment<char>(new char[] { 'g', 'h', 'i', 'j' }, 3, 1);
|
|
||||||
var buffer = new TestHtmlContentBuilder();
|
|
||||||
var writer = new HtmlContentWrapperTextWriter(buffer, Encoding.UTF8);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
writer.Write(input1.Array, input1.Offset, input1.Count);
|
|
||||||
await writer.WriteAsync(input2.Array, input2.Offset, input2.Count);
|
|
||||||
await writer.WriteLineAsync(input3.Array, input3.Offset, input3.Count);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Collection(buffer.Values,
|
|
||||||
value => Assert.Equal("bcd", value),
|
|
||||||
value => Assert.Equal("ef", value),
|
|
||||||
value => Assert.Equal("j", value),
|
|
||||||
value => Assert.Equal(Environment.NewLine, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Write_SplitsCharBuffer_Into1kbStrings()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var charArray = Enumerable.Range(0, 2050).Select(_ => 'a').ToArray();
|
|
||||||
var buffer = new TestHtmlContentBuilder();
|
|
||||||
var writer = new HtmlContentWrapperTextWriter(buffer, Encoding.UTF8);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
writer.Write(charArray);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Collection(
|
|
||||||
buffer.Values,
|
|
||||||
value => Assert.Equal(new string('a', 1024), value),
|
|
||||||
value => Assert.Equal(new string('a', 1024), value),
|
|
||||||
value => Assert.Equal("aa", value));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Write_HtmlContent_AddsToEntries()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var buffer = new TestHtmlContentBuilder();
|
|
||||||
var writer = new HtmlContentWrapperTextWriter(buffer, Encoding.UTF8);
|
|
||||||
var content = new HtmlString("Hello, world!");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
writer.Write(content);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Collection(
|
|
||||||
buffer.Values,
|
|
||||||
item => Assert.Same(content, item));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Write_Object_HtmlContent_AddsToEntries()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var buffer = new TestHtmlContentBuilder();
|
|
||||||
var writer = new HtmlContentWrapperTextWriter(buffer, Encoding.UTF8);
|
|
||||||
var content = new HtmlString("Hello, world!");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
writer.Write((object)content);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Collection(
|
|
||||||
buffer.Values,
|
|
||||||
item => Assert.Same(content, item));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void WriteLine_Object_HtmlContent_AddsToEntries()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var buffer = new TestHtmlContentBuilder();
|
|
||||||
var writer = new HtmlContentWrapperTextWriter(buffer, Encoding.UTF8);
|
|
||||||
var content = new HtmlString("Hello, world!");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
writer.WriteLine(content);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Collection(
|
|
||||||
buffer.Values,
|
|
||||||
item => Assert.Same(content, item),
|
|
||||||
item => Assert.Equal(Environment.NewLine, item));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Write_WritesStringBuffer()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var newLine = Environment.NewLine;
|
|
||||||
var input1 = "Hello";
|
|
||||||
var input2 = "from";
|
|
||||||
var input3 = "ASP";
|
|
||||||
var input4 = ".Net";
|
|
||||||
var buffer = new TestHtmlContentBuilder();
|
|
||||||
var writer = new HtmlContentWrapperTextWriter(buffer, Encoding.UTF8);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
writer.Write(input1);
|
|
||||||
writer.WriteLine(input2);
|
|
||||||
await writer.WriteAsync(input3);
|
|
||||||
await writer.WriteLineAsync(input4);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(new[] { input1, input2, newLine, input3, input4, newLine }, buffer.Values);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Write_HtmlContent_WritesToBuffer()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var buffer = new TestHtmlContentBuilder();
|
|
||||||
var writer = new HtmlContentWrapperTextWriter(buffer, Encoding.UTF8);
|
|
||||||
var content = new HtmlString("Hello, world!");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
writer.Write(content);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Collection(
|
|
||||||
buffer.Values,
|
|
||||||
item => Assert.Same(content, item));
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TestHtmlContentBuilder : IHtmlContentBuilder
|
|
||||||
{
|
|
||||||
public List<object> Values { get; } = new List<object>();
|
|
||||||
|
|
||||||
public IHtmlContentBuilder Append(string unencoded)
|
|
||||||
{
|
|
||||||
Values.Add(unencoded);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IHtmlContentBuilder AppendHtml(IHtmlContent content)
|
|
||||||
{
|
|
||||||
Values.Add(content);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IHtmlContentBuilder AppendHtml(string encoded)
|
|
||||||
{
|
|
||||||
Values.Add(new HtmlString(encoded));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IHtmlContentBuilder Clear()
|
|
||||||
{
|
|
||||||
Values.Clear();
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void WriteTo(TextWriter writer, HtmlEncoder encoder)
|
|
||||||
{
|
|
||||||
throw new NotSupportedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -9,13 +9,13 @@ using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
{
|
{
|
||||||
public class ViewBufferTextWriterTest
|
public class PagedBufferedStringWriterTest
|
||||||
{
|
{
|
||||||
private static readonly char[] Content;
|
private static readonly char[] Content;
|
||||||
|
|
||||||
static ViewBufferTextWriterTest()
|
static PagedBufferedStringWriterTest()
|
||||||
{
|
{
|
||||||
Content = new char[4 * ViewBufferTextWriter.PageSize];
|
Content = new char[4 * PagedBufferedTextWriter.PageSize];
|
||||||
for (var i = 0; i < Content.Length; i++)
|
for (var i = 0; i < Content.Length; i++)
|
||||||
{
|
{
|
||||||
Content[i] = (char)((i % 26) + 'A');
|
Content[i] = (char)((i % 26) + 'A');
|
||||||
|
|
@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
var pool = new TestArrayPool();
|
var pool = new TestArrayPool();
|
||||||
var inner = new StringWriter();
|
var inner = new StringWriter();
|
||||||
|
|
||||||
var writer = new ViewBufferTextWriter(pool, inner);
|
var writer = new PagedBufferedTextWriter(pool, inner);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
for (var i = 0; i < Content.Length; i++)
|
for (var i = 0; i < Content.Length; i++)
|
||||||
|
|
@ -50,7 +50,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
var pool = new TestArrayPool();
|
var pool = new TestArrayPool();
|
||||||
var inner = new StringWriter();
|
var inner = new StringWriter();
|
||||||
|
|
||||||
var writer = new ViewBufferTextWriter(pool, inner);
|
var writer = new PagedBufferedTextWriter(pool, inner);
|
||||||
|
|
||||||
// These numbers chosen to hit boundary conditions in buffer lengths
|
// These numbers chosen to hit boundary conditions in buffer lengths
|
||||||
Assert.Equal(4096, Content.Length); // Update these numbers if this changes.
|
Assert.Equal(4096, Content.Length); // Update these numbers if this changes.
|
||||||
|
|
@ -89,7 +89,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
var pool = new TestArrayPool();
|
var pool = new TestArrayPool();
|
||||||
var inner = new StringWriter();
|
var inner = new StringWriter();
|
||||||
|
|
||||||
var writer = new ViewBufferTextWriter(pool, inner);
|
var writer = new PagedBufferedTextWriter(pool, inner);
|
||||||
|
|
||||||
// These numbers chosen to hit boundary conditions in buffer lengths
|
// These numbers chosen to hit boundary conditions in buffer lengths
|
||||||
Assert.Equal(4096, Content.Length); // Update these numbers if this changes.
|
Assert.Equal(4096, Content.Length); // Update these numbers if this changes.
|
||||||
|
|
@ -116,7 +116,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
var pool = new TestArrayPool();
|
var pool = new TestArrayPool();
|
||||||
var inner = new StringWriter();
|
var inner = new StringWriter();
|
||||||
|
|
||||||
var writer = new ViewBufferTextWriter(pool, inner);
|
var writer = new PagedBufferedTextWriter(pool, inner);
|
||||||
|
|
||||||
// These numbers chosen to hit boundary conditions in buffer lengths
|
// These numbers chosen to hit boundary conditions in buffer lengths
|
||||||
Assert.Equal(4096, Content.Length); // Update these numbers if this changes.
|
Assert.Equal(4096, Content.Length); // Update these numbers if this changes.
|
||||||
|
|
@ -144,7 +144,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
var pool = new TestArrayPool();
|
var pool = new TestArrayPool();
|
||||||
var inner = new StringWriter();
|
var inner = new StringWriter();
|
||||||
|
|
||||||
var writer = new ViewBufferTextWriter(pool, inner);
|
var writer = new PagedBufferedTextWriter(pool, inner);
|
||||||
|
|
||||||
for (var i = 0; i < Content.Length; i++)
|
for (var i = 0; i < Content.Length; i++)
|
||||||
{
|
{
|
||||||
|
|
@ -9,26 +9,25 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
{
|
{
|
||||||
public class TestViewBufferScope : IViewBufferScope
|
public class TestViewBufferScope : IViewBufferScope
|
||||||
{
|
{
|
||||||
public const int DefaultBufferSize = 128;
|
public IList<ViewBufferValue[]> CreatedBuffers { get; } = new List<ViewBufferValue[]>();
|
||||||
private readonly int _bufferSize;
|
|
||||||
|
|
||||||
public TestViewBufferScope(int bufferSize = DefaultBufferSize)
|
|
||||||
{
|
|
||||||
_bufferSize = bufferSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IList<ViewBufferValue[]> ReturnedBuffers { get; } = new List<ViewBufferValue[]>();
|
public IList<ViewBufferValue[]> ReturnedBuffers { get; } = new List<ViewBufferValue[]>();
|
||||||
|
|
||||||
public ViewBufferValue[] GetSegment() => new ViewBufferValue[_bufferSize];
|
public ViewBufferValue[] GetPage(int size)
|
||||||
|
{
|
||||||
|
var buffer = new ViewBufferValue[size];
|
||||||
|
CreatedBuffers.Add(buffer);
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
public void ReturnSegment(ViewBufferValue[] segment)
|
public void ReturnSegment(ViewBufferValue[] segment)
|
||||||
{
|
{
|
||||||
ReturnedBuffers.Add(segment);
|
ReturnedBuffers.Add(segment);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ViewBufferTextWriter CreateWriter(TextWriter writer)
|
public PagedBufferedTextWriter CreateWriter(TextWriter writer)
|
||||||
{
|
{
|
||||||
return new ViewBufferTextWriter(ArrayPool<char>.Shared, writer);
|
return new PagedBufferedTextWriter(ArrayPool<char>.Shared, writer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
public class ViewBufferTest
|
public class ViewBufferTest
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Append_AddsStringRazorValue()
|
public void Append_AddsEncodingWrapper()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name");
|
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 32);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
buffer.Append("Hello world");
|
buffer.Append("Hello world");
|
||||||
|
|
@ -27,14 +27,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
// Assert
|
// Assert
|
||||||
var page = Assert.Single(buffer.Pages);
|
var page = Assert.Single(buffer.Pages);
|
||||||
Assert.Equal(1, page.Count);
|
Assert.Equal(1, page.Count);
|
||||||
Assert.Equal("Hello world", page.Buffer[0].Value);
|
Assert.IsAssignableFrom<IHtmlContent>(page.Buffer[0].Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Append_AddsHtmlContentRazorValue()
|
public void AppendHtml_AddsHtmlContentRazorValue()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name");
|
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 32);
|
||||||
var content = new HtmlString("hello-world");
|
var content = new HtmlString("hello-world");
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -47,10 +47,10 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AppendHtml_AddsHtmlStringValues()
|
public void AppendHtml_AddsString()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name");
|
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 32);
|
||||||
var value = "Hello world";
|
var value = "Hello world";
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -59,24 +59,23 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
// Assert
|
// Assert
|
||||||
var page = Assert.Single(buffer.Pages);
|
var page = Assert.Single(buffer.Pages);
|
||||||
Assert.Equal(1, page.Count);
|
Assert.Equal(1, page.Count);
|
||||||
var htmlString = Assert.IsType<HtmlString>(page.Buffer[0].Value);
|
Assert.Equal("Hello world", Assert.IsType<string>(page.Buffer[0].Value));
|
||||||
Assert.Equal("Hello world", htmlString.ToString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Append_CreatesNewPages_WhenCurrentPageIsFull()
|
public void Append_CreatesNewPages_WhenCurrentPageIsFull()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name");
|
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 32);
|
||||||
var expected = Enumerable.Range(0, TestViewBufferScope.DefaultBufferSize).Select(i => i.ToString());
|
var expected = Enumerable.Range(0, 32).Select(i => i.ToString());
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
foreach (var item in expected)
|
foreach (var item in expected)
|
||||||
{
|
{
|
||||||
buffer.Append(item);
|
buffer.AppendHtml(item);
|
||||||
}
|
}
|
||||||
buffer.Append("Hello");
|
buffer.AppendHtml("Hello");
|
||||||
buffer.Append("world");
|
buffer.AppendHtml("world");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(2, buffer.Pages.Count);
|
Assert.Equal(2, buffer.Pages.Count);
|
||||||
|
|
@ -92,19 +91,19 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(1)]
|
[InlineData(1)]
|
||||||
[InlineData(TestViewBufferScope.DefaultBufferSize + 3)]
|
[InlineData(35)]
|
||||||
public void Clear_ResetsBackingBufferAndIndex(int valuesToWrite)
|
public void Clear_ResetsBackingBufferAndIndex(int valuesToWrite)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name");
|
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 32);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
for (var i = 0; i < valuesToWrite; i++)
|
for (var i = 0; i < valuesToWrite; i++)
|
||||||
{
|
{
|
||||||
buffer.Append("Hello");
|
buffer.AppendHtml("Hello");
|
||||||
}
|
}
|
||||||
buffer.Clear();
|
buffer.Clear();
|
||||||
buffer.Append("world");
|
buffer.AppendHtml("world");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
var page = Assert.Single(buffer.Pages);
|
var page = Assert.Single(buffer.Pages);
|
||||||
|
|
@ -112,27 +111,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
Assert.Equal("world", page.Buffer[0].Value);
|
Assert.Equal("world", page.Buffer[0].Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void WriteTo_WritesSelf_WhenWriterIsHtmlTextWriter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name");
|
|
||||||
var htmlWriter = new Mock<HtmlTextWriter>();
|
|
||||||
htmlWriter.Setup(w => w.Write(buffer)).Verifiable();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
buffer.Append("Hello world");
|
|
||||||
buffer.WriteTo(htmlWriter.Object, new HtmlTestEncoder());
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
htmlWriter.Verify();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void WriteTo_WritesRazorValues_ToTextWriter()
|
public void WriteTo_WritesRazorValues_ToTextWriter()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name");
|
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 32);
|
||||||
var writer = new StringWriter();
|
var writer = new StringWriter();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -142,7 +125,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
buffer.WriteTo(writer, new HtmlTestEncoder());
|
buffer.WriteTo(writer, new HtmlTestEncoder());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("Hello world 123", writer.ToString());
|
Assert.Equal("HtmlEncode[[Hello]] world 123", writer.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -153,7 +136,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
public void WriteTo_WritesRazorValuesFromAllBuffers(int valuesToWrite)
|
public void WriteTo_WritesRazorValuesFromAllBuffers(int valuesToWrite)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var buffer = new ViewBuffer(new TestViewBufferScope(4), "some-name");
|
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4);
|
||||||
var writer = new StringWriter();
|
var writer = new StringWriter();
|
||||||
var expected = string.Join("", Enumerable.Range(0, valuesToWrite).Select(_ => "abc"));
|
var expected = string.Join("", Enumerable.Range(0, valuesToWrite).Select(_ => "abc"));
|
||||||
|
|
||||||
|
|
@ -168,27 +151,11 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
Assert.Equal(expected, writer.ToString());
|
Assert.Equal(expected, writer.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task WriteToAsync_WritesSelf_WhenWriterIsHtmlTextWriter()
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name");
|
|
||||||
var htmlWriter = new Mock<HtmlTextWriter>();
|
|
||||||
htmlWriter.Setup(w => w.Write(buffer)).Verifiable();
|
|
||||||
|
|
||||||
// Act
|
|
||||||
buffer.Append("Hello world");
|
|
||||||
await buffer.WriteToAsync(htmlWriter.Object, new HtmlTestEncoder());
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
htmlWriter.Verify();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task WriteToAsync_WritesRazorValues_ToTextWriter()
|
public async Task WriteToAsync_WritesRazorValues_ToTextWriter()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name");
|
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 128);
|
||||||
var writer = new StringWriter();
|
var writer = new StringWriter();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -199,7 +166,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
await buffer.WriteToAsync(writer, new HtmlTestEncoder());
|
await buffer.WriteToAsync(writer, new HtmlTestEncoder());
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("Hello world 123", writer.ToString());
|
Assert.Equal("HtmlEncode[[Hello]] world 123", writer.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
|
|
@ -210,7 +177,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
public async Task WriteToAsync_WritesRazorValuesFromAllBuffers(int valuesToWrite)
|
public async Task WriteToAsync_WritesRazorValuesFromAllBuffers(int valuesToWrite)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var buffer = new ViewBuffer(new TestViewBufferScope(4), "some-name");
|
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4);
|
||||||
var writer = new StringWriter();
|
var writer = new StringWriter();
|
||||||
var expected = string.Join("", Enumerable.Range(0, valuesToWrite).Select(_ => "abc"));
|
var expected = string.Join("", Enumerable.Range(0, valuesToWrite).Select(_ => "abc"));
|
||||||
|
|
||||||
|
|
@ -227,20 +194,66 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AppendHtml_ViewBuffer_TakesPage_IfOriginalIsEmpty()
|
public void CopyTo_Flattens()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var scope = new TestViewBufferScope(4);
|
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4);
|
||||||
|
|
||||||
var original = new ViewBuffer(scope, "original");
|
var nestedItems = new List<object>();
|
||||||
var other = new ViewBuffer(scope, "other");
|
var nested = new HtmlContentBuilder(nestedItems);
|
||||||
|
nested.AppendHtml("Hello");
|
||||||
|
buffer.AppendHtml(nested);
|
||||||
|
|
||||||
other.Append("Hi");
|
var destinationItems = new List<object>();
|
||||||
|
var destination = new HtmlContentBuilder(destinationItems);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
buffer.CopyTo(destination);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Same(nested, buffer.Pages[0].Buffer[0].Value);
|
||||||
|
Assert.Equal("Hello", Assert.IsType<HtmlEncodedString>(nestedItems[0]).Value);
|
||||||
|
Assert.Equal("Hello", Assert.IsType<HtmlEncodedString>(destinationItems[0]).Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MoveTo_FlattensAndClears()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4);
|
||||||
|
|
||||||
|
var nestedItems = new List<object>();
|
||||||
|
var nested = new HtmlContentBuilder(nestedItems);
|
||||||
|
nested.AppendHtml("Hello");
|
||||||
|
buffer.AppendHtml(nested);
|
||||||
|
|
||||||
|
var destinationItems = new List<object>();
|
||||||
|
var destination = new HtmlContentBuilder(destinationItems);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
buffer.MoveTo(destination);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Empty(nestedItems);
|
||||||
|
Assert.Empty(buffer.Pages);
|
||||||
|
Assert.Equal("Hello", Assert.IsType<HtmlEncodedString>(destinationItems[0]).Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MoveTo_ViewBuffer_TakesPage_IfOriginalIsEmpty()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var scope = new TestViewBufferScope();
|
||||||
|
|
||||||
|
var original = new ViewBuffer(scope, "original", pageSize: 4);
|
||||||
|
var other = new ViewBuffer(scope, "other", pageSize: 4);
|
||||||
|
|
||||||
|
other.AppendHtml("Hi");
|
||||||
|
|
||||||
var page = other.Pages[0];
|
var page = other.Pages[0];
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
original.AppendHtml(other);
|
other.MoveTo(original);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(other.Pages); // Page was taken
|
Assert.Empty(other.Pages); // Page was taken
|
||||||
|
|
@ -248,25 +261,25 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AppendHtml_ViewBuffer_TakesPage_IfCurrentPageInOriginalIsFull()
|
public void MoveTo_ViewBuffer_TakesPage_IfCurrentPageInOriginalIsFull()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var scope = new TestViewBufferScope(4);
|
var scope = new TestViewBufferScope();
|
||||||
|
|
||||||
var original = new ViewBuffer(scope, "original");
|
var original = new ViewBuffer(scope, "original", pageSize: 4);
|
||||||
var other = new ViewBuffer(scope, "other");
|
var other = new ViewBuffer(scope, "other", pageSize: 4);
|
||||||
|
|
||||||
for (var i = 0; i < 4; i++)
|
for (var i = 0; i < 4; i++)
|
||||||
{
|
{
|
||||||
original.Append($"original-{i}");
|
original.AppendHtml($"original-{i}");
|
||||||
}
|
}
|
||||||
|
|
||||||
other.Append("Hi");
|
other.AppendHtml("Hi");
|
||||||
|
|
||||||
var page = other.Pages[0];
|
var page = other.Pages[0];
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
original.AppendHtml(other);
|
other.MoveTo(original);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(other.Pages); // Page was taken
|
Assert.Empty(other.Pages); // Page was taken
|
||||||
|
|
@ -275,30 +288,30 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AppendHtml_ViewBuffer_TakesPage_IfCurrentPageDoesNotHaveCapacity()
|
public void MoveTo_ViewBuffer_TakesPage_IfCurrentPageDoesNotHaveCapacity()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var scope = new TestViewBufferScope(4);
|
var scope = new TestViewBufferScope();
|
||||||
|
|
||||||
var original = new ViewBuffer(scope, "original");
|
var original = new ViewBuffer(scope, "original", pageSize: 4);
|
||||||
var other = new ViewBuffer(scope, "other");
|
var other = new ViewBuffer(scope, "other", pageSize: 4);
|
||||||
|
|
||||||
for (var i = 0; i < 3; i++)
|
for (var i = 0; i < 3; i++)
|
||||||
{
|
{
|
||||||
original.Append($"original-{i}");
|
original.AppendHtml($"original-{i}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// With two items, we'd try to copy the items, but there's no room in the current page.
|
// With two items, we'd try to copy the items, but there's no room in the current page.
|
||||||
// So we just take over the page.
|
// So we just take over the page.
|
||||||
for (var i = 0; i < 2; i++)
|
for (var i = 0; i < 2; i++)
|
||||||
{
|
{
|
||||||
other.Append($"other-{i}");
|
other.AppendHtml($"other-{i}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var page = other.Pages[0];
|
var page = other.Pages[0];
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
original.AppendHtml(other);
|
other.MoveTo(original);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(other.Pages); // Page was taken
|
Assert.Empty(other.Pages); // Page was taken
|
||||||
|
|
@ -307,29 +320,29 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AppendHtml_ViewBuffer_CopiesItems_IfCurrentPageHasRoom()
|
public void MoveTo_ViewBuffer_CopiesItems_IfCurrentPageHasRoom()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var scope = new TestViewBufferScope(4);
|
var scope = new TestViewBufferScope();
|
||||||
|
|
||||||
var original = new ViewBuffer(scope, "original");
|
var original = new ViewBuffer(scope, "original", pageSize: 4);
|
||||||
var other = new ViewBuffer(scope, "other");
|
var other = new ViewBuffer(scope, "other", pageSize: 4);
|
||||||
|
|
||||||
for (var i = 0; i < 2; i++)
|
for (var i = 0; i < 2; i++)
|
||||||
{
|
{
|
||||||
original.Append($"original-{i}");
|
original.AppendHtml($"original-{i}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// With two items, this is half full so we try to copy the items.
|
// With two items, this is half full so we try to copy the items.
|
||||||
for (var i = 0; i < 2; i++)
|
for (var i = 0; i < 2; i++)
|
||||||
{
|
{
|
||||||
other.Append($"other-{i}");
|
other.AppendHtml($"other-{i}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var page = other.Pages[0];
|
var page = other.Pages[0];
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
original.AppendHtml(other);
|
other.MoveTo(original);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(other.Pages); // Other is cleared
|
Assert.Empty(other.Pages); // Other is cleared
|
||||||
|
|
@ -344,30 +357,30 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AppendHtml_ViewBuffer_CanAddToTakenPage()
|
public void MoveTo_ViewBuffer_CanAddToTakenPage()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var scope = new TestViewBufferScope(4);
|
var scope = new TestViewBufferScope();
|
||||||
|
|
||||||
var original = new ViewBuffer(scope, "original");
|
var original = new ViewBuffer(scope, "original", pageSize: 4);
|
||||||
var other = new ViewBuffer(scope, "other");
|
var other = new ViewBuffer(scope, "other", pageSize: 4);
|
||||||
|
|
||||||
for (var i = 0; i < 3; i++)
|
for (var i = 0; i < 3; i++)
|
||||||
{
|
{
|
||||||
original.Append($"original-{i}");
|
original.AppendHtml($"original-{i}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// More than half full, so we take the page
|
// More than half full, so we take the page
|
||||||
for (var i = 0; i < 3; i++)
|
for (var i = 0; i < 3; i++)
|
||||||
{
|
{
|
||||||
other.Append($"other-{i}");
|
other.AppendHtml($"other-{i}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var page = other.Pages[0];
|
var page = other.Pages[0];
|
||||||
original.AppendHtml(other);
|
other.MoveTo(original);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
original.Append("after-merge");
|
original.AppendHtml("after-merge");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(other.Pages); // Other is cleared
|
Assert.Empty(other.Pages); // Other is cleared
|
||||||
|
|
@ -388,28 +401,28 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AppendHtml_ViewBuffer_MultiplePages()
|
public void MoveTo_ViewBuffer_MultiplePages()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var scope = new TestViewBufferScope(4);
|
var scope = new TestViewBufferScope();
|
||||||
|
|
||||||
var original = new ViewBuffer(scope, "original");
|
var original = new ViewBuffer(scope, "original", pageSize: 4);
|
||||||
var other = new ViewBuffer(scope, "other");
|
var other = new ViewBuffer(scope, "other", pageSize: 4);
|
||||||
|
|
||||||
for (var i = 0; i < 2; i++)
|
for (var i = 0; i < 2; i++)
|
||||||
{
|
{
|
||||||
original.Append($"original-{i}");
|
original.AppendHtml($"original-{i}");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var i = 0; i < 9; i++)
|
for (var i = 0; i < 9; i++)
|
||||||
{
|
{
|
||||||
other.Append($"other-{i}");
|
other.AppendHtml($"other-{i}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var pages = new List<ViewBufferPage>(other.Pages);
|
var pages = new List<ViewBufferPage>(other.Pages);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
original.AppendHtml(other);
|
other.MoveTo(original);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Empty(other.Pages); // Other is cleared
|
Assert.Empty(other.Pages); // Other is cleared
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,14 @@ using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
|
|
||||||
using Microsoft.AspNetCore.Testing;
|
using Microsoft.AspNetCore.Testing;
|
||||||
using Microsoft.Extensions.WebEncoders.Testing;
|
using Microsoft.Extensions.WebEncoders.Testing;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
||||||
{
|
{
|
||||||
public class RazorTextWriterTest
|
public class ViewBufferTextWriterTest
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
[ReplaceCulture]
|
[ReplaceCulture]
|
||||||
|
|
@ -24,8 +23,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = new object[] { "True", "3", "18446744073709551615", "Hello world", "3.14", "2.718", "m" };
|
var expected = new object[] { "True", "3", "18446744073709551615", "Hello world", "3.14", "2.718", "m" };
|
||||||
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name");
|
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4);
|
||||||
var writer = new RazorTextWriter(TextWriter.Null, buffer, new HtmlTestEncoder());
|
var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
writer.Write(true);
|
writer.Write(true);
|
||||||
|
|
@ -46,10 +45,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var expected = new[] { "True", "3", "18446744073709551615", "Hello world", "3.14", "2.718" };
|
var expected = new[] { "True", "3", "18446744073709551615", "Hello world", "3.14", "2.718" };
|
||||||
var unbufferedWriter = new Mock<TextWriter>();
|
var inner = new Mock<TextWriter>();
|
||||||
unbufferedWriter.SetupGet(w => w.Encoding).Returns(Encoding.UTF8);
|
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4);
|
||||||
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name");
|
var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8, new HtmlTestEncoder(), inner.Object);
|
||||||
var writer = new RazorTextWriter(unbufferedWriter.Object, buffer, new HtmlTestEncoder());
|
|
||||||
var testClass = new TestClass();
|
var testClass = new TestClass();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -65,7 +63,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
Assert.Empty(buffer.Pages);
|
Assert.Empty(buffer.Pages);
|
||||||
foreach (var item in expected)
|
foreach (var item in expected)
|
||||||
{
|
{
|
||||||
unbufferedWriter.Verify(v => v.Write(item), Times.Once());
|
inner.Verify(v => v.Write(item), Times.Once());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,10 +72,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
public async Task Write_WritesCharValues_ToUnderlyingStream_WhenNotBuffering()
|
public async Task Write_WritesCharValues_ToUnderlyingStream_WhenNotBuffering()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var unbufferedWriter = new Mock<TextWriter> { CallBase = true };
|
var inner = new Mock<TextWriter> { CallBase = true };
|
||||||
unbufferedWriter.SetupGet(w => w.Encoding).Returns(Encoding.UTF8);
|
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4);
|
||||||
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name");
|
var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8, new HtmlTestEncoder(), inner.Object);
|
||||||
var writer = new RazorTextWriter(unbufferedWriter.Object, buffer, new HtmlTestEncoder());
|
|
||||||
var buffer1 = new[] { 'a', 'b', 'c', 'd' };
|
var buffer1 = new[] { 'a', 'b', 'c', 'd' };
|
||||||
var buffer2 = new[] { 'd', 'e', 'f' };
|
var buffer2 = new[] { 'd', 'e', 'f' };
|
||||||
|
|
||||||
|
|
@ -90,12 +87,12 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
await writer.WriteLineAsync(buffer1);
|
await writer.WriteLineAsync(buffer1);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
unbufferedWriter.Verify(v => v.Write('x'), Times.Once());
|
inner.Verify(v => v.Write('x'), Times.Once());
|
||||||
unbufferedWriter.Verify(v => v.Write(buffer1, 1, 2), Times.Once());
|
inner.Verify(v => v.Write(buffer1, 1, 2), Times.Once());
|
||||||
unbufferedWriter.Verify(v => v.Write(buffer1, 0, 4), Times.Once());
|
inner.Verify(v => v.Write(buffer1, 0, 4), Times.Once());
|
||||||
unbufferedWriter.Verify(v => v.Write(buffer2, 0, 3), Times.Once());
|
inner.Verify(v => v.Write(buffer2, 0, 3), Times.Once());
|
||||||
unbufferedWriter.Verify(v => v.WriteAsync(buffer2, 1, 1), Times.Once());
|
inner.Verify(v => v.WriteAsync(buffer2, 1, 1), Times.Once());
|
||||||
unbufferedWriter.Verify(v => v.WriteLine(), Times.Once());
|
inner.Verify(v => v.WriteLine(), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -103,10 +100,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
public async Task Write_WritesStringValues_ToUnbufferedStream_WhenNotBuffering()
|
public async Task Write_WritesStringValues_ToUnbufferedStream_WhenNotBuffering()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var unbufferedWriter = new Mock<TextWriter>();
|
var inner = new Mock<TextWriter>();
|
||||||
unbufferedWriter.SetupGet(w => w.Encoding).Returns(Encoding.UTF8);
|
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4);
|
||||||
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name");
|
var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8, new HtmlTestEncoder(), inner.Object);
|
||||||
var writer = new RazorTextWriter(unbufferedWriter.Object, buffer, new HtmlTestEncoder());
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
await writer.FlushAsync();
|
await writer.FlushAsync();
|
||||||
|
|
@ -116,10 +112,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
await writer.WriteLineAsync("gh");
|
await writer.WriteLineAsync("gh");
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
unbufferedWriter.Verify(v => v.Write("a"), Times.Once());
|
inner.Verify(v => v.Write("a"), Times.Once());
|
||||||
unbufferedWriter.Verify(v => v.WriteLine("ab"), Times.Once());
|
inner.Verify(v => v.WriteLine("ab"), Times.Once());
|
||||||
unbufferedWriter.Verify(v => v.WriteAsync("ef"), Times.Once());
|
inner.Verify(v => v.WriteAsync("ef"), Times.Once());
|
||||||
unbufferedWriter.Verify(v => v.WriteLineAsync("gh"), Times.Once());
|
inner.Verify(v => v.WriteLineAsync("gh"), Times.Once());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -129,8 +125,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
// Arrange
|
// Arrange
|
||||||
var newLine = Environment.NewLine;
|
var newLine = Environment.NewLine;
|
||||||
var expected = new List<object> { "False", newLine, "1.1", newLine, "3", newLine };
|
var expected = new List<object> { "False", newLine, "1.1", newLine, "3", newLine };
|
||||||
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name");
|
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4);
|
||||||
var writer = new RazorTextWriter(TextWriter.Null, buffer, new HtmlTestEncoder());
|
var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
writer.WriteLine(false);
|
writer.WriteLine(false);
|
||||||
|
|
@ -146,10 +142,9 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
public void WriteLine_WritesDataTypes_ToUnbufferedStream_WhenNotBuffering()
|
public void WriteLine_WritesDataTypes_ToUnbufferedStream_WhenNotBuffering()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var unbufferedWriter = new Mock<TextWriter>();
|
var inner = new Mock<TextWriter>();
|
||||||
unbufferedWriter.SetupGet(w => w.Encoding).Returns(Encoding.UTF8);
|
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4);
|
||||||
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name");
|
var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8, new HtmlTestEncoder(), inner.Object);
|
||||||
var writer = new RazorTextWriter(unbufferedWriter.Object, buffer, new HtmlTestEncoder());
|
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
writer.Flush();
|
writer.Flush();
|
||||||
|
|
@ -158,10 +153,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
writer.WriteLine(3L);
|
writer.WriteLine(3L);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
unbufferedWriter.Verify(v => v.Write("False"), Times.Once());
|
inner.Verify(v => v.Write("False"), Times.Once());
|
||||||
unbufferedWriter.Verify(v => v.Write("1.1"), Times.Once());
|
inner.Verify(v => v.Write("1.1"), Times.Once());
|
||||||
unbufferedWriter.Verify(v => v.Write("3"), Times.Once());
|
inner.Verify(v => v.Write("3"), Times.Once());
|
||||||
unbufferedWriter.Verify(v => v.WriteLine(), Times.Exactly(3));
|
inner.Verify(v => v.WriteLine(), Times.Exactly(3));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -169,8 +164,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var newLine = Environment.NewLine;
|
var newLine = Environment.NewLine;
|
||||||
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name");
|
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4);
|
||||||
var writer = new RazorTextWriter(TextWriter.Null, buffer, new HtmlTestEncoder());
|
var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
writer.WriteLine();
|
writer.WriteLine();
|
||||||
|
|
@ -190,8 +185,8 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
var input2 = "from";
|
var input2 = "from";
|
||||||
var input3 = "ASP";
|
var input3 = "ASP";
|
||||||
var input4 = ".Net";
|
var input4 = ".Net";
|
||||||
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name");
|
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4);
|
||||||
var writer = new RazorTextWriter(TextWriter.Null, buffer, new HtmlTestEncoder());
|
var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
writer.Write(input1);
|
writer.Write(input1);
|
||||||
|
|
@ -208,9 +203,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
public void Write_HtmlContent_AfterFlush_GoesToStream()
|
public void Write_HtmlContent_AfterFlush_GoesToStream()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var stringWriter = new StringWriter();
|
var inner = new StringWriter();
|
||||||
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name");
|
var buffer = new ViewBuffer(new TestViewBufferScope(), "some-name", pageSize: 4);
|
||||||
var writer = new RazorTextWriter(stringWriter, buffer, new HtmlTestEncoder());
|
var writer = new ViewBufferTextWriter(buffer, Encoding.UTF8, new HtmlTestEncoder(), inner);
|
||||||
|
|
||||||
writer.Flush();
|
writer.Flush();
|
||||||
|
|
||||||
var content = new HtmlString("Hello, world!");
|
var content = new HtmlString("Hello, world!");
|
||||||
|
|
@ -219,7 +215,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Test
|
||||||
writer.Write(content);
|
writer.Write(content);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("Hello, world!", stringWriter.ToString());
|
Assert.Equal("Hello, world!", inner.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object[] GetValues(ViewBuffer buffer)
|
private static object[] GetValues(ViewBuffer buffer)
|
||||||
|
|
@ -60,7 +60,7 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
|
||||||
htmlHelperOptions: new HtmlHelperOptions());
|
htmlHelperOptions: new HtmlHelperOptions());
|
||||||
var view = Mock.Of<IView>();
|
var view = Mock.Of<IView>();
|
||||||
var viewData = new ViewDataDictionary(originalContext.ViewData);
|
var viewData = new ViewDataDictionary(originalContext.ViewData);
|
||||||
var writer = new HtmlContentWrapperTextWriter(new HtmlContentBuilder(), Encoding.UTF8);
|
var writer = new StringWriter();
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var context = new ViewContext(originalContext, view, viewData, writer);
|
var context = new ViewContext(originalContext, view, viewData, writer);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue