1 /* ****************************************************************************
\r
3 * Copyright (c) Microsoft Corporation. All rights reserved.
\r
5 * This software is subject to the Microsoft Public License (Ms-PL).
\r
6 * A copy of the license can be found in the license.htm file included
\r
7 * in this distribution.
\r
9 * You must not remove this notice, or any other, from this software.
\r
11 * ***************************************************************************/
\r
13 namespace System.Web.Mvc.Html {
\r
15 using System.Collections;
\r
16 using System.Collections.Generic;
\r
17 using System.Globalization;
\r
20 using System.Linq.Expressions;
\r
21 using System.Web.Mvc.Resources;
\r
22 using System.Web.Routing;
\r
23 using System.Web.UI.WebControls;
\r
25 internal static class TemplateHelpers {
\r
26 static readonly Dictionary<DataBoundControlMode, string> modeViewPaths =
\r
27 new Dictionary<DataBoundControlMode, string> {
\r
28 { DataBoundControlMode.ReadOnly, "DisplayTemplates" },
\r
29 { DataBoundControlMode.Edit, "EditorTemplates" }
\r
32 static readonly Dictionary<string, Func<HtmlHelper, string>> defaultDisplayActions =
\r
33 new Dictionary<string, Func<HtmlHelper, string>>(StringComparer.OrdinalIgnoreCase) {
\r
34 { "EmailAddress", DefaultDisplayTemplates.EmailAddressTemplate },
\r
35 { "HiddenInput", DefaultDisplayTemplates.HiddenInputTemplate },
\r
36 { "Html", DefaultDisplayTemplates.HtmlTemplate },
\r
37 { "Text", DefaultDisplayTemplates.StringTemplate },
\r
38 { "Url", DefaultDisplayTemplates.UrlTemplate },
\r
39 { "Collection", DefaultDisplayTemplates.CollectionTemplate },
\r
40 { typeof(bool).Name, DefaultDisplayTemplates.BooleanTemplate },
\r
41 { typeof(decimal).Name, DefaultDisplayTemplates.DecimalTemplate },
\r
42 { typeof(string).Name, DefaultDisplayTemplates.StringTemplate },
\r
43 { typeof(object).Name, DefaultDisplayTemplates.ObjectTemplate },
\r
46 static readonly Dictionary<string, Func<HtmlHelper, string>> defaultEditorActions =
\r
47 new Dictionary<string, Func<HtmlHelper, string>>(StringComparer.OrdinalIgnoreCase) {
\r
48 { "HiddenInput", DefaultEditorTemplates.HiddenInputTemplate },
\r
49 { "MultilineText", DefaultEditorTemplates.MultilineTextTemplate },
\r
50 { "Password", DefaultEditorTemplates.PasswordTemplate },
\r
51 { "Text", DefaultEditorTemplates.StringTemplate },
\r
52 { "Collection", DefaultEditorTemplates.CollectionTemplate },
\r
53 { typeof(bool).Name, DefaultEditorTemplates.BooleanTemplate },
\r
54 { typeof(decimal).Name, DefaultEditorTemplates.DecimalTemplate },
\r
55 { typeof(string).Name, DefaultEditorTemplates.StringTemplate },
\r
56 { typeof(object).Name, DefaultEditorTemplates.ObjectTemplate },
\r
59 internal static string cacheItemId = Guid.NewGuid().ToString();
\r
61 internal delegate string ExecuteTemplateDelegate(HtmlHelper html, ViewDataDictionary viewData, string templateName, DataBoundControlMode mode, GetViewNamesDelegate getViewNames);
\r
63 internal static string ExecuteTemplate(HtmlHelper html, ViewDataDictionary viewData, string templateName, DataBoundControlMode mode, GetViewNamesDelegate getViewNames) {
\r
64 Dictionary<string, ActionCacheItem> actionCache = GetActionCache(html);
\r
65 Dictionary<string, Func<HtmlHelper, string>> defaultActions = mode == DataBoundControlMode.ReadOnly ? defaultDisplayActions : defaultEditorActions;
\r
66 string modeViewPath = modeViewPaths[mode];
\r
68 foreach (string viewName in getViewNames(viewData.ModelMetadata, templateName, viewData.ModelMetadata.TemplateHint, viewData.ModelMetadata.DataTypeName)) {
\r
69 string fullViewName = modeViewPath + "/" + viewName;
\r
70 ActionCacheItem cacheItem;
\r
72 if (actionCache.TryGetValue(fullViewName, out cacheItem)) {
\r
73 if (cacheItem != null) {
\r
74 return cacheItem.Execute(html, viewData);
\r
78 ViewEngineResult viewEngineResult = ViewEngines.Engines.FindPartialView(html.ViewContext, fullViewName);
\r
79 if (viewEngineResult.View != null) {
\r
80 actionCache[fullViewName] = new ActionCacheViewItem { ViewName = fullViewName };
\r
82 StringWriter writer = new StringWriter(CultureInfo.InvariantCulture);
\r
83 viewEngineResult.View.Render(new ViewContext(html.ViewContext, viewEngineResult.View, viewData, html.ViewContext.TempData, writer), writer);
\r
84 return writer.ToString();
\r
87 Func<HtmlHelper, string> defaultAction;
\r
88 if (defaultActions.TryGetValue(viewName, out defaultAction)) {
\r
89 actionCache[fullViewName] = new ActionCacheCodeItem { Action = defaultAction };
\r
90 return defaultAction(
\r
92 new ViewContext(html.ViewContext, html.ViewContext.View, viewData, html.ViewContext.TempData, html.ViewContext.Writer),
\r
93 html.ViewDataContainer
\r
98 actionCache[fullViewName] = null;
\r
102 throw new InvalidOperationException(
\r
104 CultureInfo.CurrentCulture,
\r
105 MvcResources.TemplateHelpers_NoTemplate,
\r
106 viewData.ModelMetadata.RealModelType.FullName
\r
111 internal static Dictionary<string, ActionCacheItem> GetActionCache(HtmlHelper html) {
\r
112 HttpContextBase context = html.ViewContext.HttpContext;
\r
113 Dictionary<string, ActionCacheItem> result;
\r
115 if (!context.Items.Contains(cacheItemId)) {
\r
116 result = new Dictionary<string, ActionCacheItem>();
\r
117 context.Items[cacheItemId] = result;
\r
120 result = (Dictionary<string, ActionCacheItem>)context.Items[cacheItemId];
\r
126 internal delegate IEnumerable<string> GetViewNamesDelegate(ModelMetadata metadata, params string[] templateHints);
\r
128 internal static IEnumerable<string> GetViewNames(ModelMetadata metadata, params string[] templateHints) {
\r
129 foreach (string templateHint in templateHints.Where(s => !String.IsNullOrEmpty(s))) {
\r
130 yield return templateHint;
\r
133 // We don't want to search for Nullable<T>, we want to search for T (which should handle both T and Nullable<T>)
\r
134 Type fieldType = Nullable.GetUnderlyingType(metadata.RealModelType) ?? metadata.RealModelType;
\r
136 // TODO: Make better string names for generic types
\r
137 yield return fieldType.Name;
\r
139 if (!metadata.IsComplexType) {
\r
140 yield return "String";
\r
142 else if (fieldType.IsInterface) {
\r
143 if (typeof(IEnumerable).IsAssignableFrom(fieldType)) {
\r
144 yield return "Collection";
\r
147 yield return "Object";
\r
150 bool isEnumerable = typeof(IEnumerable).IsAssignableFrom(fieldType);
\r
153 fieldType = fieldType.BaseType;
\r
154 if (fieldType == null)
\r
157 if (isEnumerable && fieldType == typeof(Object)) {
\r
158 yield return "Collection";
\r
161 yield return fieldType.Name;
\r
166 internal static MvcHtmlString Template(HtmlHelper html, string expression, string templateName, string htmlFieldName, DataBoundControlMode mode, object additionalViewData) {
\r
167 return MvcHtmlString.Create(Template(html, expression, templateName, htmlFieldName, mode, additionalViewData, TemplateHelper));
\r
170 // Unit testing version
\r
171 internal static string Template(HtmlHelper html, string expression, string templateName, string htmlFieldName,
\r
172 DataBoundControlMode mode, object additionalViewData, TemplateHelperDelegate templateHelper) {
\r
173 return templateHelper(html,
\r
174 ModelMetadata.FromStringExpression(expression, html.ViewData),
\r
175 htmlFieldName ?? ExpressionHelper.GetExpressionText(expression),
\r
178 additionalViewData);
\r
181 internal static MvcHtmlString TemplateFor<TContainer, TValue>(this HtmlHelper<TContainer> html, Expression<Func<TContainer, TValue>> expression,
\r
182 string templateName, string htmlFieldName, DataBoundControlMode mode,
\r
183 object additionalViewData) {
\r
184 return MvcHtmlString.Create(TemplateFor(html, expression, templateName, htmlFieldName, mode, additionalViewData, TemplateHelper));
\r
187 // Unit testing version
\r
188 internal static string TemplateFor<TContainer, TValue>(this HtmlHelper<TContainer> html, Expression<Func<TContainer, TValue>> expression,
\r
189 string templateName, string htmlFieldName, DataBoundControlMode mode,
\r
190 object additionalViewData, TemplateHelperDelegate templateHelper) {
\r
191 return templateHelper(html,
\r
192 ModelMetadata.FromLambdaExpression(expression, html.ViewData),
\r
193 htmlFieldName ?? ExpressionHelper.GetExpressionText(expression),
\r
196 additionalViewData);
\r
199 internal delegate string TemplateHelperDelegate(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string templateName, DataBoundControlMode mode, object additionalViewData);
\r
201 internal static string TemplateHelper(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string templateName, DataBoundControlMode mode, object additionalViewData) {
\r
202 return TemplateHelper(html, metadata, htmlFieldName, templateName, mode, additionalViewData, ExecuteTemplate);
\r
205 internal static string TemplateHelper(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string templateName, DataBoundControlMode mode, object additionalViewData, ExecuteTemplateDelegate executeTemplate) {
\r
206 // TODO: Convert Editor into Display if model.IsReadOnly is true? Need to be careful about this because
\r
207 // the Model property on the ViewPage/ViewUserControl is get-only, so the type descriptor automatically
\r
208 // decorates it with a [ReadOnly] attribute...
\r
210 if (metadata.ConvertEmptyStringToNull && String.Empty.Equals(metadata.Model)) {
\r
211 metadata.Model = null;
\r
214 object formattedModelValue = metadata.Model;
\r
215 if (metadata.Model == null && mode == DataBoundControlMode.ReadOnly) {
\r
216 formattedModelValue = metadata.NullDisplayText;
\r
219 string formatString = mode == DataBoundControlMode.ReadOnly ? metadata.DisplayFormatString : metadata.EditFormatString;
\r
220 if (metadata.Model != null && !String.IsNullOrEmpty(formatString)) {
\r
221 formattedModelValue = String.Format(CultureInfo.CurrentCulture, formatString, metadata.Model);
\r
224 // Normally this shouldn't happen, unless someone writes their own custom Object templates which
\r
225 // don't check to make sure that the object hasn't already been displayed
\r
226 object visitedObjectsKey = metadata.Model ?? metadata.RealModelType;
\r
227 if (html.ViewDataContainer.ViewData.TemplateInfo.VisitedObjects.Contains(visitedObjectsKey)) { // DDB #224750
\r
228 return String.Empty;
\r
231 ViewDataDictionary viewData = new ViewDataDictionary(html.ViewDataContainer.ViewData) {
\r
232 Model = metadata.Model,
\r
233 ModelMetadata = metadata,
\r
234 TemplateInfo = new TemplateInfo {
\r
235 FormattedModelValue = formattedModelValue,
\r
236 HtmlFieldPrefix = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName),
\r
237 VisitedObjects = new HashSet<object>(html.ViewContext.ViewData.TemplateInfo.VisitedObjects), // DDB #224750
\r
241 if (additionalViewData != null) {
\r
242 foreach (KeyValuePair<string, object> kvp in new RouteValueDictionary(additionalViewData)) {
\r
243 viewData[kvp.Key] = kvp.Value;
\r
247 viewData.TemplateInfo.VisitedObjects.Add(visitedObjectsKey); // DDB #224750
\r
249 return executeTemplate(html, viewData, templateName, mode, GetViewNames);
\r
252 internal abstract class ActionCacheItem {
\r
253 public abstract string Execute(HtmlHelper html, ViewDataDictionary viewData);
\r
256 internal class ActionCacheCodeItem : ActionCacheItem {
\r
257 public Func<HtmlHelper, string> Action { get; set; }
\r
259 public override string Execute(HtmlHelper html, ViewDataDictionary viewData) {
\r
260 ViewContext newViewContext = new ViewContext(html.ViewContext, html.ViewContext.View, viewData, html.ViewContext.TempData, html.ViewContext.Writer);
\r
261 HtmlHelper newHtmlHelper = new HtmlHelper(newViewContext, html.ViewDataContainer);
\r
262 return Action(newHtmlHelper);
\r
266 internal class ActionCacheViewItem : ActionCacheItem {
\r
267 public string ViewName { get; set; }
\r
269 public override string Execute(HtmlHelper html, ViewDataDictionary viewData) {
\r
270 ViewEngineResult viewEngineResult = ViewEngines.Engines.FindPartialView(html.ViewContext, ViewName);
\r
271 StringWriter writer = new StringWriter(CultureInfo.InvariantCulture);
\r
272 viewEngineResult.View.Render(new ViewContext(html.ViewContext, viewEngineResult.View, viewData, html.ViewContext.TempData, writer), writer);
\r
273 return writer.ToString();
\r