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:
N. Taylor Mullen 2014-04-02 14:13:37 -07:00
parent fc01cf6eea
commit 5da827b58f
7 changed files with 317 additions and 25 deletions

View File

@ -46,8 +46,14 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
set { _convertEmptyStringToNull = value; }
}
public virtual string DataTypeName { get; set; }
public virtual string Description { get; set; }
public virtual string DisplayFormatString { get; set; }
public virtual string EditFormatString { get; set; }
public virtual bool IsComplexType
{
get { return !ModelType.HasStringConverter(); }
@ -91,6 +97,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
get { return _modelType; }
}
public virtual string NullDisplayText { get; set; }
public virtual IEnumerable<ModelMetadata> Properties
{
get
@ -111,9 +119,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
protected IModelMetadataProvider Provider { get; set; }
/// <returns>
/// Gets TModel if ModelType is Nullable(TModel), ModelType otherwise.
/// Gets TModel if ModelType is Nullable{TModel}, ModelType otherwise.
/// </returns>
internal Type RealModelType
public Type RealModelType
{
get
{
@ -133,6 +141,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
}
public virtual string TemplateHint { get; set; }
internal EfficientTypePropertyKey<Type, string> CacheKey
{
get

View File

@ -41,6 +41,8 @@ namespace Microsoft.AspNet.Mvc.Rendering
IdAttributeDotReplacement = "_";
}
public IModelMetadataProvider MetadataProvider { get; private set; }
public string IdAttributeDotReplacement { get; set; }
public HttpContext HttpContext { get; private set; }
@ -81,7 +83,43 @@ namespace Microsoft.AspNet.Mvc.Rendering
protected IModelMetadataProvider MetadataProvider { get; private set; }
/// <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.
/// <example>
/// 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>
public static IDictionary<string, object> AnonymousObjectToHtmlAttributes(object htmlAttributes)
{
Dictionary<string, object> result;
var valuesAsDictionary = htmlAttributes as IDictionary<string, object>;
if (valuesAsDictionary != null)
{
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;
// NOTE: This should be doing more than just returning a generic conversion from obj -> dict
// Once GitHub #80 has been completed this will do more than be a call through.
return ObjectToDictionary(htmlAttributes);
}
public virtual void Contextualize([NotNull] ViewContext viewContext)

View File

@ -13,7 +13,7 @@ namespace Microsoft.AspNet.Mvc.Rendering
/// Initializes a new instance of the <see cref="HtmlHelper{TModel}"/> class.
/// </summary>
public HtmlHelper([NotNull] IViewEngine viewEngine, [NotNull] IModelMetadataProvider metadataProvider)
: base(viewEngine, metadataProvider)
: base(viewEngine, metadataProvider)
{
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}
}
}

View File

@ -187,6 +187,22 @@ namespace Microsoft.AspNet.Mvc.Rendering
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>
/// The model item passed is null, but this ViewDataDictionary instance requires a non-null model item of type '{0}'.
/// </summary>

View File

@ -147,6 +147,9 @@
<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>
</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">
<value>The model item passed is null, but this ViewDataDictionary instance requires a non-null model item of type '{0}'.</value>
</data>