Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Commit

Permalink
Add a <partial /> tag helper
Browse files Browse the repository at this point in the history
Fixes #5916
  • Loading branch information
pranavkm committed Nov 29, 2017
1 parent 008a562 commit 3002f4f
Show file tree
Hide file tree
Showing 14 changed files with 447 additions and 103 deletions.
66 changes: 66 additions & 0 deletions src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// 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.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
/// <summary>
/// Renders a partial view.
/// </summary>
[HtmlTargetElement("partial", Attributes = "name", TagStructure = TagStructure.WithoutEndTag)]
public class PartialTagHelper : TagHelper
{
private readonly IHtmlGenerator _htmlGenerator;
private readonly IViewBufferScope _viewBufferScope;

public PartialTagHelper(
IHtmlGenerator htmlGenerator,
IViewBufferScope viewBufferScope)
{
_htmlGenerator = htmlGenerator ?? throw new ArgumentNullException(nameof(htmlGenerator));
_viewBufferScope = viewBufferScope ?? throw new ArgumentNullException(nameof(viewBufferScope));
}

/// <summary>
/// The name of the partial view used to create the HTML markup. Must not be <c>null</c>.
/// </summary>
public string Name { get; set; }

/// <summary>
/// A model to pass into the partial view.
/// </summary>
public object Model { get; set; }

/// <summary>
/// A <see cref="ViewDataDictionary"/> to pass into the partial view.
/// </summary>
public ViewDataDictionary ViewData { get; set; }

[ViewContext]
public ViewContext ViewContext { get; set; }

public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var viewBuffer = new ViewBuffer(_viewBufferScope, Name, ViewBuffer.PartialViewPageSize);
using (var writer = new ViewBufferTextWriter(viewBuffer, Encoding.UTF8))
{
await _htmlGenerator.RenderPartialViewAsync(
ViewContext,
Name,
Model,
ViewData,
writer);

output.SuppressOutput();
output.Content.SetHtmlContent(viewBuffer);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Internal;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using Microsoft.Extensions.Options;

Expand All @@ -35,6 +38,7 @@ public class DefaultHtmlGenerator : IHtmlGenerator
private readonly IUrlHelperFactory _urlHelperFactory;
private readonly HtmlEncoder _htmlEncoder;
private readonly ValidationHtmlAttributeProvider _validationAttributeProvider;
private readonly ICompositeViewEngine _viewEngine;

/// <summary>
/// Initializes a new instance of the <see cref="DefaultHtmlGenerator"/> class.
Expand All @@ -46,49 +50,27 @@ public class DefaultHtmlGenerator : IHtmlGenerator
/// <param name="urlHelperFactory">The <see cref="IUrlHelperFactory"/>.</param>
/// <param name="htmlEncoder">The <see cref="HtmlEncoder"/>.</param>
/// <param name="validationAttributeProvider">The <see cref="ValidationHtmlAttributeProvider"/>.</param>
/// <param name="viewEngine">The <see cref="ICompositeViewEngine"/>.</param>
public DefaultHtmlGenerator(
IAntiforgery antiforgery,
IOptions<MvcViewOptions> optionsAccessor,
IModelMetadataProvider metadataProvider,
IUrlHelperFactory urlHelperFactory,
HtmlEncoder htmlEncoder,
ValidationHtmlAttributeProvider validationAttributeProvider)
ValidationHtmlAttributeProvider validationAttributeProvider,
ICompositeViewEngine viewEngine)
{
if (antiforgery == null)
{
throw new ArgumentNullException(nameof(antiforgery));
}

if (optionsAccessor == null)
{
throw new ArgumentNullException(nameof(optionsAccessor));
}

if (metadataProvider == null)
{
throw new ArgumentNullException(nameof(metadataProvider));
}

if (urlHelperFactory == null)
{
throw new ArgumentNullException(nameof(urlHelperFactory));
}

if (htmlEncoder == null)
{
throw new ArgumentNullException(nameof(htmlEncoder));
}

if (validationAttributeProvider == null)
{
throw new ArgumentNullException(nameof(validationAttributeProvider));
}

_antiforgery = antiforgery;
_metadataProvider = metadataProvider;
_urlHelperFactory = urlHelperFactory;
_htmlEncoder = htmlEncoder;
_validationAttributeProvider = validationAttributeProvider;
_antiforgery = antiforgery ?? throw new ArgumentNullException(nameof(antiforgery));
_metadataProvider = metadataProvider ?? throw new ArgumentNullException(nameof(metadataProvider));
_urlHelperFactory = urlHelperFactory ?? throw new ArgumentNullException(nameof(urlHelperFactory));
_htmlEncoder = htmlEncoder ?? throw new ArgumentNullException(nameof(htmlEncoder));
_validationAttributeProvider = validationAttributeProvider ?? throw new ArgumentNullException(nameof(validationAttributeProvider));
_viewEngine = viewEngine;

// Underscores are fine characters in id's.
IdAttributeDotReplacement = optionsAccessor.Value.HtmlHelperOptions.IdAttributeDotReplacement;
Expand Down Expand Up @@ -214,8 +196,7 @@ public virtual TagBuilder GenerateCheckBox(

if (modelExplorer.Model != null)
{
bool modelChecked;
if (bool.TryParse(modelExplorer.Model.ToString(), out modelChecked))
if (bool.TryParse(modelExplorer.Model.ToString(), out var modelChecked))
{
isChecked = modelChecked;
}
Expand Down Expand Up @@ -363,8 +344,7 @@ public virtual TagBuilder GenerateHidden(
}

// Special-case opaque values and arbitrary binary data.
var byteArrayValue = value as byte[];
if (byteArrayValue != null)
if (value is byte[] byteArrayValue)
{
value = Convert.ToBase64String(byteArrayValue);
}
Expand Down Expand Up @@ -631,8 +611,7 @@ public virtual TagBuilder GenerateSelect(
}

// If there are any errors for a named field, we add the css attribute.
ModelStateEntry entry;
if (viewContext.ViewData.ModelState.TryGetValue(fullName, out entry))
if (viewContext.ViewData.ModelState.TryGetValue(fullName, out var entry))
{
if (entry.Errors.Count > 0)
{
Expand Down Expand Up @@ -684,8 +663,7 @@ public virtual TagBuilder GenerateTextArea(
nameof(expression));
}

ModelStateEntry entry;
viewContext.ViewData.ModelState.TryGetValue(fullName, out entry);
viewContext.ViewData.ModelState.TryGetValue(fullName, out var entry);

var value = string.Empty;
if (entry != null && entry.AttemptedValue != null)
Expand Down Expand Up @@ -791,8 +769,7 @@ public virtual TagBuilder GenerateValidationMessage(
return null;
}

ModelStateEntry entry;
var tryGetModelStateResult = viewContext.ViewData.ModelState.TryGetValue(fullName, out entry);
var tryGetModelStateResult = viewContext.ViewData.ModelState.TryGetValue(fullName, out var entry);
var modelErrors = tryGetModelStateResult ? entry.Errors : null;

ModelError modelError = null;
Expand Down Expand Up @@ -868,9 +845,8 @@ public virtual TagBuilder GenerateValidationSummary(
return null;
}

ModelStateEntry entryForModel;
if (excludePropertyErrors &&
(!viewData.ModelState.TryGetValue(viewData.TemplateInfo.HtmlFieldPrefix, out entryForModel) ||
(!viewData.ModelState.TryGetValue(viewData.TemplateInfo.HtmlFieldPrefix, out var entryForModel) ||
entryForModel.Errors.Count == 0))
{
// Client-side validation (if enabled) will not affect the generated element and element will be empty.
Expand Down Expand Up @@ -1095,6 +1071,65 @@ public virtual ICollection<string> GetCurrentValues(
return currentValues;
}

/// <inheritdoc />
public async Task RenderPartialViewAsync(
ViewContext viewContext,
string partialViewName,
object model,
ViewDataDictionary viewData,
TextWriter writer)
{
if (viewContext == null)
{
throw new ArgumentNullException(nameof(viewContext));
}

if (partialViewName == null)
{
throw new ArgumentNullException(nameof(partialViewName));
}

if (writer == null)
{
throw new ArgumentNullException(nameof(writer));
}

var viewEngineResult = _viewEngine.GetView(
viewContext.ExecutingFilePath,
partialViewName,
isMainPage: false);
var getViewLocations = viewEngineResult.SearchedLocations;
if (!viewEngineResult.Success)
{
viewEngineResult = _viewEngine.FindView(viewContext, partialViewName, isMainPage: false);
}

if (!viewEngineResult.Success)
{
var searchedLocations = Enumerable.Concat(getViewLocations, viewEngineResult.SearchedLocations);
var locations = string.Empty;
if (searchedLocations.Any())
{
locations += Environment.NewLine + string.Join(Environment.NewLine, searchedLocations);
}

throw new InvalidOperationException(
Resources.FormatViewEngine_PartialViewNotFound(partialViewName, locations));
}

var view = viewEngineResult.View;
using (view as IDisposable)
{
// Determine which ViewData we should use to construct a new ViewData
var baseViewData = viewData ?? viewContext.ViewData;

var newViewData = new ViewDataDictionary<object>(baseViewData, model);
var partialViewContext = new ViewContext(viewContext, view, newViewData, writer);

await viewEngineResult.View.RenderAsync(partialViewContext);
}
}

internal static string EvalString(ViewContext viewContext, string key, string format)
{
return Convert.ToString(viewContext.ViewData.Eval(key, format), CultureInfo.CurrentCulture);
Expand Down Expand Up @@ -1133,8 +1168,7 @@ internal static TagBuilder GenerateOption(SelectListItem item, string text, bool

internal static object GetModelStateValue(ViewContext viewContext, string key, Type destinationType)
{
ModelStateEntry entry;
if (viewContext.ViewData.ModelState.TryGetValue(key, out entry) && entry.RawValue != null)
if (viewContext.ViewData.ModelState.TryGetValue(key, out var entry) && entry.RawValue != null)
{
return ModelBindingHelper.ConvertTo(entry.RawValue, destinationType, culture: null);
}
Expand Down Expand Up @@ -1249,8 +1283,7 @@ protected virtual TagBuilder GenerateInput(
case InputType.Radio:
if (!usedModelState)
{
var modelStateValue = GetModelStateValue(viewContext, fullName, typeof(string)) as string;
if (modelStateValue != null)
if (GetModelStateValue(viewContext, fullName, typeof(string)) is string modelStateValue)
{
isChecked = string.Equals(modelStateValue, valueParameter, StringComparison.Ordinal);
usedModelState = true;
Expand Down Expand Up @@ -1313,8 +1346,7 @@ protected virtual TagBuilder GenerateInput(
}

// If there are any errors for a named field, we add the CSS attribute.
ModelStateEntry entry;
if (viewContext.ViewData.ModelState.TryGetValue(fullName, out entry) && entry.Errors.Count > 0)
if (viewContext.ViewData.ModelState.TryGetValue(fullName, out var entry) && entry.Errors.Count > 0)
{
tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName);
}
Expand Down Expand Up @@ -1410,8 +1442,7 @@ private static Enum ConvertEnumFromInteger(object value, Type targetType)

private static object ConvertEnumFromString<TEnum>(string value) where TEnum : struct
{
TEnum enumValue;
if (Enum.TryParse(value, out enumValue))
if (Enum.TryParse(value, out TEnum enumValue))
{
return enumValue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,7 @@ public static IDictionary<string, object> ObjectToDictionary(object value)
/// </remarks>
public static IDictionary<string, object> AnonymousObjectToHtmlAttributes(object htmlAttributes)
{
var dictionary = htmlAttributes as IDictionary<string, object>;
if (dictionary != null)
if (htmlAttributes is IDictionary<string, object> dictionary)
{
return new Dictionary<string, object>(dictionary, StringComparer.OrdinalIgnoreCase);
}
Expand Down Expand Up @@ -504,7 +503,7 @@ protected virtual IHtmlContent GenerateDisplay(
return templateBuilder.Build();
}

protected virtual async Task RenderPartialCoreAsync(
protected virtual Task RenderPartialCoreAsync(
string partialViewName,
object model,
ViewDataDictionary viewData,
Expand All @@ -515,45 +514,12 @@ protected virtual async Task RenderPartialCoreAsync(
throw new ArgumentNullException(nameof(partialViewName));
}

var viewEngineResult = _viewEngine.GetView(
ViewContext.ExecutingFilePath,
return _htmlGenerator.RenderPartialViewAsync(
ViewContext,
partialViewName,
isMainPage: false);
var originalLocations = viewEngineResult.SearchedLocations;
if (!viewEngineResult.Success)
{
viewEngineResult = _viewEngine.FindView(ViewContext, partialViewName, isMainPage: false);
}

if (!viewEngineResult.Success)
{
var locations = string.Empty;
if (originalLocations.Any())
{
locations = Environment.NewLine + string.Join(Environment.NewLine, originalLocations);
}

if (viewEngineResult.SearchedLocations.Any())
{
locations +=
Environment.NewLine + string.Join(Environment.NewLine, viewEngineResult.SearchedLocations);
}

throw new InvalidOperationException(
Resources.FormatViewEngine_PartialViewNotFound(partialViewName, locations));
}

var view = viewEngineResult.View;
using (view as IDisposable)
{
// Determine which ViewData we should use to construct a new ViewData
var baseViewData = viewData ?? ViewData;

var newViewData = new ViewDataDictionary<object>(baseViewData, model);
var viewContext = new ViewContext(ViewContext, view, newViewData, writer);

await viewEngineResult.View.RenderAsync(viewContext);
}
model,
viewData,
writer);
}

/// <inheritdoc />
Expand Down
Loading

0 comments on commit 3002f4f

Please sign in to comment.