1 namespace System.Web.Mvc.Html {
3 using System.Collections;
4 using System.Collections.Generic;
5 using System.Globalization;
8 using System.Linq.Expressions;
9 using System.Web.Mvc.Resources;
10 using System.Web.Routing;
11 using System.Web.UI.WebControls;
13 internal static class TemplateHelpers {
14 static readonly Dictionary<DataBoundControlMode, string> modeViewPaths =
15 new Dictionary<DataBoundControlMode, string> {
16 { DataBoundControlMode.ReadOnly, "DisplayTemplates" },
17 { DataBoundControlMode.Edit, "EditorTemplates" }
20 static readonly Dictionary<string, Func<HtmlHelper, string>> defaultDisplayActions =
21 new Dictionary<string, Func<HtmlHelper, string>>(StringComparer.OrdinalIgnoreCase) {
22 { "EmailAddress", DefaultDisplayTemplates.EmailAddressTemplate },
23 { "HiddenInput", DefaultDisplayTemplates.HiddenInputTemplate },
24 { "Html", DefaultDisplayTemplates.HtmlTemplate },
25 { "Text", DefaultDisplayTemplates.StringTemplate },
26 { "Url", DefaultDisplayTemplates.UrlTemplate },
27 { "Collection", DefaultDisplayTemplates.CollectionTemplate },
28 { typeof(bool).Name, DefaultDisplayTemplates.BooleanTemplate },
29 { typeof(decimal).Name, DefaultDisplayTemplates.DecimalTemplate },
30 { typeof(string).Name, DefaultDisplayTemplates.StringTemplate },
31 { typeof(object).Name, DefaultDisplayTemplates.ObjectTemplate },
34 static readonly Dictionary<string, Func<HtmlHelper, string>> defaultEditorActions =
35 new Dictionary<string, Func<HtmlHelper, string>>(StringComparer.OrdinalIgnoreCase) {
36 { "HiddenInput", DefaultEditorTemplates.HiddenInputTemplate },
37 { "MultilineText", DefaultEditorTemplates.MultilineTextTemplate },
38 { "Password", DefaultEditorTemplates.PasswordTemplate },
39 { "Text", DefaultEditorTemplates.StringTemplate },
40 { "Collection", DefaultEditorTemplates.CollectionTemplate },
41 { typeof(bool).Name, DefaultEditorTemplates.BooleanTemplate },
42 { typeof(decimal).Name, DefaultEditorTemplates.DecimalTemplate },
43 { typeof(string).Name, DefaultEditorTemplates.StringTemplate },
44 { typeof(object).Name, DefaultEditorTemplates.ObjectTemplate },
47 internal static string cacheItemId = Guid.NewGuid().ToString();
49 internal delegate string ExecuteTemplateDelegate(HtmlHelper html, ViewDataDictionary viewData, string templateName, DataBoundControlMode mode, GetViewNamesDelegate getViewNames, GetDefaultActionsDelegate getDefaultActions);
51 internal static string ExecuteTemplate(HtmlHelper html, ViewDataDictionary viewData, string templateName, DataBoundControlMode mode, GetViewNamesDelegate getViewNames, GetDefaultActionsDelegate getDefaultActions) {
52 Dictionary<string, ActionCacheItem> actionCache = GetActionCache(html);
53 Dictionary<string, Func<HtmlHelper, string>> defaultActions = getDefaultActions(mode);
54 string modeViewPath = modeViewPaths[mode];
56 foreach (string viewName in getViewNames(viewData.ModelMetadata, templateName, viewData.ModelMetadata.TemplateHint, viewData.ModelMetadata.DataTypeName)) {
57 string fullViewName = modeViewPath + "/" + viewName;
58 ActionCacheItem cacheItem;
60 if (actionCache.TryGetValue(fullViewName, out cacheItem)) {
61 if (cacheItem != null) {
62 return cacheItem.Execute(html, viewData);
66 ViewEngineResult viewEngineResult = ViewEngines.Engines.FindPartialView(html.ViewContext, fullViewName);
67 if (viewEngineResult.View != null) {
68 actionCache[fullViewName] = new ActionCacheViewItem { ViewName = fullViewName };
70 using (StringWriter writer = new StringWriter(CultureInfo.InvariantCulture)) {
71 viewEngineResult.View.Render(new ViewContext(html.ViewContext, viewEngineResult.View, viewData, html.ViewContext.TempData, writer), writer);
72 return writer.ToString();
76 Func<HtmlHelper, string> defaultAction;
77 if (defaultActions.TryGetValue(viewName, out defaultAction)) {
78 actionCache[fullViewName] = new ActionCacheCodeItem { Action = defaultAction };
79 return defaultAction(MakeHtmlHelper(html, viewData));
82 actionCache[fullViewName] = null;
86 throw new InvalidOperationException(
88 CultureInfo.CurrentCulture,
89 MvcResources.TemplateHelpers_NoTemplate,
90 viewData.ModelMetadata.RealModelType.FullName
95 internal static Dictionary<string, ActionCacheItem> GetActionCache(HtmlHelper html) {
96 HttpContextBase context = html.ViewContext.HttpContext;
97 Dictionary<string, ActionCacheItem> result;
99 if (!context.Items.Contains(cacheItemId)) {
100 result = new Dictionary<string, ActionCacheItem>();
101 context.Items[cacheItemId] = result;
104 result = (Dictionary<string, ActionCacheItem>)context.Items[cacheItemId];
110 internal delegate Dictionary<string, Func<HtmlHelper, string>> GetDefaultActionsDelegate(DataBoundControlMode mode);
112 internal static Dictionary<string, Func<HtmlHelper, string>> GetDefaultActions(DataBoundControlMode mode) {
113 return mode == DataBoundControlMode.ReadOnly ? defaultDisplayActions : defaultEditorActions;
116 internal delegate IEnumerable<string> GetViewNamesDelegate(ModelMetadata metadata, params string[] templateHints);
118 internal static IEnumerable<string> GetViewNames(ModelMetadata metadata, params string[] templateHints) {
119 foreach (string templateHint in templateHints.Where(s => !String.IsNullOrEmpty(s))) {
120 yield return templateHint;
123 // We don't want to search for Nullable<T>, we want to search for T (which should handle both T and Nullable<T>)
124 Type fieldType = Nullable.GetUnderlyingType(metadata.RealModelType) ?? metadata.RealModelType;
126 // TODO: Make better string names for generic types
127 yield return fieldType.Name;
129 if (!metadata.IsComplexType) {
130 yield return "String";
132 else if (fieldType.IsInterface) {
133 if (typeof(IEnumerable).IsAssignableFrom(fieldType)) {
134 yield return "Collection";
137 yield return "Object";
140 bool isEnumerable = typeof(IEnumerable).IsAssignableFrom(fieldType);
143 fieldType = fieldType.BaseType;
144 if (fieldType == null)
147 if (isEnumerable && fieldType == typeof(Object)) {
148 yield return "Collection";
151 yield return fieldType.Name;
156 internal static MvcHtmlString Template(HtmlHelper html, string expression, string templateName, string htmlFieldName, DataBoundControlMode mode, object additionalViewData) {
157 return MvcHtmlString.Create(Template(html, expression, templateName, htmlFieldName, mode, additionalViewData, TemplateHelper));
160 // Unit testing version
161 internal static string Template(HtmlHelper html, string expression, string templateName, string htmlFieldName,
162 DataBoundControlMode mode, object additionalViewData, TemplateHelperDelegate templateHelper) {
163 return templateHelper(html,
164 ModelMetadata.FromStringExpression(expression, html.ViewData),
165 htmlFieldName ?? ExpressionHelper.GetExpressionText(expression),
171 internal static MvcHtmlString TemplateFor<TContainer, TValue>(this HtmlHelper<TContainer> html, Expression<Func<TContainer, TValue>> expression,
172 string templateName, string htmlFieldName, DataBoundControlMode mode,
173 object additionalViewData) {
174 return MvcHtmlString.Create(TemplateFor(html, expression, templateName, htmlFieldName, mode, additionalViewData, TemplateHelper));
177 // Unit testing version
178 internal static string TemplateFor<TContainer, TValue>(this HtmlHelper<TContainer> html, Expression<Func<TContainer, TValue>> expression,
179 string templateName, string htmlFieldName, DataBoundControlMode mode,
180 object additionalViewData, TemplateHelperDelegate templateHelper) {
181 return templateHelper(html,
182 ModelMetadata.FromLambdaExpression(expression, html.ViewData),
183 htmlFieldName ?? ExpressionHelper.GetExpressionText(expression),
189 internal delegate string TemplateHelperDelegate(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string templateName, DataBoundControlMode mode, object additionalViewData);
191 internal static string TemplateHelper(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string templateName, DataBoundControlMode mode, object additionalViewData) {
192 return TemplateHelper(html, metadata, htmlFieldName, templateName, mode, additionalViewData, ExecuteTemplate);
195 internal static string TemplateHelper(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string templateName, DataBoundControlMode mode, object additionalViewData, ExecuteTemplateDelegate executeTemplate) {
196 // TODO: Convert Editor into Display if model.IsReadOnly is true? Need to be careful about this because
197 // the Model property on the ViewPage/ViewUserControl is get-only, so the type descriptor automatically
198 // decorates it with a [ReadOnly] attribute...
200 if (metadata.ConvertEmptyStringToNull && String.Empty.Equals(metadata.Model)) {
201 metadata.Model = null;
204 object formattedModelValue = metadata.Model;
205 if (metadata.Model == null && mode == DataBoundControlMode.ReadOnly) {
206 formattedModelValue = metadata.NullDisplayText;
209 string formatString = mode == DataBoundControlMode.ReadOnly ? metadata.DisplayFormatString : metadata.EditFormatString;
210 if (metadata.Model != null && !String.IsNullOrEmpty(formatString)) {
211 formattedModelValue = String.Format(CultureInfo.CurrentCulture, formatString, metadata.Model);
214 // Normally this shouldn't happen, unless someone writes their own custom Object templates which
215 // don't check to make sure that the object hasn't already been displayed
216 object visitedObjectsKey = metadata.Model ?? metadata.RealModelType;
217 if (html.ViewDataContainer.ViewData.TemplateInfo.VisitedObjects.Contains(visitedObjectsKey)) { // DDB #224750
221 ViewDataDictionary viewData = new ViewDataDictionary(html.ViewDataContainer.ViewData) {
222 Model = metadata.Model,
223 ModelMetadata = metadata,
224 TemplateInfo = new TemplateInfo {
225 FormattedModelValue = formattedModelValue,
226 HtmlFieldPrefix = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName),
227 VisitedObjects = new HashSet<object>(html.ViewContext.ViewData.TemplateInfo.VisitedObjects), // DDB #224750
231 if (additionalViewData != null) {
232 foreach (KeyValuePair<string, object> kvp in new RouteValueDictionary(additionalViewData)) {
233 viewData[kvp.Key] = kvp.Value;
237 viewData.TemplateInfo.VisitedObjects.Add(visitedObjectsKey); // DDB #224750
239 return executeTemplate(html, viewData, templateName, mode, GetViewNames, GetDefaultActions);
244 private static HtmlHelper MakeHtmlHelper(HtmlHelper html, ViewDataDictionary viewData) {
245 return new HtmlHelper(
246 new ViewContext(html.ViewContext, html.ViewContext.View, viewData, html.ViewContext.TempData, html.ViewContext.Writer),
247 new ViewDataContainer(viewData)
251 internal abstract class ActionCacheItem {
252 public abstract string Execute(HtmlHelper html, ViewDataDictionary viewData);
255 internal class ActionCacheCodeItem : ActionCacheItem {
256 public Func<HtmlHelper, string> Action { get; set; }
258 public override string Execute(HtmlHelper html, ViewDataDictionary viewData) {
259 return Action(MakeHtmlHelper(html, viewData));
263 internal class ActionCacheViewItem : ActionCacheItem {
264 public string ViewName { get; set; }
266 public override string Execute(HtmlHelper html, ViewDataDictionary viewData) {
267 ViewEngineResult viewEngineResult = ViewEngines.Engines.FindPartialView(html.ViewContext, ViewName);
268 using (StringWriter writer = new StringWriter(CultureInfo.InvariantCulture)) {
269 viewEngineResult.View.Render(new ViewContext(html.ViewContext, viewEngineResult.View, viewData, html.ViewContext.TempData, writer), writer);
270 return writer.ToString();
275 private class ViewDataContainer : IViewDataContainer {
276 public ViewDataContainer(ViewDataDictionary viewData) {
280 public ViewDataDictionary ViewData { get; set; }