Remove `WebUtility` use and handle `IHtmlContent` return values from view components

- #3571 and part of #3123
- split `ContentViewComponentResult` in two
- add `HtmlEncoder` to `ViewComponentContext`
- remove use of `WebUtility.HtmlEncode()` and `HtmlDecode()`

nits: remove unused `using`s in files I had open
This commit is contained in:
Doug Bunting 2015-11-22 17:05:01 -08:00
parent bbdfbf4cc3
commit eb70b1c28c
15 changed files with 216 additions and 81 deletions

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Security.Claims;
@ -379,7 +378,7 @@ namespace Microsoft.AspNet.Mvc.Razor
}
else
{
// This special case alows us to keep buffering as IHtmlContent until we get to the 'final'
// This special case allows us to keep buffering as IHtmlContent until we get to the 'final'
// TextWriter.
htmlTextWriter.Write(htmlContent);
}

View File

@ -2,9 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.Internal;
namespace Microsoft.AspNet.Mvc.ViewComponents
{
@ -12,20 +11,15 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
/// An <see cref="IViewComponentResult"/> which writes text when executed.
/// </summary>
/// <remarks>
/// <see cref="ContentViewComponentResult"/> always writes HTML encoded text from the
/// <see cref="EncodedContent"/> property.
///
/// When using <see cref="ContentViewComponentResult(string)"/>, the provided content will be HTML
/// encoded and stored in <see cref="EncodedContent"/>.
///
/// To write pre-encoded conent, use <see cref="ContentViewComponentResult(HtmlString)"/>.
/// The provided content will be HTML-encoded when written. To write pre-encoded content, use an
/// <see cref="HtmlContentViewComponentResult"/>.
/// </remarks>
public class ContentViewComponentResult : IViewComponentResult
{
/// <summary>
/// Initializes a new <see cref="ContentViewComponentResult"/>.
/// </summary>
/// <param name="content">Content to write. The content be HTML encoded when output.</param>
/// <param name="content">Content to write. The content will be HTML encoded when written.</param>
public ContentViewComponentResult(string content)
{
if (content == null)
@ -34,25 +28,6 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
}
Content = content;
EncodedContent = new HtmlString(WebUtility.HtmlEncode(content));
}
/// <summary>
/// Initializes a new <see cref="ContentViewComponentResult"/>.
/// </summary>
/// <param name="encodedContent">
/// Content to write. The content is treated as already HTML encoded, and no further encoding
/// will be performed.
/// </param>
public ContentViewComponentResult(HtmlString encodedContent)
{
if (encodedContent == null)
{
throw new ArgumentNullException(nameof(encodedContent));
}
EncodedContent = encodedContent;
Content = WebUtility.HtmlDecode(encodedContent.ToString());
}
/// <summary>
@ -61,12 +36,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
public string Content { get; }
/// <summary>
/// Gets the encoded content.
/// </summary>
public HtmlString EncodedContent { get; }
/// <summary>
/// Writes the <see cref="EncodedContent"/>.
/// Encodes and writes the <see cref="Content"/>.
/// </summary>
/// <param name="context">The <see cref="ViewComponentContext"/>.</param>
public void Execute(ViewComponentContext context)
@ -76,22 +46,19 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
throw new ArgumentNullException(nameof(context));
}
context.Writer.Write(EncodedContent.ToString());
context.HtmlEncoder.Encode(context.Writer, Content);
}
/// <summary>
/// Writes the <see cref="EncodedContent"/>.
/// Encodes and writes the <see cref="Content"/>.
/// </summary>
/// <param name="context">The <see cref="ViewComponentContext"/>.</param>
/// <returns>A completed <see cref="Task"/>.</returns>
public Task ExecuteAsync(ViewComponentContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
Execute(context);
return context.Writer.WriteAsync(EncodedContent.ToString());
return TaskCache.CompletedTask;
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.IO;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNet.Html.Abstractions;
using Microsoft.AspNet.Mvc.Rendering;
@ -14,12 +15,14 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
public class DefaultViewComponentHelper : IViewComponentHelper, ICanHasViewContext
{
private readonly IViewComponentDescriptorCollectionProvider _descriptorProvider;
private readonly HtmlEncoder _htmlEncoder;
private readonly IViewComponentInvokerFactory _invokerFactory;
private readonly IViewComponentSelector _selector;
private ViewContext _viewContext;
public DefaultViewComponentHelper(
IViewComponentDescriptorCollectionProvider descriptorProvider,
HtmlEncoder htmlEncoder,
IViewComponentSelector selector,
IViewComponentInvokerFactory invokerFactory)
{
@ -28,6 +31,11 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
throw new ArgumentNullException(nameof(descriptorProvider));
}
if (htmlEncoder == null)
{
throw new ArgumentNullException(nameof(htmlEncoder));
}
if (selector == null)
{
throw new ArgumentNullException(nameof(selector));
@ -39,6 +47,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
}
_descriptorProvider = descriptorProvider;
_htmlEncoder = htmlEncoder;
_selector = selector;
_invokerFactory = invokerFactory;
}
@ -202,7 +211,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
throw new ArgumentNullException(nameof(descriptor));
}
var context = new ViewComponentContext(descriptor, arguments, _viewContext, writer);
var context = new ViewComponentContext(descriptor, arguments, _htmlEncoder, _viewContext, writer);
var invoker = _invokerFactory.CreateInstance(context);
if (invoker == null)
@ -229,7 +238,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
throw new ArgumentNullException(nameof(descriptor));
}
var context = new ViewComponentContext(descriptor, arguments, _viewContext, writer);
var context = new ViewComponentContext(descriptor, arguments, _htmlEncoder, _viewContext, writer);
var invoker = _invokerFactory.CreateInstance(context);
if (invoker == null)

View File

@ -6,10 +6,10 @@ using System.Diagnostics;
using System.Reflection;
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
using Microsoft.AspNet.Html.Abstractions;
using Microsoft.AspNet.Mvc.Controllers;
using Microsoft.AspNet.Mvc.Diagnostics;
using Microsoft.AspNet.Mvc.Infrastructure;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Mvc.ViewFeatures.Logging;
using Microsoft.Extensions.Logging;
@ -175,8 +175,6 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
var component = CreateComponent(context);
object result = null;
using (_logger.ViewComponentScope(context))
{
_diagnosticSource.BeforeViewComponent(context, component);
@ -185,7 +183,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
try
{
var startTime = Environment.TickCount;
result = method.Invoke(component, context.Arguments);
var result = method.Invoke(component, context.Arguments);
var viewComponentResult = CoerceToViewComponentResult(result);
_logger.ViewComponentExecuted(context, startTime, viewComponentResult);
@ -222,15 +220,15 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
return new ContentViewComponentResult(stringResult);
}
var htmlStringResult = value as HtmlString;
if (htmlStringResult != null)
var htmlContent = value as IHtmlContent;
if (htmlContent != null)
{
return new ContentViewComponentResult(htmlStringResult);
return new HtmlContentViewComponentResult(htmlContent);
}
throw new InvalidOperationException(Resources.FormatViewComponent_InvalidReturnValue(
typeof(string).Name,
typeof(HtmlString).Name,
typeof(IHtmlContent).Name,
typeof(IViewComponentResult).Name));
}
}

View File

@ -0,0 +1,72 @@
// 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.Threading.Tasks;
using Microsoft.AspNet.Html.Abstractions;
using Microsoft.AspNet.Mvc.Internal;
namespace Microsoft.AspNet.Mvc.ViewComponents
{
/// <summary>
/// An <see cref="IViewComponentResult"/> which writes an <see cref="IHtmlContent"/> when executed.
/// </summary>
/// <remarks>
/// The provided content will be HTML-encoded as specified when the content was created. To encoded and write
/// text, use a <see cref="ContentViewComponentResult"/>.
/// </remarks>
public class HtmlContentViewComponentResult : IViewComponentResult
{
/// <summary>
/// Initializes a new <see cref="HtmlContentViewComponentResult"/>.
/// </summary>
public HtmlContentViewComponentResult(IHtmlContent encodedContent)
{
if (encodedContent == null)
{
throw new ArgumentNullException(nameof(encodedContent));
}
EncodedContent = encodedContent;
}
/// <summary>
/// Gets the encoded content.
/// </summary>
public IHtmlContent EncodedContent { get; }
/// <summary>
/// Writes the <see cref="EncodedContent"/>.
/// </summary>
/// <param name="context">The <see cref="ViewComponentContext"/>.</param>
public void Execute(ViewComponentContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var htmlWriter = context.Writer as HtmlTextWriter;
if (htmlWriter == null)
{
EncodedContent.WriteTo(context.Writer, context.HtmlEncoder);
}
else
{
htmlWriter.Write(EncodedContent);
}
}
/// <summary>
/// Writes the <see cref="EncodedContent"/>.
/// </summary>
/// <param name="context">The <see cref="ViewComponentContext"/>.</param>
/// <returns>A completed <see cref="Task"/>.</returns>
public Task ExecuteAsync(ViewComponentContext context)
{
Execute(context);
return TaskCache.CompletedTask;
}
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.IO;
using System.Text.Encodings.Web;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.ViewFeatures;
@ -38,6 +39,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
public ViewComponentContext(
ViewComponentDescriptor viewComponentDescriptor,
object[] arguments,
HtmlEncoder htmlEncoder,
ViewContext viewContext,
TextWriter writer)
{
@ -51,6 +53,11 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
throw new ArgumentNullException(nameof(arguments));
}
if (htmlEncoder == null)
{
throw new ArgumentNullException(nameof(htmlEncoder));
}
if (viewContext == null)
{
throw new ArgumentNullException(nameof(viewContext));
@ -63,6 +70,7 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
ViewComponentDescriptor = viewComponentDescriptor;
Arguments = arguments;
HtmlEncoder = htmlEncoder;
// We want to create a defensive copy of the VDD here so that changes done in the VC
// aren't visible in the calling view.
@ -74,13 +82,21 @@ namespace Microsoft.AspNet.Mvc.ViewComponents
}
/// <summary>
/// Gets or sets the View Component arguments.
/// Gets or sets the View Component arguments.
/// </summary>
/// <remarks>
/// The property setter is provided for unit test purposes only.
/// </remarks>
public object[] Arguments { get; set; }
/// <summary>
/// Gets or sets the <see cref="HtmlEncoder"/>.
/// </summary>
/// <remarks>
/// The property setter is provided for unit test purposes only.
/// </remarks>
public HtmlEncoder HtmlEncoder { get; set; }
/// <summary>
/// Gets or sets the <see cref="ViewComponentDescriptor"/> for the View Component being invoked.
/// </summary>

View File

@ -12,7 +12,6 @@ using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.AspNet.Mvc.ViewFeatures.Internal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Internal;
namespace Microsoft.AspNet.Mvc.ViewFeatures
{

View File

@ -15,7 +15,6 @@ using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.ViewFeatures.Internal;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.OptionsModel;
namespace Microsoft.AspNet.Mvc.ViewFeatures

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Internal;
@ -20,6 +21,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.OptionsModel;
using Microsoft.Extensions.WebEncoders.Testing;
using Microsoft.Net.Http.Headers;
using Xunit;
@ -480,6 +482,7 @@ namespace Microsoft.AspNet.Mvc
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
services.AddSingleton<ITempDataDictionary>(new TempDataDictionary(httpContext, tempDataProvider));
services.AddTransient<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<HtmlEncoder, HtmlTestEncoder>();
return services;
}

View File

@ -65,7 +65,6 @@ namespace Microsoft.AspNet.Mvc
// Assert
Assert.IsType<ContentViewComponentResult>(actualResult);
Assert.Same(expectedContent, actualResult.Content);
Assert.Equal(expectedEncodedContent.ToString(), actualResult.EncodedContent.ToString());
}
[Fact]

View File

@ -11,6 +11,7 @@ using Microsoft.AspNet.Mvc.ViewComponents;
using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Routing;
using Microsoft.Extensions.WebEncoders.Testing;
using Moq;
using Xunit;
@ -32,26 +33,7 @@ namespace Microsoft.AspNet.Mvc
buffer.Position = 0;
// Assert
Assert.Equal("&lt;Test /&gt;", result.EncodedContent.ToString());
Assert.Equal("&lt;Test /&gt;", new StreamReader(buffer).ReadToEnd());
}
[Fact]
public void Execute_WritesData_PreEncoded()
{
// Arrange
var buffer = new MemoryStream();
var viewComponentContext = GetViewComponentContext(Mock.Of<IView>(), buffer);
var result = new ContentViewComponentResult(new HtmlString("<Test />"));
// Act
result.Execute(viewComponentContext);
buffer.Position = 0;
// Assert
Assert.Equal("<Test />", result.Content);
Assert.Equal("<Test />", new StreamReader(buffer).ReadToEnd());
Assert.Equal("HtmlEncode[[<Test />]]", new StreamReader(buffer).ReadToEnd());
}
private static ViewComponentContext GetViewComponentContext(IView view, Stream stream)
@ -61,7 +43,7 @@ namespace Microsoft.AspNet.Mvc
var viewContext = new ViewContext(
actionContext,
view,
viewData,
viewData,
new TempDataDictionary(new HttpContextAccessor(), new SessionStateTempDataProvider()),
TextWriter.Null,
new HtmlHelperOptions());
@ -73,7 +55,13 @@ namespace Microsoft.AspNet.Mvc
Type = typeof(object),
};
var viewComponentContext = new ViewComponentContext(viewComponentDescriptor, new object[0], viewContext, writer);
var viewComponentContext = new ViewComponentContext(
viewComponentDescriptor,
new object[0],
new HtmlTestEncoder(),
viewContext,
writer);
return viewComponentContext;
}
}

View File

@ -0,0 +1,69 @@
// 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.
#if MOCK_SUPPORT
using System.IO;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.ViewComponents;
using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Routing;
using Microsoft.Extensions.WebEncoders.Testing;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc
{
public class HtmlContentViewComponentResultTest
{
[Fact]
public void Execute_WritesData_PreEncoded()
{
// Arrange
var buffer = new MemoryStream();
var viewComponentContext = GetViewComponentContext(Mock.Of<IView>(), buffer);
var result = new HtmlContentViewComponentResult(new HtmlString("<Test />"));
// Act
result.Execute(viewComponentContext);
buffer.Position = 0;
// Assert
Assert.Equal("<Test />", new StreamReader(buffer).ReadToEnd());
}
private static ViewComponentContext GetViewComponentContext(IView view, Stream stream)
{
var actionContext = new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider());
var viewContext = new ViewContext(
actionContext,
view,
viewData,
new TempDataDictionary(new HttpContextAccessor(), new SessionStateTempDataProvider()),
TextWriter.Null,
new HtmlHelperOptions());
var writer = new StreamWriter(stream) { AutoFlush = true };
var viewComponentDescriptor = new ViewComponentDescriptor()
{
Type = typeof(object),
};
var viewComponentContext = new ViewComponentContext(
viewComponentDescriptor,
new object[0],
new HtmlTestEncoder(),
viewContext,
writer);
return viewComponentContext;
}
}
}
#endif

View File

@ -15,6 +15,7 @@ using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.WebEncoders.Testing;
using Moq;
using Newtonsoft.Json;
using Xunit;
@ -83,7 +84,13 @@ namespace Microsoft.AspNet.Mvc
Type = typeof(object),
};
var viewComponentContext = new ViewComponentContext(viewComponentDescriptor, new object[0], viewContext, writer);
var viewComponentContext = new ViewComponentContext(
viewComponentDescriptor,
new object[0],
new HtmlTestEncoder(),
viewContext,
writer);
return viewComponentContext;
}

View File

@ -16,6 +16,7 @@ using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.WebEncoders.Testing;
using Moq;
using Xunit;
@ -540,6 +541,7 @@ namespace Microsoft.AspNet.Mvc
var viewComponentContext = new ViewComponentContext(
viewComponentDescriptor,
new object[0],
new HtmlTestEncoder(),
viewContext,
TextWriter.Null);

View File

@ -4,6 +4,7 @@
using System;
using System.IO;
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Rendering;
@ -24,6 +25,12 @@ namespace MvcSample.Web.Components
"non proident, sunt in culpa qui officia deserunt mollit anim id est laborum")
.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
.ToArray();
private readonly HtmlEncoder _htmlEncoder;
public TagCloudViewComponentTagHelper(HtmlEncoder htmlEncoder)
{
_htmlEncoder = htmlEncoder;
}
public int Count { get; set; }
@ -52,6 +59,7 @@ namespace MvcSample.Web.Components
await result.ExecuteAsync(new ViewComponentContext(
viewComponentDescriptor,
new object[0],
_htmlEncoder,
ViewContext,
writer));