From 5da827b58f1bc9b49c0b0b20cb16d3e1bcb33b87 Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Wed, 2 Apr 2014 14:13:37 -0700 Subject: [PATCH] Add infrastructure for templated display helpers. I changed code paths for how we render templates from the old world to better separate code. --- .../Metadata/ModelMetadata.cs | 14 +- .../Html/HtmlHelper.cs | 64 +++++--- .../Html/HtmlHelperOfT.cs | 2 +- .../Html/TemplatedHelpers/TemplateBuilder.cs | 92 +++++++++++ .../Html/TemplatedHelpers/TemplateRenderer.cs | 151 ++++++++++++++++++ .../Properties/Resources.Designer.cs | 16 ++ .../Properties/Resources.resx | 3 + 7 files changed, 317 insertions(+), 25 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Rendering/Html/TemplatedHelpers/TemplateBuilder.cs create mode 100644 src/Microsoft.AspNet.Mvc.Rendering/Html/TemplatedHelpers/TemplateRenderer.cs diff --git a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs index b77ed3699d..3d1f853f79 100644 --- a/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs +++ b/src/Microsoft.AspNet.Mvc.ModelBinding/Metadata/ModelMetadata.cs @@ -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 Properties { get @@ -111,9 +119,9 @@ namespace Microsoft.AspNet.Mvc.ModelBinding protected IModelMetadataProvider Provider { get; set; } /// - /// Gets TModel if ModelType is Nullable(TModel), ModelType otherwise. + /// Gets TModel if ModelType is Nullable{TModel}, ModelType otherwise. /// - internal Type RealModelType + public Type RealModelType { get { @@ -133,6 +141,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding } } + public virtual string TemplateHint { get; set; } + internal EfficientTypePropertyKey CacheKey { get diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelper.cs b/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelper.cs index 49c89e89a8..68c0a5863b 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelper.cs @@ -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; } /// - /// 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. + /// + /// + /// new { property_name = "value" } will translate to the entry { "property_name" , "value" } + /// in the resulting dictionary. + /// + /// The object to be converted. + /// The created dictionary of property names and property values. + public static IDictionary ObjectToDictionary(object obj) + { + IDictionary result; + var valuesAsDictionary = obj as IDictionary; + if (valuesAsDictionary != null) + { + result = new Dictionary(valuesAsDictionary, StringComparer.OrdinalIgnoreCase); + } + else + { + result = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (obj != null) + { + foreach (var prop in obj.GetType().GetRuntimeProperties()) + { + var value = prop.GetValue(obj); + result.Add(prop.Name, value); + } + } + } + + return result; + } + + /// + /// Creates a dictionary of HTML attributes from the input object, /// translating underscores to dashes. /// /// new { data_name="value" } will translate to the entry { "data-name" , "value" } @@ -92,27 +130,9 @@ namespace Microsoft.AspNet.Mvc.Rendering /// A dictionary that represents HTML attributes. public static IDictionary AnonymousObjectToHtmlAttributes(object htmlAttributes) { - Dictionary result; - var valuesAsDictionary = htmlAttributes as IDictionary; - if (valuesAsDictionary != null) - { - result = new Dictionary(valuesAsDictionary, StringComparer.OrdinalIgnoreCase); - } - else - { - result = new Dictionary(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) diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelperOfT.cs b/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelperOfT.cs index aaec0c699d..c8fa02880e 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelperOfT.cs +++ b/src/Microsoft.AspNet.Mvc.Rendering/Html/HtmlHelperOfT.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNet.Mvc.Rendering /// Initializes a new instance of the class. /// public HtmlHelper([NotNull] IViewEngine viewEngine, [NotNull] IModelMetadataProvider metadataProvider) - : base(viewEngine, metadataProvider) + : base(viewEngine, metadataProvider) { } diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Html/TemplatedHelpers/TemplateBuilder.cs b/src/Microsoft.AspNet.Mvc.Rendering/Html/TemplatedHelpers/TemplateBuilder.cs new file mode 100644 index 0000000000..ab1605cb2e --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Rendering/Html/TemplatedHelpers/TemplateBuilder.cs @@ -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 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(); + } + + + } +} diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Html/TemplatedHelpers/TemplateRenderer.cs b/src/Microsoft.AspNet.Mvc.Rendering/Html/TemplatedHelpers/TemplateRenderer.cs new file mode 100644 index 0000000000..6731ae1f10 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Rendering/Html/TemplatedHelpers/TemplateRenderer.cs @@ -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, Task> defaultAction; + if (defaultActions.TryGetValue(viewName, out defaultAction)) + { + // Right now there's no IhtmlHelper 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, Task>> GetDefaultActions() + { + // TODO: Implement default templates + return new Dictionary, Task>>(StringComparer.OrdinalIgnoreCase); + } + + private IEnumerable 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, we want to search for T (which should handle both T and Nullable) + 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; + } + } + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.Designer.cs index 4c109eedc7..df90954f8c 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.Designer.cs @@ -187,6 +187,22 @@ namespace Microsoft.AspNet.Mvc.Rendering return GetString("TemplateHelpers_TemplateLimitations"); } + /// + /// Unable to locate an appropriate template for type {0}. + /// + internal static string TemplateHelpers_NoTemplate + { + get { return GetString("TemplateHelpers_NoTemplate"); } + } + + /// + /// Unable to locate an appropriate template for type {0}. + /// + internal static string FormatTemplateHelpers_NoTemplate(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("TemplateHelpers_NoTemplate"), p0); + } + /// /// The model item passed is null, but this ViewDataDictionary instance requires a non-null model item of type '{0}'. /// diff --git a/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.resx b/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.resx index 82726bc1a2..ba0733c1eb 100644 --- a/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Rendering/Properties/Resources.resx @@ -147,6 +147,9 @@ Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions. + + Unable to locate an appropriate template for type {0}. + The model item passed is null, but this ViewDataDictionary instance requires a non-null model item of type '{0}'.