Skip to content

Instantly share code, notes, and snippets.

@nickalbrecht
Last active November 5, 2022 19:57
  • Star 15 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save nickalbrecht/3813767 to your computer and use it in GitHub Desktop.
SelectListItem Extension Methods for DropDowns in MVC Core. Added ability to specify the option's Group, and for some bug fixes, more XML docs
public static class SelectListExtensionMethods
{
/// <summary>
/// The SelectListItem to use by default as the placeholder for select lists generated by these extension methods when the user needs to pick a value.
/// </summary>
public static readonly SelectListItem DefaultEmptySelectListItem = new SelectListItem() { Text = "-- Pick One --", Value = string.Empty };
/// <summary>
/// The SelectListItem to use by default as the placeholder for select lists generated by these extension methods when not picking a value is the same as using as of the possible choices (meant for filtering typically)
/// </summary>
public static readonly SelectListItem AnySelectListItem = new SelectListItem() { Text = "-- Any --", Value = string.Empty };
private static bool IsDefault(object value)
{
if (value is Guid guid && guid.IsEmpty())
return true;
if (value is string text && string.IsNullOrWhiteSpace(text))
return true;
else
return value == default;
}
private static SelectListGroup GetGroup<TGroup>(TGroup groupValue, Dictionary<TGroup, SelectListGroup> groups)
{
if (groupValue == null)
return null;
var hit = groups.TryGetValue(groupValue, out var selectListGroup);
if (!hit)
{
if (groupValue == null)
return null;
selectListGroup = new SelectListGroup() { Name = groupValue.ToString() };
groups.Add(groupValue, selectListGroup);
}
return selectListGroup;
}
/// <summary>
/// Allows you to set the group of any given SelectListItem based off of the groups already in use in a given collection of SelectListItems.
/// </summary>
/// <param name="item">The item you want to set the Group for</param>
/// <param name="groupName">The Group name you want to set</param>
/// <param name="selectListItems">The list to check if the new Group exists and thus use, or to create a new one</param>
public static void SetGroup(this SelectListItem item, string groupName, IEnumerable<SelectListItem> selectListItems)
{
item.Group = selectListItems.FirstOrDefault(x => x.Group?.Name == groupName)?.Group ?? new SelectListGroup() { Name = groupName };
}
#region Primitive Keys
/// <summary>
/// Returns a collection of SelectListItem for each of the items in the collection passed in.
/// </summary>
/// <example>
/// people.ToSelectList(x => x.PersonId, x => x.FullName);
/// </example>
/// <param name="enumerable">The collection to generate the list from</param>
/// <param name="key">The property to use as the value attribute of each list item.</param>
/// <param name="text">The property to use as the text attribute of each list item.</param>
public static ICollection<SelectListItem> ToSelectList<TType, TKey>(this IEnumerable<TType> enumerable, Func<TType, TKey> key, Func<TType, string> text)
{
return ToSelectList(enumerable, key, text, (Func<TType, string>)null, null, DefaultEmptySelectListItem);
}
/// <summary>
/// Returns a collection of SelectListItem for each of the items in the collection passed in.
/// </summary>
/// <example>
/// people.ToSelectList(x => x.PersonId, x => x.FullName);
/// </example>
/// <param name="enumerable">The collection to generate the list from</param>
/// <param name="key">The property to use as the value attribute of each list item.</param>
/// <param name="text">The property to use as the text attribute of each list item.</param>
/// <param name="group">The property to use as the group attribute of each list item.</param>
public static ICollection<SelectListItem> ToSelectList<TType, TKey, TGroup>(this IEnumerable<TType> enumerable, Func<TType, TKey> key, Func<TType, string> text,
Func<TType, TGroup> group)
{
return ToSelectList(enumerable, key, text, group, null, DefaultEmptySelectListItem);
}
/// <summary>
/// Returns a collection of SelectListItem for each of the items in the collection passed in, with a specific list item selected and optionally an empty list item.
/// </summary>
/// <example>
/// <code>
/// people.ToSelectList(x => x.PersonId, x => x.Name, 2345);
/// // or
/// people.ToSelectList(x => x.PersonId, x => x.Name, 2345, false); if you don't want the empty list item
/// </code>
/// </example>
/// <param name="enumerable">The collection to generate the list from</param>
/// <param name="key">The property to use as the value attribute of each list item.</param>
/// <param name="text">The property to use as the text attribute of each list item.</param>
/// <param name="currentKey">The String value of the list item that should be selected by default.</param>
/// <param name="includeEmptyListItem">Whether or not a default list item should be the first list item before those from the collection.</param>
public static ICollection<SelectListItem> ToSelectList<TType, TKey>(this IEnumerable<TType> enumerable, Func<TType, TKey> key, Func<TType, string> text, TKey currentKey,
bool includeEmptyListItem = true)
{
return ToSelectList(enumerable, key, text, (Func<TType, string>)null, currentKey != null ? new[] { currentKey } : null, includeEmptyListItem ? DefaultEmptySelectListItem : null);
}
public static ICollection<SelectListItem> ToSelectList<TType, TKey, TGroup>(this IEnumerable<TType> enumerable, Func<TType, TKey> key, Func<TType, string> text,
Func<TType, TGroup> group, TKey currentKey, bool includeEmptyListItem = true)
{
return ToSelectList(enumerable, key, text, group, currentKey != null ? new[] { currentKey } : null, includeEmptyListItem ? DefaultEmptySelectListItem : null);
}
/// <summary>
/// Returns a collection of SelectListItem for each of the items in the collection passed in, with a specific list item selected and a custom empty list item.
/// </summary>
/// <example>
/// <code>
/// people.ToSelectList(x => x.PersonId, x => x.Name, 2345, new SelectListItem() {Text = Resources.Views.Shared.PickOne Value = ""});
/// </code>
/// </example>
/// <param name="enumerable">The collection to generate the list from</param>
/// <param name="key">The property to use as the value attribute of each list item.</param>
/// <param name="text">The property to use as the text attribute of each list item.</param>
/// <param name="currentKey">The String value of the list item that should be selected by default.</param>
/// <param name="emptyListItem">The list item to use as the first list item before those from the collection.</param>
public static ICollection<SelectListItem> ToSelectList<TType, TKey>(this IEnumerable<TType> enumerable, Func<TType, TKey> key, Func<TType, string> text, TKey currentKey,
SelectListItem emptyListItem)
{
return ToSelectList(enumerable, key, text, (Func<TType, string>)null, currentKey != null ? new[] { currentKey } : null, emptyListItem);
}
public static ICollection<SelectListItem> ToSelectList<TType, TKey, TGroup>(this IEnumerable<TType> enumerable, Func<TType, TKey> key, Func<TType, string> text,
Func<TType, TGroup> group, TKey currentKey, SelectListItem emptyListItem)
{
return ToSelectList(enumerable, key, text, group, currentKey != null ? new[] { currentKey } : null, emptyListItem);
}
public static ICollection<SelectListItem> ToSelectList<TType, TKey>(this IEnumerable<TType> enumerable, Func<TType, TKey> key, Func<TType, string> text,
IEnumerable<TKey> currentKeys, bool includeEmptyListItem = true)
{
return ToSelectList(enumerable, key, text, (Func<TType, string>)null, currentKeys, includeEmptyListItem ? DefaultEmptySelectListItem : null);
}
public static ICollection<SelectListItem> ToSelectList<TType, TKey, TGroup>(this IEnumerable<TType> enumerable, Func<TType, TKey> key, Func<TType, string> text,
Func<TType, TGroup> group, IEnumerable<TKey> currentKeys, bool includeEmptyListItem = true)
{
return ToSelectList(enumerable, key, text, group, currentKeys, includeEmptyListItem ? DefaultEmptySelectListItem : null);
}
public static ICollection<SelectListItem> ToSelectList<TType, TKey>(this IEnumerable<TType> enumerable, Func<TType, TKey> key, Func<TType, string> text,
IEnumerable<TKey> currentKeys, SelectListItem emptyListItem)
{
return ToSelectList(enumerable, key, text, (Func<TType, string>)null, currentKeys, emptyListItem);
}
public static ICollection<SelectListItem> ToSelectList<TType, TKey, TText, TGroup>(this IEnumerable<TType> enumerable, Func<TType, TKey> key,
Func<TType, TText> text, Func<TType, TGroup> group, IEnumerable<TKey> currentKeys, SelectListItem emptyListItem)
{
var selectList = new List<SelectListItem>();
var groups = new Dictionary<TGroup, SelectListGroup>();
//Iterate over the enumerable, and create the initial list of SelectListItems from it
if (enumerable != null)
{
selectList = enumerable.Select(x => new SelectListItem()
{
Value = key.Invoke(x).ToString(),
Text = text.Invoke(x).ToString(),
//Using ternary operator for the group because we can't call Invoke on a null object, and group?.Invoke(x) is not permitted because the generic type TGroup is not (and can't be) constrained to a struct or class
//Primitive types (int, DateTime, Guid) are all structs but string are an object, and I want to allow primitive types to avoid cluttering calls to ToSelectList with .ToString()
Group = group != null ? GetGroup(group.Invoke(x), groups) : null,
Selected = (currentKeys != null && currentKeys.Contains(key.Invoke(x)))
})
.ToList();
}
//If any of the currentKeys do not have a respective SelectListItem already, add a new item for each of them under a new group called "Currently Selected"
if (currentKeys != null)
{
var currentSelectListGroup = new SelectListGroup() { Name = "Currently Selected" };
foreach (var currentKey in currentKeys)
{
var currentKeyString = currentKey.ToString();
if (!IsDefault(currentKey) && selectList.All(x => x.Value != currentKeyString))
{
var currentSelectListItem = new SelectListItem(currentKeyString, currentKeyString, true)
{ Group = currentSelectListGroup };
selectList.Insert(0, currentSelectListItem);
}
}
}
//If needed, add the item to serve as the "no selection yet" indicator to the top of the list to serve as the default selection
if (emptyListItem != null)
selectList.Insert(0, emptyListItem);
return selectList;
}
#endregion
#region Enumerable Enums
// The following three methods are only present in case you wish to control how the list if Enum's is built, useful if you need to omit some due for security reasons.
// Check out http://www.kodefuguru.com/post/2011/09/21/Empowering-Enums.aspx for a good example of how to do this yourself
public static ICollection<SelectListItem> ToSelectList<TEnum>(this IEnumerable<TEnum> enumerable, bool includeEmptyListItem = true) where TEnum : struct
{
return ToSelectList(enumerable, null, includeEmptyListItem ? DefaultEmptySelectListItem : null);
}
public static ICollection<SelectListItem> ToSelectList<TEnum>(this IEnumerable<TEnum> enumerable, SelectListItem emptyListItem) where TEnum : struct
{
return ToSelectList(enumerable, null, emptyListItem);
}
public static ICollection<SelectListItem> ToSelectList<TEnum>(this IEnumerable<TEnum> enumerable, TEnum? currentKey, bool includeEmptyListItem = true) where TEnum : struct
{
return ToSelectList(enumerable, currentKey, includeEmptyListItem ? DefaultEmptySelectListItem : null);
}
public static ICollection<SelectListItem> ToSelectList<TEnum>(this IEnumerable<TEnum> enumerable, int currentKey, bool includeEmptyListItem = true) where TEnum : struct
{
TEnum enumCurrentKey = (TEnum)Enum.ToObject(typeof(TEnum), currentKey);
return ToSelectList(enumerable, enumCurrentKey, includeEmptyListItem ? DefaultEmptySelectListItem : null);
}
/// <summary>
/// Returns a collection of SelectListItem from a provided collection of Enum. Typically you would do this when you have a source enum, but want to premtively exclude certain options.
/// </summary>
/// <example>
/// <code>
/// IEnumerable&lt;EnumName&gt; myEnums = Enum.GetValues(typeof(EnumName)).Cast&lt;EnumName&gt;();
/// myEnums.ToSelectList(currentKey, new SelectListItem() {Text = Resources.Views.Shared.PickOne, Value = ""});
/// </code>
/// </example>
public static ICollection<SelectListItem> ToSelectList<TEnum>(this IEnumerable<TEnum> enumerable, TEnum? currentKey, SelectListItem emptyListItem) where TEnum : struct
{
var selectList = new List<SelectListItem>();
if (enumerable != null)
selectList = enumerable
.Select(x => new SelectListItem() { Value = x.ToString(), Text = x.ToString(), Selected = currentKey != null && (int)(object)x == (int)(object)currentKey })
.ToList();
if (emptyListItem != null)
selectList.Insert(0, emptyListItem);
return selectList;
}
#endregion
#region Enums
/// <summary>
/// Returns a collection of SelectListItem for each possible value of an Enum, with a specific list item selected and optionally an empty list item.
/// </summary>
/// <example>
/// <code>
/// ExtensionMethods.ToSelectList<Colors>(2);
/// // or
/// ExtensionMethods.ToSelectList<Colors>(2, false); if you don't want the empty list item
/// </code>
/// </example>
/// <param name="currentKey">The Guid value of the list item that should be selected by default.</param>
/// <param name="includeEmptyListItem">Whether or not a default list item should be the first list item before those from the collection.</param>
public static ICollection<SelectListItem> ToSelectList<TEnum>(int currentKey, bool includeEmptyListItem = true) where TEnum : struct
{
TEnum enumCurrentKey = (TEnum)Enum.ToObject(typeof(TEnum), currentKey);
return ToSelectList<TEnum>(enumCurrentKey, includeEmptyListItem ? DefaultEmptySelectListItem : null);
}
/// <summary>
/// Returns a collection of SelectListItem for each possible value of an Enum, and optionally an empty list item.
/// </summary>
/// <example>
/// <code>
/// ExtensionMethods.ToSelectList<Colors>();
/// // or
/// ExtensionMethods.ToSelectList<Colors>(false); if you don't want the empty list item
/// </code>
/// </example>
/// <param name="includeEmptyListItem">Whether or not a default list item should be the first list item before those from the collection.</param>
public static ICollection<SelectListItem> ToSelectList<TEnum>(bool includeEmptyListItem = true) where TEnum : struct
{
return ToEnumSelectList<TEnum>(null, includeEmptyListItem ? DefaultEmptySelectListItem : null);
}
public static ICollection<SelectListItem> ToSelectList<TEnum>(SelectListItem defaultListItem) where TEnum : struct
{
return ToEnumSelectList<TEnum>(null, defaultListItem);
}
/// <summary>
/// Returns a collection of SelectListItem for each possible value of an Enum, with a specific list item selected and optionally an empty list item.
/// </summary>
/// <example>
/// <code>
/// // Useful when your enum is a nullable viewmodel
/// Colors? viewModel = null;
/// ExtensionMethods.ToSelectList(viewModel)
/// </code>
/// </example>
/// <param name="currentKey">The Guid value of the list item that should be selected by default.</param>
/// <param name="includeEmptyListItem">Whether or not a default list item should be the first list item before those from the collection.</param>
public static ICollection<SelectListItem> ToSelectList<TEnum>(TEnum? currentKey, bool includeEmptyListItem = true) where TEnum : struct
{
return ToEnumSelectList<TEnum>(currentKey, includeEmptyListItem ? DefaultEmptySelectListItem : null);
}
/// <summary>
/// Returns a collection of SelectListItem for each possible value of an Enum, with a specific list item selected and optionally an empty list item.
/// </summary>
/// <example>
/// <code>
/// Colors.Green.ToSelectList();
/// // or
/// Colors.Green.ToSelectList(false); if you don't want the empty list item
/// // or
/// var color = Colors.Green;
/// color.ToSelectList();
/// </code>
/// </example>
/// <param name="currentKey">Value to mark as Selected</param>
/// <param name="includeEmptyListItem">Whether or not a default list item should be the first list item before those from the collection.</param>
public static ICollection<SelectListItem> ToSelectList<TEnum>(this TEnum currentKey, bool includeEmptyListItem = true) where TEnum : struct
{
return ToEnumSelectList<TEnum>(currentKey, includeEmptyListItem ? DefaultEmptySelectListItem : null);
}
/// <summary>
/// Returns a collection of SelectListItem for each possible value of an Enum, with a specific list item selected and a custom empty list item.
/// </summary>
/// <example>
/// <code>
/// Colors.Green.ToSelectList(new SelectListItem() {Text = "~~ Pick One!!! ~~", Value = string.Empty}).Dump();
/// </code>
/// </example>
/// <param name="currentKey">Value to mark as Selected</param>
/// <param name="emptyListItem">The list item to use as the first list item before those from the collection.</param>
public static ICollection<SelectListItem> ToSelectList<TEnum>(this TEnum currentKey, SelectListItem emptyListItem) where TEnum : struct
{
return ToEnumSelectList<TEnum>(currentKey, emptyListItem);
}
private static ICollection<SelectListItem> ToEnumSelectList<TEnum>(TEnum? currentKey, SelectListItem emptyListItem) where TEnum : struct
{
IList<SelectListItem> selectList;
if (typeof(TEnum).GetCustomAttributes(typeof(FlagsAttribute), false).Any())
selectList = Enum.GetValues(typeof(TEnum))
.Cast<TEnum>()
.Select(x => new SelectListItem()
{
Value = x.ToString(),
Text = x.GetDisplayName(),
Selected = currentKey != null && ((int)(object)x & (int)(object)currentKey) == (int)(object)x
})
.ToList();
else
selectList = Enum.GetValues(typeof(TEnum))
.Cast<TEnum>()
.Select(x => new SelectListItem()
{
Value = x.ToString(),
Text = x.GetDisplayName(),
Selected = currentKey != null && (int)(object)x == (int)(object)currentKey
})
.ToList();
if (emptyListItem != null)
selectList.Insert(0, emptyListItem);
return selectList;
}
#endregion
}
public static string GetDisplayName<TEnum>(this TEnum enumer) where TEnum : struct
{
var result = null as string;
var iEnumer = Convert.ToInt32(enumer);
if (!typeof(TEnum).GetTypeInfo().IsDefined(typeof(FlagsAttribute), false) || (iEnumer & (iEnumer - 1)) == 0)
{
var display = enumer.GetType()
.GetMember(enumer.ToString()).First()
.GetCustomAttributes(false)
.OfType<DisplayAttribute>()
.LastOrDefault();
result = display != null ? display.GetName() : enumer.ToString().SplitPascalCase();
}
else
{
result = string.Join(", ", GetValues<TEnum>((Enum)(object)enumer).Select(GetDisplayName));
}
return result ?? enumer.ToString().SplitPascalCase();
}
public static IEnumerable<TEnum> GetValues<TEnum>(this Enum value) where TEnum : struct
{
if (!typeof(TEnum).GetTypeInfo().IsEnum)
{
throw new ArgumentException("Generic parameter must be enum");
}
int valueAsInt = Convert.ToInt32(value, System.Globalization.CultureInfo.CurrentCulture);
foreach (object item in Enum.GetValues(typeof(TEnum)))
{
int itemAsInt = Convert.ToInt32(item, System.Globalization.CultureInfo.CurrentCulture);
if (itemAsInt == (valueAsInt & itemAsInt) && itemAsInt != 0)
{
yield return (TEnum)item;
}
}
}
public static string SplitPascalCase(this string str)
{
return SplitCamelCase(str);
}
public static string SplitCamelCase(this string str)
{
return Regex.Replace(Regex.Replace(str, @"(\P{Ll})(\P{Ll}\p{Ll})", "$1 $2"), @"(\p{Ll})(\P{Ll})", "$1 $2");
}
@nickalbrecht
Copy link
Author

Fixes the logic when dealing with SelectListGroup to not use GroupBy or OrderBy so that it leaves the enumerable's order intact. Refactored out a method for handling which SelectListGroup gets used. Refactored the methods so that the they don't chain so much from one static method to the next to make it easier to follow, and debug if needed. Refactored the methods that use a generic key down to one method instead of two. General cleanups for a few other pieces like comments and a few other small code changed. If anyone encounters an error/bug from using this, let me know :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment