From 284eb9ac6a4f69f80f2c767a6d35f40dbd091131 Mon Sep 17 00:00:00 2001 From: sornaks Date: Mon, 23 Feb 2015 14:35:12 -0800 Subject: [PATCH] Reacting to Razor changes - TagHelperOutput is writing to TextWriter instead of string. --- .../Rendering/BufferEntryCollection.cs | 177 ------------------ .../Rendering/StringCollectionTextWriter.cs | 3 +- src/Microsoft.AspNet.Mvc.Core/project.json | 1 + .../MvcRazorHost.cs | 4 +- src/Microsoft.AspNet.Mvc.Razor/RazorPage.cs | 87 ++++++++- .../CacheTagHelper.cs | 5 +- .../FormTagHelper.cs | 4 +- .../InputTagHelper.cs | 6 +- .../LabelTagHelper.cs | 8 +- .../LinkTagHelper.cs | 24 +-- .../OptionTagHelper.cs | 13 +- .../ScriptTagHelper.cs | 31 ++- .../SelectTagHelper.cs | 2 +- .../TextAreaTagHelper.cs | 2 +- .../ValidationMessageTagHelper.cs | 8 +- .../ValidationSummaryTagHelper.cs | 3 +- .../Rendering/BufferEntryCollectionTest.cs | 160 ---------------- .../Runtime/ModelExpressionTagHelper.cs | 26 +-- .../RazorPageTest.cs | 131 ++++++++++++- .../AnchorTagHelperTest.cs | 45 +++-- .../CacheTagHelperTest.cs | 173 ++++++++--------- .../EnvironmentTagHelperTest.cs | 17 +- .../FormTagHelperTest.cs | 84 ++++++--- .../InputTagHelperTest.cs | 141 +++++++++----- .../Internal/AttributeMatcherTest.cs | 7 +- .../LabelTagHelperTest.cs | 48 ++--- .../LinkTagHelperTest.cs | 20 +- .../OptionTagHelperTest.cs | 39 ++-- .../ScriptTagHelperTest.cs | 25 ++- .../SelectTagHelperTest.cs | 83 +++++--- .../TagHelperOutputExtensionsTest.cs | 14 +- .../TextAreaTagHelperTest.cs | 26 ++- .../ValidationMessageTagHelperTest.cs | 88 +++++---- .../ValidationSummaryTagHelperTest.cs | 89 +++++---- .../TagHelpers/FooterTagHelper.cs | 2 +- .../TagHelpers/HiddenTagHelper.cs | 2 +- .../TagHelpers/RepeatContentTagHelper.cs | 4 +- .../TagHelpers/TitleTagHelper.cs | 2 +- .../RequestScopedTagHelper.cs | 2 +- .../TagHelpers/ATagHelper.cs | 2 +- .../TagHelpers/AutoLinkerTagHelper.cs | 7 +- .../TagHelpers/NestedViewStartTagHelper.cs | 2 +- .../TagHelpers/RootViewStartTagHelper.cs | 2 +- .../TagCloudViewComponentTagHelper.cs | 3 +- .../TagHelpers/WebsiteInformationTagHelper.cs | 4 +- 45 files changed, 825 insertions(+), 801 deletions(-) delete mode 100644 src/Microsoft.AspNet.Mvc.Core/Rendering/BufferEntryCollection.cs delete mode 100644 test/Microsoft.AspNet.Mvc.Core.Test/Rendering/BufferEntryCollectionTest.cs 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