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