Add infrastructure for templated display helpers.
I changed code paths for how we render templates from the old world to better separate code.
This commit is contained in:
parent
fc01cf6eea
commit
5da827b58f
|
|
@ -46,8 +46,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
set { _convertEmptyStringToNull = value; }
|
set { _convertEmptyStringToNull = value; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual string DataTypeName { get; set; }
|
||||||
|
|
||||||
public virtual string Description { get; set; }
|
public virtual string Description { get; set; }
|
||||||
|
|
||||||
|
public virtual string DisplayFormatString { get; set; }
|
||||||
|
|
||||||
|
public virtual string EditFormatString { get; set; }
|
||||||
|
|
||||||
public virtual bool IsComplexType
|
public virtual bool IsComplexType
|
||||||
{
|
{
|
||||||
get { return !ModelType.HasStringConverter(); }
|
get { return !ModelType.HasStringConverter(); }
|
||||||
|
|
@ -91,6 +97,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
get { return _modelType; }
|
get { return _modelType; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual string NullDisplayText { get; set; }
|
||||||
|
|
||||||
public virtual IEnumerable<ModelMetadata> Properties
|
public virtual IEnumerable<ModelMetadata> Properties
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
@ -111,9 +119,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
protected IModelMetadataProvider Provider { get; set; }
|
protected IModelMetadataProvider Provider { get; set; }
|
||||||
|
|
||||||
/// <returns>
|
/// <returns>
|
||||||
/// Gets TModel if ModelType is Nullable(TModel), ModelType otherwise.
|
/// Gets TModel if ModelType is Nullable{TModel}, ModelType otherwise.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
internal Type RealModelType
|
public Type RealModelType
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
|
|
@ -133,6 +141,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public virtual string TemplateHint { get; set; }
|
||||||
|
|
||||||
internal EfficientTypePropertyKey<Type, string> CacheKey
|
internal EfficientTypePropertyKey<Type, string> CacheKey
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,8 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
IdAttributeDotReplacement = "_";
|
IdAttributeDotReplacement = "_";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IModelMetadataProvider MetadataProvider { get; private set; }
|
||||||
|
|
||||||
public string IdAttributeDotReplacement { get; set; }
|
public string IdAttributeDotReplacement { get; set; }
|
||||||
|
|
||||||
public HttpContext HttpContext { get; private set; }
|
public HttpContext HttpContext { get; private set; }
|
||||||
|
|
@ -81,7 +83,43 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
protected IModelMetadataProvider MetadataProvider { get; private set; }
|
protected IModelMetadataProvider MetadataProvider { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a dictionary of HTML attributes from the input object,
|
/// Creates a dictionary from an object, by adding each public instance property as a key with its associated
|
||||||
|
/// value to the dictionary. It will expose public properties from derived types as well. This is typically used
|
||||||
|
/// with objects of an anonymous type.
|
||||||
|
/// </summary>
|
||||||
|
/// <example>
|
||||||
|
/// <c>new { property_name = "value" }</c> will translate to the entry <c>{ "property_name" , "value" }</c>
|
||||||
|
/// in the resulting dictionary.
|
||||||
|
/// </example>
|
||||||
|
/// <param name="obj">The object to be converted.</param>
|
||||||
|
/// <returns>The created dictionary of property names and property values.</returns>
|
||||||
|
public static IDictionary<string, object> ObjectToDictionary(object obj)
|
||||||
|
{
|
||||||
|
IDictionary<string, object> result;
|
||||||
|
var valuesAsDictionary = obj as IDictionary<string, object>;
|
||||||
|
if (valuesAsDictionary != null)
|
||||||
|
{
|
||||||
|
result = new Dictionary<string, object>(valuesAsDictionary, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (obj != null)
|
||||||
|
{
|
||||||
|
foreach (var prop in obj.GetType().GetRuntimeProperties())
|
||||||
|
{
|
||||||
|
var value = prop.GetValue(obj);
|
||||||
|
result.Add(prop.Name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a dictionary of HTML attributes from the input object,
|
||||||
/// translating underscores to dashes.
|
/// translating underscores to dashes.
|
||||||
/// <example>
|
/// <example>
|
||||||
/// new { data_name="value" } will translate to the entry { "data-name" , "value" }
|
/// new { data_name="value" } will translate to the entry { "data-name" , "value" }
|
||||||
|
|
@ -92,27 +130,9 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
/// <returns>A dictionary that represents HTML attributes.</returns>
|
/// <returns>A dictionary that represents HTML attributes.</returns>
|
||||||
public static IDictionary<string, object> AnonymousObjectToHtmlAttributes(object htmlAttributes)
|
public static IDictionary<string, object> AnonymousObjectToHtmlAttributes(object htmlAttributes)
|
||||||
{
|
{
|
||||||
Dictionary<string, object> result;
|
// NOTE: This should be doing more than just returning a generic conversion from obj -> dict
|
||||||
var valuesAsDictionary = htmlAttributes as IDictionary<string, object>;
|
// Once GitHub #80 has been completed this will do more than be a call through.
|
||||||
if (valuesAsDictionary != null)
|
return ObjectToDictionary(htmlAttributes);
|
||||||
{
|
|
||||||
result = new Dictionary<string, object>(valuesAsDictionary, StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
if (htmlAttributes != null)
|
|
||||||
{
|
|
||||||
foreach (var prop in htmlAttributes.GetType().GetRuntimeProperties())
|
|
||||||
{
|
|
||||||
var value = prop.GetValue(htmlAttributes);
|
|
||||||
result.Add(prop.Name, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void Contextualize([NotNull] ViewContext viewContext)
|
public virtual void Contextualize([NotNull] ViewContext viewContext)
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
/// Initializes a new instance of the <see cref="HtmlHelper{TModel}"/> class.
|
/// Initializes a new instance of the <see cref="HtmlHelper{TModel}"/> class.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public HtmlHelper([NotNull] IViewEngine viewEngine, [NotNull] IModelMetadataProvider metadataProvider)
|
public HtmlHelper([NotNull] IViewEngine viewEngine, [NotNull] IModelMetadataProvider metadataProvider)
|
||||||
: base(viewEngine, metadataProvider)
|
: base(viewEngine, metadataProvider)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using Microsoft.AspNet.Mvc.ModelBinding;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
|
{
|
||||||
|
internal class TemplateBuilder
|
||||||
|
{
|
||||||
|
private IViewEngine _viewEngine;
|
||||||
|
private ViewContext _viewContext;
|
||||||
|
private ViewDataDictionary _viewData;
|
||||||
|
private ModelMetadata _metadata;
|
||||||
|
private string _htmlFieldName;
|
||||||
|
private string _templateName;
|
||||||
|
private bool _readOnly;
|
||||||
|
private object _additionalViewData;
|
||||||
|
|
||||||
|
public TemplateBuilder([NotNull] IViewEngine viewEngine,
|
||||||
|
[NotNull] ViewContext viewContext,
|
||||||
|
[NotNull] ViewDataDictionary viewData,
|
||||||
|
[NotNull] ModelMetadata metadata,
|
||||||
|
string htmlFieldName,
|
||||||
|
string templateName,
|
||||||
|
bool readOnly,
|
||||||
|
object additionalViewData)
|
||||||
|
{
|
||||||
|
_viewEngine = viewEngine;
|
||||||
|
_viewContext = viewContext;
|
||||||
|
_viewData = viewData;
|
||||||
|
_metadata = metadata;
|
||||||
|
_htmlFieldName = htmlFieldName;
|
||||||
|
_templateName = templateName;
|
||||||
|
_readOnly = readOnly;
|
||||||
|
_additionalViewData = additionalViewData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Build()
|
||||||
|
{
|
||||||
|
if (_metadata.ConvertEmptyStringToNull && string.Empty.Equals(_metadata.Model))
|
||||||
|
{
|
||||||
|
_metadata.Model = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var formattedModelValue = _metadata.Model;
|
||||||
|
if (_metadata.Model == null && _readOnly)
|
||||||
|
{
|
||||||
|
formattedModelValue = _metadata.NullDisplayText;
|
||||||
|
}
|
||||||
|
|
||||||
|
var formatString = _readOnly ? _metadata.DisplayFormatString : _metadata.EditFormatString;
|
||||||
|
|
||||||
|
if (_metadata.Model != null && !string.IsNullOrEmpty(formatString))
|
||||||
|
{
|
||||||
|
formattedModelValue = string.Format(CultureInfo.CurrentCulture, formatString, _metadata.Model);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normally this shouldn't happen, unless someone writes their own custom Object templates which
|
||||||
|
// don't check to make sure that the object hasn't already been displayed
|
||||||
|
if (_viewData.TemplateInfo.Visited(_metadata))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var viewData = new ViewDataDictionary(_viewData)
|
||||||
|
{
|
||||||
|
Model = _metadata.Model,
|
||||||
|
ModelMetadata = _metadata
|
||||||
|
};
|
||||||
|
|
||||||
|
viewData.TemplateInfo.FormattedModelValue = formattedModelValue;
|
||||||
|
viewData.TemplateInfo.HtmlFieldPrefix = _viewData.TemplateInfo.GetFullHtmlFieldName(_htmlFieldName);
|
||||||
|
|
||||||
|
if (_additionalViewData != null)
|
||||||
|
{
|
||||||
|
foreach (KeyValuePair<string, object> kvp in HtmlHelper.ObjectToDictionary(_additionalViewData))
|
||||||
|
{
|
||||||
|
viewData[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object visitedObjectsKey = _metadata.Model ?? _metadata.RealModelType;
|
||||||
|
viewData.TemplateInfo.AddVisited(visitedObjectsKey);
|
||||||
|
|
||||||
|
var templateRenderer = new TemplateRenderer(_viewEngine, _viewContext, viewData, _templateName, _readOnly);
|
||||||
|
|
||||||
|
return templateRenderer.Render();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNet.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
|
{
|
||||||
|
internal class TemplateRenderer
|
||||||
|
{
|
||||||
|
private static readonly string DisplayTemplateViewPath = "DisplayTemplates";
|
||||||
|
private static readonly string EditorTemplateViewPath = "EditorTemplates";
|
||||||
|
|
||||||
|
private ViewContext _viewContext;
|
||||||
|
private ViewDataDictionary _viewData;
|
||||||
|
private IViewEngine _viewEngine;
|
||||||
|
private string _templateName;
|
||||||
|
private bool _readOnly;
|
||||||
|
|
||||||
|
public TemplateRenderer([NotNull] IViewEngine viewEngine,
|
||||||
|
[NotNull] ViewContext viewContext,
|
||||||
|
[NotNull] ViewDataDictionary viewData,
|
||||||
|
string templateName,
|
||||||
|
bool readOnly)
|
||||||
|
{
|
||||||
|
_viewEngine = viewEngine;
|
||||||
|
_viewContext = viewContext;
|
||||||
|
_viewData = viewData;
|
||||||
|
_templateName = templateName;
|
||||||
|
_readOnly = readOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Render()
|
||||||
|
{
|
||||||
|
var defaultActions = GetDefaultActions();
|
||||||
|
var modeViewPath = _readOnly ? DisplayTemplateViewPath : EditorTemplateViewPath;
|
||||||
|
|
||||||
|
foreach (string viewName in GetViewNames())
|
||||||
|
{
|
||||||
|
var fullViewName = modeViewPath + "/" + viewName;
|
||||||
|
|
||||||
|
// Forcing synchronous behavior so users don't have to await templates.
|
||||||
|
var viewEngineResult = _viewEngine.FindPartialView(_viewContext.ViewEngineContext, fullViewName).Result;
|
||||||
|
if (viewEngineResult.Success)
|
||||||
|
{
|
||||||
|
using (var writer = new StringWriter(CultureInfo.InvariantCulture))
|
||||||
|
{
|
||||||
|
// Forcing synchronous behavior so users don't have to await templates.
|
||||||
|
// TODO: Pass through TempData once implemented.
|
||||||
|
viewEngineResult.View.RenderAsync(new ViewContext(_viewContext)
|
||||||
|
{
|
||||||
|
ViewData = _viewData,
|
||||||
|
Writer = writer,
|
||||||
|
}).Wait();
|
||||||
|
|
||||||
|
return writer.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Func<IHtmlHelper<object>, Task<string>> defaultAction;
|
||||||
|
if (defaultActions.TryGetValue(viewName, out defaultAction))
|
||||||
|
{
|
||||||
|
// Right now there's no IhtmlHelper<object> pass in or default templates so this will be
|
||||||
|
// changed once a decision has been reached.
|
||||||
|
return defaultAction(null).Result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException(Resources.FormatTemplateHelpers_NoTemplate(_viewData.ModelMetadata.RealModelType.FullName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<string, Func<IHtmlHelper<object>, Task<string>>> GetDefaultActions()
|
||||||
|
{
|
||||||
|
// TODO: Implement default templates
|
||||||
|
return new Dictionary<string, Func<IHtmlHelper<object>, Task<string>>>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<string> GetViewNames()
|
||||||
|
{
|
||||||
|
var metadata = _viewData.ModelMetadata;
|
||||||
|
var templateHints = new string[] {
|
||||||
|
_templateName,
|
||||||
|
metadata.TemplateHint,
|
||||||
|
metadata.DataTypeName
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (string templateHint in templateHints.Where(s => !string.IsNullOrEmpty(s)))
|
||||||
|
{
|
||||||
|
yield return templateHint;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't want to search for Nullable<T>, we want to search for T (which should handle both T and Nullable<T>)
|
||||||
|
var fieldType = Nullable.GetUnderlyingType(metadata.RealModelType) ?? metadata.RealModelType;
|
||||||
|
|
||||||
|
yield return fieldType.Name;
|
||||||
|
|
||||||
|
if (fieldType == typeof(string))
|
||||||
|
{
|
||||||
|
// Nothing more to provide
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
else if (!metadata.IsComplexType)
|
||||||
|
{
|
||||||
|
// IsEnum is false for the Enum class itself
|
||||||
|
if (fieldType.IsEnum)
|
||||||
|
{
|
||||||
|
// Same as fieldType.BaseType.Name in this case
|
||||||
|
yield return "Enum";
|
||||||
|
}
|
||||||
|
else if (fieldType == typeof(DateTimeOffset))
|
||||||
|
{
|
||||||
|
yield return "DateTime";
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return "String";
|
||||||
|
}
|
||||||
|
else if (fieldType.IsInterface)
|
||||||
|
{
|
||||||
|
if (typeof(IEnumerable).IsAssignableFrom(fieldType))
|
||||||
|
{
|
||||||
|
yield return "Collection";
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return "Object";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bool isEnumerable = typeof(IEnumerable).IsAssignableFrom(fieldType);
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
fieldType = fieldType.BaseType;
|
||||||
|
if (fieldType == null)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEnumerable && fieldType == typeof(Object))
|
||||||
|
{
|
||||||
|
yield return "Collection";
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return fieldType.Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -187,6 +187,22 @@ namespace Microsoft.AspNet.Mvc.Rendering
|
||||||
return GetString("TemplateHelpers_TemplateLimitations");
|
return GetString("TemplateHelpers_TemplateLimitations");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unable to locate an appropriate template for type {0}.
|
||||||
|
/// </summary>
|
||||||
|
internal static string TemplateHelpers_NoTemplate
|
||||||
|
{
|
||||||
|
get { return GetString("TemplateHelpers_NoTemplate"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unable to locate an appropriate template for type {0}.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatTemplateHelpers_NoTemplate(object p0)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("TemplateHelpers_NoTemplate"), p0);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The model item passed is null, but this ViewDataDictionary instance requires a non-null model item of type '{0}'.
|
/// The model item passed is null, but this ViewDataDictionary instance requires a non-null model item of type '{0}'.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,9 @@
|
||||||
<data name="TemplateHelpers_TemplateLimitations" xml:space="preserve">
|
<data name="TemplateHelpers_TemplateLimitations" xml:space="preserve">
|
||||||
<value>Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions.</value>
|
<value>Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="TemplateHelpers_NoTemplate" xml:space="preserve">
|
||||||
|
<value>Unable to locate an appropriate template for type {0}.</value>
|
||||||
|
</data>
|
||||||
<data name="ViewData_ModelCannotBeNull" xml:space="preserve">
|
<data name="ViewData_ModelCannotBeNull" xml:space="preserve">
|
||||||
<value>The model item passed is null, but this ViewDataDictionary instance requires a non-null model item of type '{0}'.</value>
|
<value>The model item passed is null, but this ViewDataDictionary instance requires a non-null model item of type '{0}'.</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue