Skip to content

Instantly share code, notes, and snippets.

@dsidirop
Forked from apuchkov/ExtendedSelectExtensions.cs
Last active December 20, 2020 22:13
Show Gist options
  • Save dsidirop/211bce7e800756e1b44ed43fc003637b to your computer and use it in GitHub Desktop.
Save dsidirop/211bce7e800756e1b44ed43fc003637b to your computer and use it in GitHub Desktop.
ASP.NET MVC HTML DropList helper with a way to add custom attributes to options http://stackoverflow.com/a/34096927/1689049
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Web;
using System.Web.Mvc;
// to future maintainers derived from https://stackoverflow.com/questions/7536631/adding-html-class-tag-under-option-in-html-dropdownlist/34096927#34096927
// to future maintainers
// to future maintainers @Html.ExtendedDropDown() is a nifty little codegem which supports adding css-classes and data-attributes on the rendered <options>
// to future maintainers its a smarter alternative to @Html.DropDown() which lacks support for these kinds of things
namespace Extensions
{
public class ExtendedSelectListItem : SelectListItem
{
public object HtmlAttributes { get; set; }
}
public static class HtmlHelperExtendedDropDown
{
internal static object GetModelStateValue(this HtmlHelper htmlHelper, string key, Type destinationType)
{
var modelState = (ModelState)null;
return !htmlHelper.ViewData.ModelState.TryGetValue(key: key, value: out modelState)
? null
: modelState.Value?.ConvertTo(type: destinationType, culture: null);
}
public static MvcHtmlString ExtendedDropDown(this HtmlHelper htmlHelper, string name, IEnumerable<ExtendedSelectListItem> selectList)
{
return ExtendedDropDown(
name: name,
htmlHelper: htmlHelper,
selectList: selectList,
optionLabel: null,
htmlAttributes: null
);
}
public static MvcHtmlString ExtendedDropDown(this HtmlHelper htmlHelper, IEnumerable<ExtendedSelectListItem> selectList, object htmlAttributes)
{
return ExtendedDropDown(
name: null,
htmlHelper: htmlHelper,
selectList: selectList,
optionLabel: null,
htmlAttributes: CloneAnonymousObjectAsHtmlAttributesDict(htmlAttributes)
);
}
public static MvcHtmlString ExtendedDropDown(this HtmlHelper htmlHelper, string name, IEnumerable<ExtendedSelectListItem> selectList, object htmlAttributes)
{
return ExtendedDropDown(
name: name,
htmlHelper: htmlHelper,
selectList: selectList,
optionLabel: null,
htmlAttributes: CloneAnonymousObjectAsHtmlAttributesDict(htmlAttributes)
);
}
public static MvcHtmlString ExtendedDropDown(this HtmlHelper htmlHelper, IEnumerable<ExtendedSelectListItem> selectList, IDictionary<string, object> htmlAttributes)
{
return ExtendedDropDown(
name: null,
htmlHelper: htmlHelper,
selectList: selectList,
optionLabel: null,
htmlAttributes: htmlAttributes
);
}
public static MvcHtmlString ExtendedDropDown(this HtmlHelper htmlHelper, string name, IEnumerable<ExtendedSelectListItem> selectList, IDictionary<string, object> htmlAttributes)
{
return ExtendedDropDown(
name: name,
htmlHelper: htmlHelper,
selectList: selectList,
optionLabel: null,
htmlAttributes: htmlAttributes
);
}
public static MvcHtmlString ExtendedDropDown(this HtmlHelper htmlHelper, string name, IEnumerable<ExtendedSelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
{
return ExtendedDropDownHelper(
metadata: null,
expression: name,
htmlHelper: htmlHelper,
selectList: selectList,
optionLabel: optionLabel,
htmlAttributes: htmlAttributes
);
}
public static MvcHtmlString ExtendedDropDownHelper(this HtmlHelper htmlHelper, ModelMetadata metadata, string expression, IEnumerable<ExtendedSelectListItem> selectList, string optionLabel, IDictionary<string, object> htmlAttributes)
{
return SelectInternal(
name: expression,
metadata: metadata,
htmlHelper: htmlHelper,
selectList: selectList,
optionLabel: optionLabel,
allowMultiple: false,
htmlAttributes: htmlAttributes
);
}
public static MvcHtmlString ExtendedDropDownFor<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<ExtendedSelectListItem> selectList,
object htmlAttributes
)
{
return ExtendedDropDownFor(
selectList: selectList,
htmlHelper: htmlHelper,
expression: expression,
optionLabel: null,
htmlAttributes: htmlAttributes
);
}
public static MvcHtmlString ExtendedDropDownFor<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<ExtendedSelectListItem> selectList,
string optionLabel,
object htmlAttributes
)
{
if (expression == null)
throw new ArgumentNullException(paramName: nameof(expression));
var metadata = ModelMetadata.FromLambdaExpression(expression: expression, viewData: htmlHelper.ViewData);
return SelectInternal(
name: ExpressionHelper.GetExpressionText(expression: expression),
metadata: metadata,
htmlHelper: htmlHelper,
selectList: selectList,
optionLabel: optionLabel,
allowMultiple: false,
htmlAttributes: CloneAnonymousObjectAsHtmlAttributesDict(htmlAttributes)
);
}
private static MvcHtmlString SelectInternal(
this HtmlHelper htmlHelper,
ModelMetadata metadata,
string optionLabel,
string name,
IEnumerable<ExtendedSelectListItem> selectList,
bool allowMultiple,
IDictionary<string, object> htmlAttributes
)
{
if (selectList == null)
throw new ArgumentException(nameof(selectList));
var fullName = htmlHelper
.ViewContext
.ViewData
.TemplateInfo
.GetFullHtmlFieldName(partialFieldName: name);
if (string.IsNullOrEmpty(value: fullName))
throw new ArgumentException(message: "No name");
var defaultValue = (
allowMultiple
? htmlHelper.GetModelStateValue(key: fullName, destinationType: typeof(string[]))
: htmlHelper.GetModelStateValue(key: fullName, destinationType: typeof(string))
) ?? htmlHelper.ViewData.Eval(expression: fullName);
// If we havent already used ViewData to get the entire list of items then we need to
// use the ViewData-supplied value before using the parameter supplied value
if (defaultValue != null)
{
var defaultValues = allowMultiple
? defaultValue as IEnumerable
: new[] {defaultValue};
var values =
from object value in defaultValues
select Convert.ToString(value: value, provider: CultureInfo.CurrentCulture);
var selectedValues = new HashSet<string>(collection: values, comparer: StringComparer.OrdinalIgnoreCase);
var newSelectList = new List<ExtendedSelectListItem>(32);
foreach (var item in selectList)
{
item.Selected = item.Value == null
? selectedValues.Contains(item: item.Text)
: selectedValues.Contains(item: item.Value);
newSelectList.Add(item: item);
}
selectList = newSelectList;
}
var listItemBuilder = new StringBuilder(); // Convert each ListItem to an <option> tag
if (optionLabel != null) // Make optionLabel the first item that gets rendered
{
listItemBuilder.Append(
value: ListItemToOption(
item: new ExtendedSelectListItem()
{
Text = optionLabel,
Value = "",
Selected = false
}
)
);
}
foreach (var item in selectList)
{
listItemBuilder.Append(value: ListItemToOption(item: item));
}
var tagBuilder = new TagBuilder(tagName: "select")
{
InnerHtml = listItemBuilder.ToString()
};
tagBuilder.MergeAttributes(attributes: htmlAttributes);
tagBuilder.MergeAttribute(key: "name", value: fullName, replaceExisting: true);
tagBuilder.GenerateId(name: fullName);
if (allowMultiple)
{
tagBuilder.MergeAttribute(key: "multiple", value: "multiple");
}
var modelState = (ModelState) null; // if there are any errors for a named field we add the css attribute
if (
htmlHelper.ViewData.ModelState.TryGetValue(key: fullName, value: out modelState) //order
&& modelState.Errors.Any() //order
)
{
tagBuilder.AddCssClass(value: HtmlHelper.ValidationInputCssClassName);
}
tagBuilder.MergeAttributes(attributes: htmlHelper.GetUnobtrusiveValidationAttributes(name: fullName, metadata: metadata));
return MvcHtmlString.Create(value: tagBuilder.ToString(renderMode: TagRenderMode.Normal));
}
internal static string ListItemToOption(ExtendedSelectListItem item)
{
var builder = new TagBuilder(tagName: "option")
{
InnerHtml = HttpUtility.HtmlEncode(s: item.Text)
};
if (item.Value != null)
{
builder.Attributes[key: "value"] = item.Value;
}
if (item.Selected)
{
builder.Attributes[key: "selected"] = "selected";
}
builder.MergeAttributes(
attributes: CloneAnonymousObjectAsHtmlAttributesDict(item.HtmlAttributes)
);
return builder.ToString(renderMode: TagRenderMode.Normal);
}
private static IDictionary<string, object> CloneAnonymousObjectAsHtmlAttributesDict(object htmlAttributes)
{
var dictionary = (IDictionary)null;
var dictionaryStringObject = (IDictionary<string, object>)null;
try
{
dictionaryStringObject = htmlAttributes as IDictionary<string, object>;
dictionary = dictionaryStringObject == null
? htmlAttributes as IDictionary
: null;
}
catch
{
// ignored
}
if (dictionaryStringObject == null && dictionary == null)
return HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
var result = (Dictionary<string, object>)null;
if (dictionary != null)
{
result = new Dictionary<string, object>(dictionary.Count);
foreach (var x in dictionary.Keys)
{
result[x.ToString()] = dictionary[x]; //vital to clone
}
}
else
{
result = dictionaryStringObject.ToDictionary(
x => x.Key,
x => x.Value
);
}
return result;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment