Merge pull request #249 from pcc/xgetinputfocus
[mono.git] / mcs / class / System.Web.Mvc3 / Mvc / Html / TemplateHelpers.cs
1 namespace System.Web.Mvc.Html {
2     using System;
3     using System.Collections;
4     using System.Collections.Generic;
5     using System.Globalization;
6     using System.IO;
7     using System.Linq;
8     using System.Linq.Expressions;
9     using System.Web.Mvc.Resources;
10     using System.Web.Routing;
11     using System.Web.UI.WebControls;
12
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" }
18             };
19
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 },
32             };
33
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 },
45             };
46
47         internal static string cacheItemId = Guid.NewGuid().ToString();
48
49         internal delegate string ExecuteTemplateDelegate(HtmlHelper html, ViewDataDictionary viewData, string templateName, DataBoundControlMode mode, GetViewNamesDelegate getViewNames, GetDefaultActionsDelegate getDefaultActions);
50
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];
55
56             foreach (string viewName in getViewNames(viewData.ModelMetadata, templateName, viewData.ModelMetadata.TemplateHint, viewData.ModelMetadata.DataTypeName)) {
57                 string fullViewName = modeViewPath + "/" + viewName;
58                 ActionCacheItem cacheItem;
59
60                 if (actionCache.TryGetValue(fullViewName, out cacheItem)) {
61                     if (cacheItem != null) {
62                         return cacheItem.Execute(html, viewData);
63                     }
64                 }
65                 else {
66                     ViewEngineResult viewEngineResult = ViewEngines.Engines.FindPartialView(html.ViewContext, fullViewName);
67                     if (viewEngineResult.View != null) {
68                         actionCache[fullViewName] = new ActionCacheViewItem { ViewName = fullViewName };
69
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();
73                         }
74                     }
75
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));
80                     }
81
82                     actionCache[fullViewName] = null;
83                 }
84             }
85
86             throw new InvalidOperationException(
87                 String.Format(
88                     CultureInfo.CurrentCulture,
89                     MvcResources.TemplateHelpers_NoTemplate,
90                     viewData.ModelMetadata.RealModelType.FullName
91                 )
92             );
93         }
94
95         internal static Dictionary<string, ActionCacheItem> GetActionCache(HtmlHelper html) {
96             HttpContextBase context = html.ViewContext.HttpContext;
97             Dictionary<string, ActionCacheItem> result;
98
99             if (!context.Items.Contains(cacheItemId)) {
100                 result = new Dictionary<string, ActionCacheItem>();
101                 context.Items[cacheItemId] = result;
102             }
103             else {
104                 result = (Dictionary<string, ActionCacheItem>)context.Items[cacheItemId];
105             }
106
107             return result;
108         }
109
110         internal delegate Dictionary<string, Func<HtmlHelper, string>> GetDefaultActionsDelegate(DataBoundControlMode mode);
111
112         internal static Dictionary<string, Func<HtmlHelper, string>> GetDefaultActions(DataBoundControlMode mode) {
113             return mode == DataBoundControlMode.ReadOnly ? defaultDisplayActions : defaultEditorActions;
114         }
115
116         internal delegate IEnumerable<string> GetViewNamesDelegate(ModelMetadata metadata, params string[] templateHints);
117
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;
121             }
122
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;
125
126             // TODO: Make better string names for generic types
127             yield return fieldType.Name;
128
129             if (!metadata.IsComplexType) {
130                 yield return "String";
131             }
132             else if (fieldType.IsInterface) {
133                 if (typeof(IEnumerable).IsAssignableFrom(fieldType)) {
134                     yield return "Collection";
135                 }
136
137                 yield return "Object";
138             }
139             else {
140                 bool isEnumerable = typeof(IEnumerable).IsAssignableFrom(fieldType);
141
142                 while (true) {
143                     fieldType = fieldType.BaseType;
144                     if (fieldType == null)
145                         break;
146
147                     if (isEnumerable && fieldType == typeof(Object)) {
148                         yield return "Collection";
149                     }
150
151                     yield return fieldType.Name;
152                 }
153             }
154         }
155
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));
158         }
159
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),
166                                   templateName,
167                                   mode,
168                                   additionalViewData);
169         }
170
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));
175         }
176
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),
184                                   templateName,
185                                   mode,
186                                   additionalViewData);
187         }
188
189         internal delegate string TemplateHelperDelegate(HtmlHelper html, ModelMetadata metadata, string htmlFieldName, string templateName, DataBoundControlMode mode, object additionalViewData);
190
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);
193         }
194
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...
199
200             if (metadata.ConvertEmptyStringToNull && String.Empty.Equals(metadata.Model)) {
201                 metadata.Model = null;
202             }
203
204             object formattedModelValue = metadata.Model;
205             if (metadata.Model == null && mode == DataBoundControlMode.ReadOnly) {
206                 formattedModelValue = metadata.NullDisplayText;
207             }
208
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);
212             }
213
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
218                 return String.Empty;
219             }
220
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
228                 }
229             };
230
231             if (additionalViewData != null) {
232                 foreach (KeyValuePair<string, object> kvp in new RouteValueDictionary(additionalViewData)) {
233                     viewData[kvp.Key] = kvp.Value;
234                 }
235             }
236
237             viewData.TemplateInfo.VisitedObjects.Add(visitedObjectsKey);    // DDB #224750
238
239             return executeTemplate(html, viewData, templateName, mode, GetViewNames, GetDefaultActions);
240         }
241
242         // Helpers
243
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)
248             );
249         }
250
251         internal abstract class ActionCacheItem {
252             public abstract string Execute(HtmlHelper html, ViewDataDictionary viewData);
253         }
254
255         internal class ActionCacheCodeItem : ActionCacheItem {
256             public Func<HtmlHelper, string> Action { get; set; }
257
258             public override string Execute(HtmlHelper html, ViewDataDictionary viewData) {
259                 return Action(MakeHtmlHelper(html, viewData));
260             }
261         }
262
263         internal class ActionCacheViewItem : ActionCacheItem {
264             public string ViewName { get; set; }
265
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();
271                 }
272             }
273         }
274
275         private class ViewDataContainer : IViewDataContainer {
276             public ViewDataContainer(ViewDataDictionary viewData) {
277                 ViewData = viewData;
278             }
279
280             public ViewDataDictionary ViewData { get; set; }
281         }
282     }
283 }