// 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Antiforgery;
using Microsoft.AspNet.Html.Abstractions;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.ModelBinding.Validation;
using Microsoft.AspNet.Mvc.Rendering.Expressions;
using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.Framework.Internal;
using Microsoft.Framework.OptionsModel;
using Microsoft.Framework.WebEncoders;
namespace Microsoft.AspNet.Mvc.Rendering
{
public class DefaultHtmlGenerator : IHtmlGenerator
{
private const string HiddenListItem = @"
";
private static readonly MethodInfo ConvertEnumFromStringMethod =
typeof(DefaultHtmlGenerator).GetTypeInfo().GetDeclaredMethod(nameof(ConvertEnumFromString));
private readonly IAntiforgery _antiforgery;
private readonly IClientModelValidatorProvider _clientModelValidatorProvider;
private readonly IModelMetadataProvider _metadataProvider;
private readonly IUrlHelper _urlHelper;
private readonly IHtmlEncoder _htmlEncoder;
///
/// Initializes a new instance of the class.
///
/// The instance which is used to generate antiforgery
/// tokens.
/// The accessor for .
/// The .
/// The .
/// The .
public DefaultHtmlGenerator(
[NotNull] IAntiforgery antiforgery,
[NotNull] IOptions optionsAccessor,
[NotNull] IModelMetadataProvider metadataProvider,
[NotNull] IUrlHelper urlHelper,
[NotNull] IHtmlEncoder htmlEncoder)
{
_antiforgery = antiforgery;
var clientValidatorProviders = optionsAccessor.Value.ClientModelValidatorProviders;
_clientModelValidatorProvider = new CompositeClientModelValidatorProvider(clientValidatorProviders);
_metadataProvider = metadataProvider;
_urlHelper = urlHelper;
_htmlEncoder = htmlEncoder;
// Underscores are fine characters in id's.
IdAttributeDotReplacement = optionsAccessor.Value.HtmlHelperOptions.IdAttributeDotReplacement;
}
///
public string IdAttributeDotReplacement { get; }
///
public string Encode(string value)
{
return !string.IsNullOrEmpty(value) ? _htmlEncoder.HtmlEncode(value) : string.Empty;
}
///
public string Encode(object value)
{
return (value != null) ? _htmlEncoder.HtmlEncode(value.ToString()) : string.Empty;
}
///
public string FormatValue(object value, string format)
{
return ViewDataDictionary.FormatValue(value, format);
}
///
public virtual TagBuilder GenerateActionLink(
[NotNull] string linkText,
string actionName,
string controllerName,
string protocol,
string hostname,
string fragment,
object routeValues,
object htmlAttributes)
{
var url = _urlHelper.Action(actionName, controllerName, routeValues, protocol, hostname, fragment);
return GenerateLink(linkText, url, htmlAttributes);
}
///
public virtual IHtmlContent GenerateAntiforgery([NotNull] ViewContext viewContext)
{
var tag = _antiforgery.GetHtml(viewContext.HttpContext);
return new HtmlString(tag);
}
///
public virtual TagBuilder GenerateCheckBox(
[NotNull] ViewContext viewContext,
ModelExplorer modelExplorer,
string expression,
bool? isChecked,
object htmlAttributes)
{
if (modelExplorer != null)
{
// CheckBoxFor() case. That API does not support passing isChecked directly.
Debug.Assert(!isChecked.HasValue);
if (modelExplorer.Model != null)
{
bool modelChecked;
if (bool.TryParse(modelExplorer.Model.ToString(), out modelChecked))
{
isChecked = modelChecked;
}
}
}
var htmlAttributeDictionary = GetHtmlAttributeDictionaryOrNull(htmlAttributes);
if (isChecked.HasValue && htmlAttributeDictionary != null)
{
// Explicit isChecked value must override "checked" in dictionary.
htmlAttributeDictionary.Remove("checked");
}
// Use ViewData only in CheckBox case (metadata null) and when the user didn't pass an isChecked value.
return GenerateInput(
viewContext,
InputType.CheckBox,
modelExplorer,
expression,
value: "true",
useViewData: (modelExplorer == null && !isChecked.HasValue),
isChecked: isChecked ?? false,
setId: true,
isExplicitValue: false,
format: null,
htmlAttributes: htmlAttributeDictionary);
}
///
public virtual TagBuilder GenerateHiddenForCheckbox(
[NotNull] ViewContext viewContext,
ModelExplorer modelExplorer,
string expression)
{
var tagBuilder = new TagBuilder("input");
tagBuilder.MergeAttribute("type", GetInputTypeString(InputType.Hidden));
tagBuilder.MergeAttribute("value", "false");
tagBuilder.TagRenderMode = TagRenderMode.SelfClosing;
var fullName = GetFullHtmlFieldName(viewContext, expression);
tagBuilder.MergeAttribute("name", fullName);
return tagBuilder;
}
///
public virtual TagBuilder GenerateForm(
[NotNull] ViewContext viewContext,
string actionName,
string controllerName,
object routeValues,
string method,
object htmlAttributes)
{
var defaultMethod = false;
if (string.IsNullOrEmpty(method))
{
defaultMethod = true;
}
else if (string.Equals(method, FormMethod.Post.ToString(), StringComparison.OrdinalIgnoreCase))
{
defaultMethod = true;
}
string action;
if (actionName == null && controllerName == null && routeValues == null && defaultMethod)
{
// Submit to the original URL in the special case that user called the BeginForm() overload without
// parameters (except for the htmlAttributes parameter). Also reachable in the even-more-unusual case
// that user called another BeginForm() overload with default argument values.
var request = viewContext.HttpContext.Request;
action = request.PathBase + request.Path + request.QueryString;
}
else
{
action = _urlHelper.Action(action: actionName, controller: controllerName, values: routeValues);
}
return GenerateFormCore(viewContext, action, method, htmlAttributes);
}
///
public TagBuilder GenerateRouteForm(
[NotNull] ViewContext viewContext,
string routeName,
object routeValues,
string method,
object htmlAttributes)
{
var action =
_urlHelper.RouteUrl(routeName, values: routeValues, protocol: null, host: null, fragment: null);
return GenerateFormCore(viewContext, action, method, htmlAttributes);
}
///
public virtual TagBuilder GenerateHidden(
[NotNull] ViewContext viewContext,
ModelExplorer modelExplorer,
string expression,
object value,
bool useViewData,
object htmlAttributes)
{
// Special-case opaque values and arbitrary binary data.
var byteArrayValue = value as byte[];
if (byteArrayValue != null)
{
value = Convert.ToBase64String(byteArrayValue);
}
var htmlAttributeDictionary = GetHtmlAttributeDictionaryOrNull(htmlAttributes);
return GenerateInput(
viewContext,
InputType.Hidden,
modelExplorer,
expression,
value,
useViewData,
isChecked: false,
setId: true,
isExplicitValue: true,
format: null,
htmlAttributes: htmlAttributeDictionary);
}
///
public virtual TagBuilder GenerateLabel(
[NotNull] ViewContext viewContext,
[NotNull] ModelExplorer modelExplorer,
string expression,
string labelText,
object htmlAttributes)
{
var resolvedLabelText = labelText ??
modelExplorer.Metadata.DisplayName ??
modelExplorer.Metadata.PropertyName;
if (resolvedLabelText == null)
{
resolvedLabelText =
string.IsNullOrEmpty(expression) ? string.Empty : expression.Split('.').Last();
}
if (string.IsNullOrEmpty(resolvedLabelText))
{
return null;
}
var tagBuilder = new TagBuilder("label");
var idString =
TagBuilder.CreateSanitizedId(GetFullHtmlFieldName(viewContext, expression), IdAttributeDotReplacement);
tagBuilder.Attributes.Add("for", idString);
tagBuilder.SetInnerText(resolvedLabelText);
tagBuilder.MergeAttributes(GetHtmlAttributeDictionaryOrNull(htmlAttributes), replaceExisting: true);
return tagBuilder;
}
///
public virtual TagBuilder GeneratePassword(
[NotNull] ViewContext viewContext,
ModelExplorer modelExplorer,
string expression,
object value,
object htmlAttributes)
{
var htmlAttributeDictionary = GetHtmlAttributeDictionaryOrNull(htmlAttributes);
return GenerateInput(
viewContext,
InputType.Password,
modelExplorer,
expression,
value,
useViewData: false,
isChecked: false,
setId: true,
isExplicitValue: true,
format: null,
htmlAttributes: htmlAttributeDictionary);
}
///
public virtual TagBuilder GenerateRadioButton(
[NotNull] ViewContext viewContext,
ModelExplorer modelExplorer,
string expression,
object value,
bool? isChecked,
object htmlAttributes)
{
var htmlAttributeDictionary = GetHtmlAttributeDictionaryOrNull(htmlAttributes);
if (modelExplorer == null)
{
// RadioButton() case. Do not override checked attribute if isChecked is implicit.
if (!isChecked.HasValue &&
(htmlAttributeDictionary == null || !htmlAttributeDictionary.ContainsKey("checked")))
{
// Note value may be null if isChecked is non-null.
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
// isChecked not provided nor found in the given attributes; fall back to view data.
var valueString = Convert.ToString(value, CultureInfo.CurrentCulture);
isChecked = string.Equals(
EvalString(viewContext, expression),
valueString,
StringComparison.OrdinalIgnoreCase);
}
}
else
{
// RadioButtonFor() case. That API does not support passing isChecked directly.
Debug.Assert(!isChecked.HasValue);
// Need a value to determine isChecked.
Debug.Assert(value != null);
var model = modelExplorer.Model;
var valueString = Convert.ToString(value, CultureInfo.CurrentCulture);
isChecked = model != null &&
string.Equals(model.ToString(), valueString, StringComparison.OrdinalIgnoreCase);
}
if (isChecked.HasValue && htmlAttributeDictionary != null)
{
// Explicit isChecked value must override "checked" in dictionary.
htmlAttributeDictionary.Remove("checked");
}
return GenerateInput(
viewContext,
InputType.Radio,
modelExplorer,
expression,
value,
useViewData: false,
isChecked: isChecked ?? false,
setId: true,
isExplicitValue: true,
format: null,
htmlAttributes: htmlAttributeDictionary);
}
///
public virtual TagBuilder GenerateRouteLink(
[NotNull] string linkText,
string routeName,
string protocol,
string hostName,
string fragment,
object routeValues,
object htmlAttributes)
{
var url = _urlHelper.RouteUrl(routeName, routeValues, protocol, hostName, fragment);
return GenerateLink(linkText, url, htmlAttributes);
}
///
public TagBuilder GenerateSelect(
[NotNull] ViewContext viewContext,
ModelExplorer modelExplorer,
string optionLabel,
string expression,
IEnumerable selectList,
bool allowMultiple,
object htmlAttributes)
{
var currentValues = GetCurrentValues(viewContext, modelExplorer, expression, allowMultiple);
return GenerateSelect(
viewContext,
modelExplorer,
optionLabel,
expression,
selectList,
currentValues,
allowMultiple,
htmlAttributes);
}
///
public virtual TagBuilder GenerateSelect(
[NotNull] ViewContext viewContext,
ModelExplorer modelExplorer,
string optionLabel,
string expression,
IEnumerable selectList,
IReadOnlyCollection currentValues,
bool allowMultiple,
object htmlAttributes)
{
var fullName = GetFullHtmlFieldName(viewContext, expression);
if (string.IsNullOrEmpty(fullName))
{
throw new ArgumentException(
Resources.FormatHtmlGenerator_FieldNameCannotBeNullOrEmpty(
typeof(IHtmlHelper).FullName,
nameof(IHtmlHelper.Editor),
typeof(IHtmlHelper<>).FullName,
nameof(IHtmlHelper