438 lines
17 KiB
C#
438 lines
17 KiB
C#
// 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 Microsoft.AspNetCore.Html;
|
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
|
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Internal
|
|
{
|
|
public static class DefaultEditorTemplates
|
|
{
|
|
private const string HtmlAttributeKey = "htmlAttributes";
|
|
|
|
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 collection = model as IEnumerable;
|
|
if (collection == 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 fieldNameBase = oldPrefix;
|
|
var result = new HtmlContentBuilder();
|
|
var viewEngine = serviceProvider.GetRequiredService<ICompositeViewEngine>();
|
|
var viewBufferScope = serviceProvider.GetRequiredService<IViewBufferScope>();
|
|
|
|
var index = 0;
|
|
foreach (var item in collection)
|
|
{
|
|
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}]", fieldNameBase, 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}", htmlHelper.ViewData.Model);
|
|
}
|
|
|
|
return StringTemplate(htmlHelper);
|
|
}
|
|
|
|
public static IHtmlContent HiddenInputTemplate(IHtmlHelper htmlHelper)
|
|
{
|
|
var viewData = htmlHelper.ViewData;
|
|
var model = viewData.Model;
|
|
|
|
var result = new HtmlContentBuilder();
|
|
if (!viewData.ModelMetadata.HideSurroundingHtml)
|
|
{
|
|
result.AppendHtml(DefaultDisplayTemplates.StringTemplate(htmlHelper));
|
|
}
|
|
|
|
// Special-case opaque values and arbitrary binary data.
|
|
var modelAsByteArray = model as byte[];
|
|
if (modelAsByteArray != null)
|
|
{
|
|
model = Convert.ToBase64String(modelAsByteArray);
|
|
}
|
|
|
|
var htmlAttributesObject = viewData[HtmlAttributeKey];
|
|
var hiddenResult = htmlHelper.Hidden(expression: null, value: model, htmlAttributes: htmlAttributesObject);
|
|
result.AppendHtml(hiddenResult);
|
|
|
|
return result;
|
|
}
|
|
|
|
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);
|
|
|
|
object htmlClassObject;
|
|
if (htmlAttributes.TryGetValue("class", out htmlClassObject))
|
|
{
|
|
var htmlClassName = htmlClassObject.ToString() + " " + 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();
|
|
foreach (var propertyExplorer in modelExplorer.Properties)
|
|
{
|
|
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);
|
|
if (!string.IsNullOrEmpty(label.ToString()))
|
|
{
|
|
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)
|
|
{
|
|
return htmlHelper.Password(
|
|
expression: null,
|
|
value: htmlHelper.ViewData.TemplateInfo.FormattedModelValue,
|
|
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 DateTimeInputTemplate(IHtmlHelper htmlHelper)
|
|
{
|
|
ApplyRfc3339DateFormattingIfNeeded(htmlHelper, "{0:yyyy-MM-ddTHH:mm:ss.fffK}");
|
|
return GenerateTextBox(htmlHelper, inputType: "datetime");
|
|
}
|
|
|
|
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 NumberInputTemplate(IHtmlHelper htmlHelper)
|
|
{
|
|
return GenerateTextBox(htmlHelper, inputType: "number");
|
|
}
|
|
|
|
public static IHtmlContent FileInputTemplate(IHtmlHelper htmlHelper)
|
|
{
|
|
if (htmlHelper == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(htmlHelper));
|
|
}
|
|
|
|
return GenerateTextBox(htmlHelper, inputType: "file");
|
|
}
|
|
|
|
public static IHtmlContent FileCollectionInputTemplate(IHtmlHelper htmlHelper)
|
|
{
|
|
if (htmlHelper == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(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(
|
|
current: null,
|
|
value: value,
|
|
format: null,
|
|
htmlAttributes: htmlAttributes);
|
|
}
|
|
}
|
|
}
|