|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures;
internal static class DefaultEditorTemplates
{
private const string HtmlAttributeKey = "htmlAttributes";
private const string UsePasswordValue = "Switch.Microsoft.AspNetCore.Mvc.UsePasswordValue";
public static IHtmlContent BooleanTemplate(IHtmlHelper htmlHelper)
{
bool? value = null;
if (htmlHelper.ViewData.Model != null)
{
value = Convert.ToBoolean(htmlHelper.ViewData.Model, CultureInfo.InvariantCulture);
}
return htmlHelper.ViewData.ModelMetadata.IsNullableValueType ?
BooleanTemplateDropDownList(htmlHelper, value) :
BooleanTemplateCheckbox(htmlHelper, value ?? false);
}
private static IHtmlContent BooleanTemplateCheckbox(IHtmlHelper htmlHelper, bool value)
{
return htmlHelper.CheckBox(
expression: null,
isChecked: value,
htmlAttributes: CreateHtmlAttributes(htmlHelper, "check-box"));
}
private static IHtmlContent BooleanTemplateDropDownList(IHtmlHelper htmlHelper, bool? value)
{
return htmlHelper.DropDownList(
expression: null,
selectList: DefaultDisplayTemplates.TriStateValues(value),
optionLabel: null,
htmlAttributes: CreateHtmlAttributes(htmlHelper, "list-box tri-state"));
}
public static IHtmlContent CollectionTemplate(IHtmlHelper htmlHelper)
{
var viewData = htmlHelper.ViewData;
var model = viewData.Model;
if (model == null)
{
return HtmlString.Empty;
}
var enumerable = model as IEnumerable;
if (enumerable == null)
{
// Only way we could reach here is if user passed templateName: "Collection" to an Editor() overload.
throw new InvalidOperationException(Resources.FormatTemplates_TypeMustImplementIEnumerable(
"Collection", model.GetType().FullName, typeof(IEnumerable).FullName));
}
var elementMetadata = htmlHelper.ViewData.ModelMetadata.ElementMetadata;
Debug.Assert(elementMetadata != null);
var typeInCollectionIsNullableValueType = elementMetadata.IsNullableValueType;
var serviceProvider = htmlHelper.ViewContext.HttpContext.RequestServices;
var metadataProvider = serviceProvider.GetRequiredService<IModelMetadataProvider>();
// Use typeof(string) instead of typeof(object) for IEnumerable collections. Neither type is Nullable<T>.
if (elementMetadata.ModelType == typeof(object))
{
elementMetadata = metadataProvider.GetMetadataForType(typeof(string));
}
var oldPrefix = viewData.TemplateInfo.HtmlFieldPrefix;
try
{
viewData.TemplateInfo.HtmlFieldPrefix = string.Empty;
var collection = model as ICollection;
var result = collection == null ? new HtmlContentBuilder() : new HtmlContentBuilder(collection.Count);
var viewEngine = serviceProvider.GetRequiredService<ICompositeViewEngine>();
var viewBufferScope = serviceProvider.GetRequiredService<IViewBufferScope>();
var index = 0;
foreach (var item in enumerable)
{
var itemMetadata = elementMetadata;
if (item != null && !typeInCollectionIsNullableValueType)
{
itemMetadata = metadataProvider.GetMetadataForType(item.GetType());
}
var modelExplorer = new ModelExplorer(
metadataProvider,
container: htmlHelper.ViewData.ModelExplorer,
metadata: itemMetadata,
model: item);
var fieldName = string.Format(CultureInfo.InvariantCulture, "{0}[{1}]", oldPrefix, index++);
var templateBuilder = new TemplateBuilder(
viewEngine,
viewBufferScope,
htmlHelper.ViewContext,
htmlHelper.ViewData,
modelExplorer,
htmlFieldName: fieldName,
templateName: null,
readOnly: false,
additionalViewData: null);
result.AppendHtml(templateBuilder.Build());
}
return result;
}
finally
{
viewData.TemplateInfo.HtmlFieldPrefix = oldPrefix;
}
}
public static IHtmlContent DecimalTemplate(IHtmlHelper htmlHelper)
{
if (htmlHelper.ViewData.TemplateInfo.FormattedModelValue == htmlHelper.ViewData.Model)
{
htmlHelper.ViewData.TemplateInfo.FormattedModelValue =
string.Format(CultureInfo.CurrentCulture, "{0:0.00}"{0:0.00}", htmlHelper.ViewData.Model);
}
return StringTemplate(htmlHelper);
}
public static IHtmlContent HiddenInputTemplate(IHtmlHelper htmlHelper)
{
var viewData = htmlHelper.ViewData;
var model = viewData.Model;
IHtmlContent display;
if (viewData.ModelMetadata.HideSurroundingHtml)
{
display = null;
}
else
{
display = DefaultDisplayTemplates.StringTemplate(htmlHelper);
}
var htmlAttributesObject = viewData[HtmlAttributeKey];
var hidden = htmlHelper.Hidden(expression: null, value: model, htmlAttributes: htmlAttributesObject);
if (viewData.ModelMetadata.HideSurroundingHtml)
{
return hidden;
}
return new HtmlContentBuilder(capacity: 2)
.AppendHtml(display)
.AppendHtml(hidden);
}
private static IDictionary<string, object> CreateHtmlAttributes(
IHtmlHelper htmlHelper,
string className,
string inputType = null)
{
var htmlAttributesObject = htmlHelper.ViewData[HtmlAttributeKey];
if (htmlAttributesObject != null)
{
return MergeHtmlAttributes(htmlAttributesObject, className, inputType);
}
var htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
{
{ "class", className }
};
if (inputType != null)
{
htmlAttributes.Add("type", inputType);
}
return htmlAttributes;
}
private static IDictionary<string, object> MergeHtmlAttributes(
object htmlAttributesObject,
string className,
string inputType)
{
var htmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributesObject);
if (htmlAttributes.TryGetValue("class", out var htmlClassObject))
{
var htmlClassName = htmlClassObject + " " + className;
htmlAttributes["class"] = htmlClassName;
}
else
{
htmlAttributes.Add("class", className);
}
// The input type from the provided htmlAttributes overrides the inputType parameter.
if (inputType != null && !htmlAttributes.ContainsKey("type"))
{
htmlAttributes.Add("type", inputType);
}
return htmlAttributes;
}
public static IHtmlContent MultilineTemplate(IHtmlHelper htmlHelper)
{
return htmlHelper.TextArea(
expression: string.Empty,
value: htmlHelper.ViewContext.ViewData.TemplateInfo.FormattedModelValue.ToString(),
rows: 0,
columns: 0,
htmlAttributes: CreateHtmlAttributes(htmlHelper, "text-box multi-line"));
}
public static IHtmlContent ObjectTemplate(IHtmlHelper htmlHelper)
{
var viewData = htmlHelper.ViewData;
var templateInfo = viewData.TemplateInfo;
var modelExplorer = viewData.ModelExplorer;
if (templateInfo.TemplateDepth > 1)
{
if (modelExplorer.Model == null)
{
return new HtmlString(modelExplorer.Metadata.NullDisplayText);
}
var text = modelExplorer.GetSimpleDisplayText();
if (modelExplorer.Metadata.HtmlEncode)
{
return new StringHtmlContent(text);
}
return new HtmlString(text);
}
var serviceProvider = htmlHelper.ViewContext.HttpContext.RequestServices;
var viewEngine = serviceProvider.GetRequiredService<ICompositeViewEngine>();
var viewBufferScope = serviceProvider.GetRequiredService<IViewBufferScope>();
var content = new HtmlContentBuilder(modelExplorer.Metadata.Properties.Count);
foreach (var propertyExplorer in modelExplorer.PropertiesInternal)
{
var propertyMetadata = propertyExplorer.Metadata;
if (!ShouldShow(propertyExplorer, templateInfo))
{
continue;
}
var templateBuilder = new TemplateBuilder(
viewEngine,
viewBufferScope,
htmlHelper.ViewContext,
htmlHelper.ViewData,
propertyExplorer,
htmlFieldName: propertyMetadata.PropertyName,
templateName: null,
readOnly: false,
additionalViewData: null);
var templateBuilderResult = templateBuilder.Build();
if (!propertyMetadata.HideSurroundingHtml)
{
var label = htmlHelper.Label(propertyMetadata.PropertyName, labelText: null, htmlAttributes: null);
using (var writer = new HasContentTextWriter())
{
label.WriteTo(writer, PassThroughHtmlEncoder.Default);
if (writer.HasContent)
{
var labelTag = new TagBuilder("div");
labelTag.AddCssClass("editor-label");
labelTag.InnerHtml.SetHtmlContent(label);
content.AppendLine(labelTag);
}
}
var valueDivTag = new TagBuilder("div");
valueDivTag.AddCssClass("editor-field");
valueDivTag.InnerHtml.AppendHtml(templateBuilderResult);
valueDivTag.InnerHtml.AppendHtml(" ");
valueDivTag.InnerHtml.AppendHtml(htmlHelper.ValidationMessage(
propertyMetadata.PropertyName,
message: null,
htmlAttributes: null,
tag: null));
content.AppendLine(valueDivTag);
}
else
{
content.AppendHtml(templateBuilderResult);
}
}
return content;
}
public static IHtmlContent PasswordTemplate(IHtmlHelper htmlHelper)
{
object value = null;
if (AppContext.TryGetSwitch(UsePasswordValue, out var usePasswordValue) && usePasswordValue)
{
value = htmlHelper.ViewData.TemplateInfo.FormattedModelValue;
}
return htmlHelper.Password(
expression: null,
value: value,
htmlAttributes: CreateHtmlAttributes(htmlHelper, "text-box single-line password"));
}
private static bool ShouldShow(ModelExplorer modelExplorer, TemplateInfo templateInfo)
{
return
modelExplorer.Metadata.ShowForEdit &&
!modelExplorer.Metadata.IsComplexType &&
!templateInfo.Visited(modelExplorer);
}
public static IHtmlContent StringTemplate(IHtmlHelper htmlHelper)
{
return GenerateTextBox(htmlHelper);
}
public static IHtmlContent PhoneNumberInputTemplate(IHtmlHelper htmlHelper)
{
return GenerateTextBox(htmlHelper, inputType: "tel");
}
public static IHtmlContent UrlInputTemplate(IHtmlHelper htmlHelper)
{
return GenerateTextBox(htmlHelper, inputType: "url");
}
public static IHtmlContent EmailAddressInputTemplate(IHtmlHelper htmlHelper)
{
return GenerateTextBox(htmlHelper, inputType: "email");
}
public static IHtmlContent DateTimeOffsetTemplate(IHtmlHelper htmlHelper)
{
ApplyRfc3339DateFormattingIfNeeded(htmlHelper, @"{0:yyyy-MM-ddTHH\:mm\:ss.fffK}");
return GenerateTextBox(htmlHelper, inputType: "text");
}
public static IHtmlContent DateTimeLocalInputTemplate(IHtmlHelper htmlHelper)
{
ApplyRfc3339DateFormattingIfNeeded(htmlHelper, @"{0:yyyy-MM-ddTHH\:mm\:ss.fff}");
return GenerateTextBox(htmlHelper, inputType: "datetime-local");
}
public static IHtmlContent DateInputTemplate(IHtmlHelper htmlHelper)
{
ApplyRfc3339DateFormattingIfNeeded(htmlHelper, "{0:yyyy-MM-dd}");
return GenerateTextBox(htmlHelper, inputType: "date");
}
public static IHtmlContent TimeInputTemplate(IHtmlHelper htmlHelper)
{
ApplyRfc3339DateFormattingIfNeeded(htmlHelper, @"{0:HH\:mm\:ss.fff}");
return GenerateTextBox(htmlHelper, inputType: "time");
}
public static IHtmlContent MonthInputTemplate(IHtmlHelper htmlHelper)
{
// "month" is a new HTML5 input type that only will be rendered in Rfc3339 mode
htmlHelper.Html5DateRenderingMode = Html5DateRenderingMode.Rfc3339;
ApplyRfc3339DateFormattingIfNeeded(htmlHelper, "{0:yyyy-MM}");
return GenerateTextBox(htmlHelper, inputType: "month");
}
public static IHtmlContent WeekInputTemplate(IHtmlHelper htmlHelper)
{
return GenerateTextBox(htmlHelper, inputType: "week");
}
public static IHtmlContent NumberInputTemplate(IHtmlHelper htmlHelper)
{
return GenerateTextBox(htmlHelper, inputType: "number");
}
public static IHtmlContent FileInputTemplate(IHtmlHelper htmlHelper)
{
ArgumentNullException.ThrowIfNull(htmlHelper);
return GenerateTextBox(htmlHelper, inputType: "file");
}
public static IHtmlContent FileCollectionInputTemplate(IHtmlHelper htmlHelper)
{
ArgumentNullException.ThrowIfNull(htmlHelper);
var htmlAttributes =
CreateHtmlAttributes(htmlHelper, className: "text-box single-line", inputType: "file");
htmlAttributes["multiple"] = "multiple";
return GenerateTextBox(htmlHelper, htmlHelper.ViewData.TemplateInfo.FormattedModelValue, htmlAttributes);
}
private static void ApplyRfc3339DateFormattingIfNeeded(IHtmlHelper htmlHelper, string format)
{
if (htmlHelper.Html5DateRenderingMode != Html5DateRenderingMode.Rfc3339)
{
return;
}
var metadata = htmlHelper.ViewData.ModelMetadata;
var value = htmlHelper.ViewData.Model;
if (htmlHelper.ViewData.TemplateInfo.FormattedModelValue != value && metadata.HasNonDefaultEditFormat)
{
return;
}
if (value is DateTime || value is DateTimeOffset)
{
htmlHelper.ViewData.TemplateInfo.FormattedModelValue =
string.Format(CultureInfo.InvariantCulture, format, value);
}
}
private static IHtmlContent GenerateTextBox(IHtmlHelper htmlHelper, string inputType = null)
{
return GenerateTextBox(htmlHelper, inputType, htmlHelper.ViewData.TemplateInfo.FormattedModelValue);
}
private static IHtmlContent GenerateTextBox(IHtmlHelper htmlHelper, string inputType, object value)
{
var htmlAttributes =
CreateHtmlAttributes(htmlHelper, className: "text-box single-line", inputType: inputType);
return GenerateTextBox(htmlHelper, value, htmlAttributes);
}
private static IHtmlContent GenerateTextBox(IHtmlHelper htmlHelper, object value, object htmlAttributes)
{
return htmlHelper.TextBox(
expression: null,
value: value,
format: null,
htmlAttributes: htmlAttributes);
}
private sealed class HasContentTextWriter : TextWriter
{
public bool HasContent { get; private set; }
public override Encoding Encoding => Null.Encoding;
public override void Write(char value)
{
HasContent = true;
}
public override void Write(char[] buffer, int index, int count)
{
HasContent |= count > 0;
}
public override void Write(ReadOnlySpan<char> buffer)
{
HasContent |= buffer.IsEmpty;
}
public override void Write(string value)
{
HasContent |= !string.IsNullOrEmpty(value);
}
}
// An HTML encoder which passes through all input data. Does no encoding.
// Copied from Microsoft.AspNetCore.Razor.TagHelpers.NullHtmlEncoder.
private sealed class PassThroughHtmlEncoder : HtmlEncoder
{
private PassThroughHtmlEncoder()
{
}
public static new PassThroughHtmlEncoder Default { get; } = new PassThroughHtmlEncoder();
public override int MaxOutputCharactersPerInputCharacter => 1;
public override string Encode(string value)
{
return value;
}
public override void Encode(TextWriter output, char[] value, int startIndex, int characterCount)
{
ArgumentNullException.ThrowIfNull(output);
if (characterCount == 0)
{
return;
}
output.Write(value, startIndex, characterCount);
}
public override void Encode(TextWriter output, string value, int startIndex, int characterCount)
{
ArgumentNullException.ThrowIfNull(output);
ArgumentNullException.ThrowIfNull(value);
if (characterCount == 0)
{
return;
}
output.Write(value.AsSpan(startIndex, characterCount));
}
public override unsafe int FindFirstCharacterToEncode(char* text, int textLength)
{
return -1;
}
public override unsafe bool TryEncodeUnicodeScalar(
int unicodeScalar,
char* buffer,
int bufferLength,
out int numberOfCharactersWritten)
{
numberOfCharactersWritten = 0;
return false;
}
public override bool WillEncode(int unicodeScalar)
{
return false;
}
}
}
|