diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/BufferEntryCollection.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/BufferEntryCollection.cs deleted file mode 100644 index c3c4bae1d5..0000000000 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/BufferEntryCollection.cs +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. 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; -using System.Collections.Generic; -using System.Diagnostics; -using Microsoft.Framework.Internal; - -namespace Microsoft.AspNet.Mvc.Rendering -{ - /// - /// Represents a hierarchy of strings and provides an enumerator that iterates over it as a sequence. - /// - public class BufferEntryCollection : IEnumerable - { - // Specifies the maximum size we'll allow for direct conversion from - // char arrays to string. - private const int MaxCharToStringLength = 1024; - private readonly List _buffer = new List(); - - public IReadOnlyList BufferEntries - { - get { return _buffer; } - } - - /// - /// Adds a string value to the buffer. - /// - /// The value to add. - public void Add(string value) - { - _buffer.Add(value); - } - - /// - /// Adds a subarray of characters to the buffer. - /// - /// The array to add. - /// The character position in the array at which to start copying data. - /// The number of characters to copy. - public void Add([NotNull] char[] value, int index, int count) - { - if (index < 0) - { - throw new ArgumentOutOfRangeException("index"); - } - if (count < 0) - { - throw new ArgumentOutOfRangeException("count"); - } - if (value.Length - index < count) - { - throw new ArgumentOutOfRangeException("count"); - } - - while (count > 0) - { - // Split large char arrays into 1KB strings. - var currentCount = Math.Min(count, MaxCharToStringLength); - Add(new string(value, index, currentCount)); - index += currentCount; - count -= currentCount; - } - } - - /// - /// Adds an instance of to the buffer. - /// - /// The buffer collection to add. - public void Add([NotNull] BufferEntryCollection buffer) - { - _buffer.Add(buffer.BufferEntries); - } - - /// - public IEnumerator GetEnumerator() - { - return new BufferEntryEnumerator(_buffer); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - private sealed class BufferEntryEnumerator : IEnumerator - { - private readonly Stack> _enumerators = new Stack>(); - private readonly List _initialBuffer; - - public BufferEntryEnumerator(List buffer) - { - _initialBuffer = buffer; - Reset(); - } - - public IEnumerator CurrentEnumerator - { - get - { - return _enumerators.Peek(); - } - } - - public string Current - { - get - { - var currentEnumerator = CurrentEnumerator; - Debug.Assert(currentEnumerator != null); - - return (string)currentEnumerator.Current; - } - } - - object IEnumerator.Current - { - get - { - return Current; - } - } - - public bool MoveNext() - { - var currentEnumerator = CurrentEnumerator; - if (currentEnumerator.MoveNext()) - { - var current = currentEnumerator.Current; - var buffer = current as List; - if (buffer != null) - { - // If the next item is a collection, recursively call in to it. - var enumerator = buffer.GetEnumerator(); - _enumerators.Push(enumerator); - return MoveNext(); - } - - return true; - } - else if (_enumerators.Count > 1) - { - // The current enumerator is exhausted and we have a parent. - // Pop the current enumerator out and continue with it's parent. - var enumerator = _enumerators.Pop(); - enumerator.Dispose(); - - return MoveNext(); - } - - // We've exactly one element in our stack which cannot move next. - return false; - } - - public void Reset() - { - DisposeEnumerators(); - - _enumerators.Push(_initialBuffer.GetEnumerator()); - } - - public void Dispose() - { - DisposeEnumerators(); - } - - private void DisposeEnumerators() - { - while (_enumerators.Count > 0) - { - _enumerators.Pop().Dispose(); - } - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Core/Rendering/StringCollectionTextWriter.cs b/src/Microsoft.AspNet.Mvc.Core/Rendering/StringCollectionTextWriter.cs index 213dbf6700..264b0ead94 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Rendering/StringCollectionTextWriter.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Rendering/StringCollectionTextWriter.cs @@ -40,7 +40,8 @@ namespace Microsoft.AspNet.Mvc.Rendering /// /// A collection of entries buffered by this instance of . /// - public BufferEntryCollection Buffer { get; private set; } + // internal for testing purposes. + internal BufferEntryCollection Buffer { get; } /// public override void Write(char value) diff --git a/src/Microsoft.AspNet.Mvc.Core/project.json b/src/Microsoft.AspNet.Mvc.Core/project.json index afa53fc9c4..db7eb1d073 100644 --- a/src/Microsoft.AspNet.Mvc.Core/project.json +++ b/src/Microsoft.AspNet.Mvc.Core/project.json @@ -15,6 +15,7 @@ "Microsoft.AspNet.Mvc.Common": { "version": "6.0.0-*", "type": "build" }, "Microsoft.AspNet.Mvc.ModelBinding": "6.0.0-*", "Microsoft.AspNet.Routing": "1.0.0-*", + "Microsoft.Framework.BufferEntryCollection.Internal": { "version": "1.0.0-*", "type": "build" }, "Microsoft.Framework.CopyOnWriteDictionary.Internal": { "version": "1.0.0-*", "type": "build" }, "Microsoft.Framework.NotNullAttribute.Internal": { "version": "1.0.0-*", "type": "build" }, "Microsoft.Framework.PropertyActivator.Internal": { "version": "1.0.0-*", "type": "build" }, diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs index a639c4cf62..24472fe115 100644 --- a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs +++ b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs @@ -98,8 +98,8 @@ namespace Microsoft.AspNet.Mvc.Razor // Can't use nameof because RazorPage is not accessible here. CreateTagHelperMethodName = "CreateTagHelper", - StartWritingScopeMethodName = "StartWritingScope", - EndWritingScopeMethodName = "EndWritingScope", + StartTagHelperWritingScopeMethodName = "StartTagHelperWritingScope", + EndTagHelperWritingScopeMethodName = "EndTagHelperWritingScope", HtmlEncoderPropertyName = "HtmlEncoder", }) { diff --git a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs index 7939e032c4..4f48356ce2 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Principal; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc.Rendering; @@ -159,11 +160,11 @@ namespace Microsoft.AspNet.Mvc.Razor /// /// /// All writes to the or after calling this method will - /// be buffered until is called. + /// be buffered until is called. /// - public void StartWritingScope() + public void StartTagHelperWritingScope() { - StartWritingScope(new StringWriter()); + StartTagHelperWritingScope(new StringCollectionTextWriter(Output.Encoding)); } /// @@ -171,9 +172,9 @@ namespace Microsoft.AspNet.Mvc.Razor /// /// /// All writes to the or after calling this method will - /// be buffered until is called. + /// be buffered until is called. /// - public void StartWritingScope(TextWriter writer) + public void StartTagHelperWritingScope(TextWriter writer) { // If there isn't a base writer take the ViewContext.Writer if (_originalWriter == null) @@ -189,11 +190,11 @@ namespace Microsoft.AspNet.Mvc.Razor } /// - /// Ends the current writing scope that was started by calling . + /// Ends the current writing scope that was started by calling . /// /// The that contains the content written to the or /// during the writing scope. - public TextWriter EndWritingScope() + public TagHelperContent EndTagHelperWritingScope() { if (_writerScopes.Count == 0) { @@ -214,7 +215,49 @@ namespace Microsoft.AspNet.Mvc.Razor _originalWriter = null; } - return writer; + var tagHelperContentWrapperTextWriter = new TagHelperContentWrapperTextWriter(Output.Encoding); + var razorWriter = writer as RazorTextWriter; + if (razorWriter != null) + { + razorWriter.CopyTo(tagHelperContentWrapperTextWriter); + } + else + { + var stringCollectionTextWriter = writer as StringCollectionTextWriter; + if (stringCollectionTextWriter != null) + { + stringCollectionTextWriter.CopyTo(tagHelperContentWrapperTextWriter); + } + else + { + tagHelperContentWrapperTextWriter.Write(writer.ToString()); + } + } + + return tagHelperContentWrapperTextWriter.Content; + } + + /// + /// Writes an to the . + /// + /// Contains the data to be written. + public void Write(ITextWriterCopyable copyableTextWriter) + { + WriteTo(Output, copyableTextWriter); + } + + /// + /// Writes an to the . + /// + /// The to which the + /// is written. + /// Contains the data to be written. + public void WriteTo(TextWriter writer, ITextWriterCopyable copyableTextWriter) + { + if (copyableTextWriter != null) + { + copyableTextWriter.CopyTo(writer); + } } /// @@ -649,5 +692,33 @@ namespace Microsoft.AspNet.Mvc.Razor throw new InvalidOperationException(Resources.FormatRazorPage_MethodCannotBeCalled(methodName)); } } + + private class TagHelperContentWrapperTextWriter : TextWriter + { + public TagHelperContentWrapperTextWriter(Encoding encoding) + { + Content = new DefaultTagHelperContent(); + Encoding = encoding; + } + + public TagHelperContent Content { get; } + + public override Encoding Encoding { get; } + + public override void Write(string value) + { + Content.Append(value); + } + + public override void Write(char value) + { + Content.Append(value.ToString()); + } + + public override string ToString() + { + return Content.ToString(); + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/CacheTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/CacheTagHelper.cs index c177c9fa99..27e61389e7 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/CacheTagHelper.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/CacheTagHelper.cs @@ -112,7 +112,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { var key = GenerateKey(context); - string result; + TagHelperContent result; if (!MemoryCache.TryGetValue(key, out result)) { // Create an EntryLink and flow it so that it is accessible via the ambient EntryLinkHelpers.ContentLink @@ -132,8 +132,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers // Clear the contents of the "cache" element since we don't want to render it. output.SuppressOutput(); - - output.Content = result; + output.Content.SetContent(result); } // Internal for unit testing diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/FormTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/FormTagHelper.cs index fe92d7f47f..7584d2b295 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/FormTagHelper.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/FormTagHelper.cs @@ -104,7 +104,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers if (tagBuilder != null) { output.MergeAttributes(tagBuilder); - output.PostContent += tagBuilder.InnerHtml; + output.PostContent.Append(tagBuilder.InnerHtml); } } @@ -113,7 +113,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers var antiForgeryTagBuilder = Generator.GenerateAntiForgery(ViewContext); if (antiForgeryTagBuilder != null) { - output.PostContent += antiForgeryTagBuilder.ToString(TagRenderMode.SelfClosing); + output.PostContent.Append(antiForgeryTagBuilder.ToString(TagRenderMode.SelfClosing)); } } } diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/InputTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/InputTagHelper.cs index 9410f36fff..49bda15739 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/InputTagHelper.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/InputTagHelper.cs @@ -206,7 +206,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers // This TagBuilder contains the one element of interest. Since this is not the "checkbox" // special-case, output is a self-closing element no longer guarunteed. output.MergeAttributes(tagBuilder); - output.Content += tagBuilder.InnerHtml; + output.Content.Append(tagBuilder.InnerHtml); } } } @@ -242,12 +242,12 @@ namespace Microsoft.AspNet.Mvc.TagHelpers output.Attributes.Clear(); output.TagName = null; - output.Content += tagBuilder.ToString(TagRenderMode.SelfClosing); + output.Content.Append(tagBuilder.ToString(TagRenderMode.SelfClosing)); tagBuilder = Generator.GenerateHiddenForCheckbox(ViewContext, modelExplorer, For.Name); if (tagBuilder != null) { - output.Content += tagBuilder.ToString(TagRenderMode.SelfClosing); + output.Content.Append(tagBuilder.ToString(TagRenderMode.SelfClosing)); } } } diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/LabelTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/LabelTagHelper.cs index 9cdebd145e..e691c018b0 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/LabelTagHelper.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/LabelTagHelper.cs @@ -47,18 +47,18 @@ namespace Microsoft.AspNet.Mvc.TagHelpers // We check for whitespace to detect scenarios such as: // - if (!output.ContentSet) + if (!output.IsContentModified) { var childContent = await context.GetChildContentAsync(); - if (string.IsNullOrWhiteSpace(childContent)) + if (childContent.IsWhiteSpace) { // Provide default label text since there was nothing useful in the Razor source. - output.Content = tagBuilder.InnerHtml; + output.Content.SetContent(tagBuilder.InnerHtml); } else { - output.Content = childContent; + output.Content.SetContent(childContent); } } } diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs index 543e863d90..134db5b895 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/LinkTagHelper.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Open Technologies, Inc. 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.Globalization; using System.Linq; @@ -175,7 +176,7 @@ namespace Microsoft.AspNet.Mvc.TagHelpers // NOTE: Values in TagHelperOutput.Attributes are already HtmlEncoded var attributes = new Dictionary(output.Attributes); - var builder = new StringBuilder(); + var builder = new DefaultTagHelperContent(); if (mode == Mode.Fallback && string.IsNullOrEmpty(HrefInclude)) { @@ -194,10 +195,10 @@ namespace Microsoft.AspNet.Mvc.TagHelpers // We've taken over tag rendering, so prevent rendering the outer tag output.TagName = null; - output.Content = builder.ToString(); + output.Content.SetContent(builder); } - private void BuildGlobbedLinkTags(IDictionary attributes, StringBuilder builder) + private void BuildGlobbedLinkTags(IDictionary attributes, TagHelperContent builder) { // Build a tag for each matched href as well as the original one in the source file string staticHref; @@ -213,30 +214,30 @@ namespace Microsoft.AspNet.Mvc.TagHelpers } } - private void BuildFallbackBlock(StringBuilder builder) + private void BuildFallbackBlock(TagHelperContent builder) { EnsureGlobbingUrlBuilder(); var fallbackHrefs = GlobbingUrlBuilder.BuildUrlList(FallbackHref, FallbackHrefInclude, FallbackHrefExclude); if (fallbackHrefs.Any()) { - builder.AppendLine(); + builder.Append(Environment.NewLine); // Build the tag that's used to test for the presence of the stylesheet - builder.AppendFormat( + builder.Append(string.Format( CultureInfo.InvariantCulture, "", - HtmlEncoder.HtmlEncode(FallbackTestClass)); + HtmlEncoder.HtmlEncode(FallbackTestClass))); // Build the "); } } @@ -252,13 +253,14 @@ namespace Microsoft.AspNet.Mvc.TagHelpers } } - private static void BuildLinkTag(IDictionary attributes, StringBuilder builder) + private static void BuildLinkTag(IDictionary attributes, TagHelperContent builder) { builder.Append(""); diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/OptionTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/OptionTagHelper.cs index 507e0ac597..aca4a87b85 100644 --- a/src/Microsoft.AspNet.Mvc.TagHelpers/OptionTagHelper.cs +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/OptionTagHelper.cs @@ -86,8 +86,17 @@ namespace Microsoft.AspNet.Mvc.TagHelpers // Select this