1 // Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
2 
3 using System.Collections;
4 using System.Collections.Generic;
5 using System.Diagnostics.CodeAnalysis;
6 using System.Globalization;
7 using System.Linq;
8 using System.Linq.Expressions;
9 using System.Text;
10 using System.Web.Mvc.Properties;
11 
12 namespace System.Web.Mvc.Html
13 {
14     public static class SelectExtensions
15     {
16         // DropDownList
17 
DropDownList(this HtmlHelper htmlHelper, string name)18         public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name)
19         {
20             return DropDownList(htmlHelper, name, null /* selectList */, null /* optionLabel */, null /* htmlAttributes */);
21         }
22 
DropDownList(this HtmlHelper htmlHelper, string name, string optionLabel)23         public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, string optionLabel)
24         {
25             return DropDownList(htmlHelper, name, null /* selectList */, optionLabel, null /* htmlAttributes */);
26         }
27 
DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList)28         public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList)
29         {
30             return DropDownList(htmlHelper, name, selectList, null /* optionLabel */, null /* htmlAttributes */);
31         }
32 
DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, object htmlAttributes)33         public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, object htmlAttributes)
34         {
35             return DropDownList(htmlHelper, name, selectList, null /* optionLabel */, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
36         }
37 
DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)38         public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
39         {
40             return DropDownList(htmlHelper, name, selectList, null /* optionLabel */, htmlAttributes);
41         }
42 
DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, string optionLabel)43         public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, string optionLabel)
44         {
45             return DropDownList(htmlHelper, name, selectList, optionLabel, null /* htmlAttributes */);
46         }
47 
DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, string optionLabel, object htmlAttributes)48         public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, string optionLabel, object htmlAttributes)
49         {
50             return DropDownList(htmlHelper, name, selectList, optionLabel, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
51         }
52 
DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)53         public static MvcHtmlString DropDownList(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
54         {
55             return DropDownListHelper(htmlHelper, metadata: null, expression: name, selectList: selectList, optionLabel: optionLabel, htmlAttributes: htmlAttributes);
56         }
57 
58         [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
DropDownListFor(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList)59         public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList)
60         {
61             return DropDownListFor(htmlHelper, expression, selectList, null /* optionLabel */, null /* htmlAttributes */);
62         }
63 
64         [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
DropDownListFor(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, object htmlAttributes)65         public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, object htmlAttributes)
66         {
67             return DropDownListFor(htmlHelper, expression, selectList, null /* optionLabel */, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
68         }
69 
70         [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
DropDownListFor(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)71         public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
72         {
73             return DropDownListFor(htmlHelper, expression, selectList, null /* optionLabel */, htmlAttributes);
74         }
75 
76         [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
DropDownListFor(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string optionLabel)77         public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string optionLabel)
78         {
79             return DropDownListFor(htmlHelper, expression, selectList, optionLabel, null /* htmlAttributes */);
80         }
81 
82         [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
DropDownListFor(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string optionLabel, object htmlAttributes)83         public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string optionLabel, object htmlAttributes)
84         {
85             return DropDownListFor(htmlHelper, expression, selectList, optionLabel, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
86         }
87 
88         [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Users cannot use anonymous methods with the LambdaExpression type")]
89         [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
DropDownListFor(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)90         public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
91         {
92             if (expression == null)
93             {
94                 throw new ArgumentNullException("expression");
95             }
96 
97             ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
98 
99             return DropDownListHelper(htmlHelper, metadata, ExpressionHelper.GetExpressionText(expression), selectList, optionLabel, htmlAttributes);
100         }
101 
DropDownListHelper(HtmlHelper htmlHelper, ModelMetadata metadata, string expression, IEnumerable<SelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)102         private static MvcHtmlString DropDownListHelper(HtmlHelper htmlHelper, ModelMetadata metadata, string expression, IEnumerable<SelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
103         {
104             return SelectInternal(htmlHelper, metadata, optionLabel, expression, selectList, allowMultiple: false, htmlAttributes: htmlAttributes);
105         }
106 
107         // ListBox
108 
ListBox(this HtmlHelper htmlHelper, string name)109         public static MvcHtmlString ListBox(this HtmlHelper htmlHelper, string name)
110         {
111             return ListBox(htmlHelper, name, null /* selectList */, null /* htmlAttributes */);
112         }
113 
ListBox(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList)114         public static MvcHtmlString ListBox(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList)
115         {
116             return ListBox(htmlHelper, name, selectList, (IDictionary<string, object>)null);
117         }
118 
ListBox(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, object htmlAttributes)119         public static MvcHtmlString ListBox(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, object htmlAttributes)
120         {
121             return ListBox(htmlHelper, name, selectList, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
122         }
123 
ListBox(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)124         public static MvcHtmlString ListBox(this HtmlHelper htmlHelper, string name, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
125         {
126             return ListBoxHelper(htmlHelper, metadata: null, name: name, selectList: selectList, htmlAttributes: htmlAttributes);
127         }
128 
129         [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
ListBoxFor(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList)130         public static MvcHtmlString ListBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList)
131         {
132             return ListBoxFor(htmlHelper, expression, selectList, null /* htmlAttributes */);
133         }
134 
135         [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
ListBoxFor(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, object htmlAttributes)136         public static MvcHtmlString ListBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, object htmlAttributes)
137         {
138             return ListBoxFor(htmlHelper, expression, selectList, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes));
139         }
140 
141         [SuppressMessage("Microsoft.Design", "CA1011:ConsiderPassingBaseTypesAsParameters", Justification = "Users cannot use anonymous methods with the LambdaExpression type")]
142         [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures", Justification = "This is an appropriate nesting of generic types")]
ListBoxFor(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)143         public static MvcHtmlString ListBoxFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
144         {
145             if (expression == null)
146             {
147                 throw new ArgumentNullException("expression");
148             }
149 
150             ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
151 
152             return ListBoxHelper(htmlHelper,
153                                  metadata,
154                                  ExpressionHelper.GetExpressionText(expression),
155                                  selectList,
156                                  htmlAttributes);
157         }
158 
ListBoxHelper(HtmlHelper htmlHelper, ModelMetadata metadata, string name, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)159         private static MvcHtmlString ListBoxHelper(HtmlHelper htmlHelper, ModelMetadata metadata, string name, IEnumerable<SelectListItem> selectList, IDictionary<string, object> htmlAttributes)
160         {
161             return SelectInternal(htmlHelper, metadata, optionLabel: null, name: name, selectList: selectList, allowMultiple: true, htmlAttributes: htmlAttributes);
162         }
163 
164         // Helper methods
165 
GetSelectData(this HtmlHelper htmlHelper, string name)166         private static IEnumerable<SelectListItem> GetSelectData(this HtmlHelper htmlHelper, string name)
167         {
168             object o = null;
169             if (htmlHelper.ViewData != null)
170             {
171                 o = htmlHelper.ViewData.Eval(name);
172             }
173             if (o == null)
174             {
175                 throw new InvalidOperationException(
176                     String.Format(
177                         CultureInfo.CurrentCulture,
178                         MvcResources.HtmlHelper_MissingSelectData,
179                         name,
180                         "IEnumerable<SelectListItem>"));
181             }
182             IEnumerable<SelectListItem> selectList = o as IEnumerable<SelectListItem>;
183             if (selectList == null)
184             {
185                 throw new InvalidOperationException(
186                     String.Format(
187                         CultureInfo.CurrentCulture,
188                         MvcResources.HtmlHelper_WrongSelectDataType,
189                         name,
190                         o.GetType().FullName,
191                         "IEnumerable<SelectListItem>"));
192             }
193             return selectList;
194         }
195 
ListItemToOption(SelectListItem item)196         internal static string ListItemToOption(SelectListItem item)
197         {
198             TagBuilder builder = new TagBuilder("option")
199             {
200                 InnerHtml = HttpUtility.HtmlEncode(item.Text)
201             };
202             if (item.Value != null)
203             {
204                 builder.Attributes["value"] = item.Value;
205             }
206             if (item.Selected)
207             {
208                 builder.Attributes["selected"] = "selected";
209             }
210             return builder.ToString(TagRenderMode.Normal);
211         }
212 
GetSelectListWithDefaultValue(IEnumerable<SelectListItem> selectList, object defaultValue, bool allowMultiple)213         private static IEnumerable<SelectListItem> GetSelectListWithDefaultValue(IEnumerable<SelectListItem> selectList, object defaultValue, bool allowMultiple)
214         {
215             IEnumerable defaultValues;
216 
217             if (allowMultiple)
218             {
219                 defaultValues = defaultValue as IEnumerable;
220                 if (defaultValues == null || defaultValues is string)
221                 {
222                     throw new InvalidOperationException(
223                         String.Format(
224                             CultureInfo.CurrentCulture,
225                             MvcResources.HtmlHelper_SelectExpressionNotEnumerable,
226                             "expression"));
227                 }
228             }
229             else
230             {
231                 defaultValues = new[] { defaultValue };
232             }
233 
234             IEnumerable<string> values = from object value in defaultValues
235                                          select Convert.ToString(value, CultureInfo.CurrentCulture);
236             HashSet<string> selectedValues = new HashSet<string>(values, StringComparer.OrdinalIgnoreCase);
237             List<SelectListItem> newSelectList = new List<SelectListItem>();
238 
239             foreach (SelectListItem item in selectList)
240             {
241                 item.Selected = (item.Value != null) ? selectedValues.Contains(item.Value) : selectedValues.Contains(item.Text);
242                 newSelectList.Add(item);
243             }
244             return newSelectList;
245         }
246 
SelectInternal(this HtmlHelper htmlHelper, ModelMetadata metadata, string optionLabel, string name, IEnumerable<SelectListItem> selectList, bool allowMultiple, IDictionary<string, object> htmlAttributes)247         private static MvcHtmlString SelectInternal(this HtmlHelper htmlHelper, ModelMetadata metadata, string optionLabel, string name, IEnumerable<SelectListItem> selectList, bool allowMultiple, IDictionary<string, object> htmlAttributes)
248         {
249             string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
250             if (String.IsNullOrEmpty(fullName))
251             {
252                 throw new ArgumentException(MvcResources.Common_NullOrEmpty, "name");
253             }
254 
255             bool usedViewData = false;
256 
257             // If we got a null selectList, try to use ViewData to get the list of items.
258             if (selectList == null)
259             {
260                 selectList = htmlHelper.GetSelectData(name);
261                 usedViewData = true;
262             }
263 
264             object defaultValue = (allowMultiple) ? htmlHelper.GetModelStateValue(fullName, typeof(string[])) : htmlHelper.GetModelStateValue(fullName, typeof(string));
265 
266             // If we haven't already used ViewData to get the entire list of items then we need to
267             // use the ViewData-supplied value before using the parameter-supplied value.
268             if (!usedViewData && defaultValue == null && !String.IsNullOrEmpty(name))
269             {
270                 defaultValue = htmlHelper.ViewData.Eval(name);
271             }
272 
273             if (defaultValue != null)
274             {
275                 selectList = GetSelectListWithDefaultValue(selectList, defaultValue, allowMultiple);
276             }
277 
278             // Convert each ListItem to an <option> tag
279             StringBuilder listItemBuilder = new StringBuilder();
280 
281             // Make optionLabel the first item that gets rendered.
282             if (optionLabel != null)
283             {
284                 listItemBuilder.AppendLine(ListItemToOption(new SelectListItem() { Text = optionLabel, Value = String.Empty, Selected = false }));
285             }
286 
287             foreach (SelectListItem item in selectList)
288             {
289                 listItemBuilder.AppendLine(ListItemToOption(item));
290             }
291 
292             TagBuilder tagBuilder = new TagBuilder("select")
293             {
294                 InnerHtml = listItemBuilder.ToString()
295             };
296             tagBuilder.MergeAttributes(htmlAttributes);
297             tagBuilder.MergeAttribute("name", fullName, true /* replaceExisting */);
298             tagBuilder.GenerateId(fullName);
299             if (allowMultiple)
300             {
301                 tagBuilder.MergeAttribute("multiple", "multiple");
302             }
303 
304             // If there are any errors for a named field, we add the css attribute.
305             ModelState modelState;
306             if (htmlHelper.ViewData.ModelState.TryGetValue(fullName, out modelState))
307             {
308                 if (modelState.Errors.Count > 0)
309                 {
310                     tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName);
311                 }
312             }
313 
314             tagBuilder.MergeAttributes(htmlHelper.GetUnobtrusiveValidationAttributes(name, metadata));
315 
316             return tagBuilder.ToMvcHtmlString(TagRenderMode.Normal);
317         }
318     }
319 }
320