ASP.NET MVC 5 DropDownList - warning

If you are using DropDownListFor or DropDownList helper in ASP.NET MVC web application you might run into trouble when using it. In this blog I will explain where you might get problems. This blog is posted for helper which is in assembly: System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35.

First problem is that not the proper value is selected because this helper uses strings to compare values instead of comparing it with specific types. A good example for having problems are decimal values. For example if you have a list of decimal values with scale of two the helper would fail to compare 2.3 and 2.30 just because it is using string to compare values. And even if you create a select list with proper value selected, it still uses its own method to select a default value. Here is the code from source file:

object defaultValue = allowMultiple ? htmlHelper.GetModelStateValue(fullHtmlFieldName, typeof (string[])) : htmlHelper.GetModelStateValue(fullHtmlFieldName, typeof (string));
if (defaultValue == null && !string.IsNullOrEmpty(name))
{
    if (!flag)
        defaultValue = htmlHelper.ViewData.Eval(name);
    else if (metadata != null)
        defaultValue = metadata.Model;
}
if (defaultValue != null)
    selectList = SelectExtensions.GetSelectListWithDefaultValue(selectList, defaultValue, allowMultiple);

As seen from code above it never checks if selected list already has an item selected. Instead it selects a default value in this method GetSelectListWithDefaultValue. And in this method comparison of values might fail because it compares string instead of specified types (e.g. decimal):

HashSet<string> hashSet = new HashSet<string>(
    Enumerable.Concat<string>(
        Enumerable.Select<object, string>(Enumerable.Cast<object>(source),
            (Func<object, string>)
                (value => Convert.ToString(value, (IFormatProvider) CultureInfo.CurrentCulture))),
        Enumerable.Select<Enum, string>(
            Enumerable.Cast<Enum>((IEnumerable) Enumerable.OfType<Enum>(source)),
            (Func<Enum, string>) (value => value.ToString("d")))),
    (IEqualityComparer<string>) StringComparer.OrdinalIgnoreCase);

Second problem comes in consequence with first one. If the helper could not find an default value nothing happens (no exception is thrown) in mater of fact the browser selects by default first value. For example your select list is 1.3, 2.2, 3.3 and your value in model is 3.30 then your browser selects first item which is 1.3. But the user leaves drop down without changing its value. When he would submit form with model back to controller the value would be changed.

To conclude if you are using DropDownList or DropDownListFor you have to notice that default value is compared by string meaning you might have problems with decimal values, dates and others which are culture specific. In addition to that if you are editing a model no exception is thrown if no value is selected and even worst it selects the first option available which could lead to updating values with wrong values.

The solution to first problem would be to use culture independent types for value e.g. integer. While there is no solution to second problem other then to write own helpers and that is exactly what we did.

The first helper creates html tag with options besides that it also checks if exactly one item is selected and that all values are unique:

public static MvcHtmlString CustomDropDownList
    (this HtmlHelper htmlHelper,string name, IEnumerable<SelectListItem> selectListItems, object htmlAttributes = null)
{
    var selectTag = new TagBuilder("select");
    selectTag.Attributes["id"] = name.Replace('.', '_');
    selectTag.Attributes["name"] = name;
    if (htmlAttributes != null)
    {
        var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
        selectTag.MergeAttributes(attributes, true);
    }

    if (selectListItems == null || !selectListItems.Any())
        return MvcHtmlString.Create(selectTag.ToString(TagRenderMode.Normal));

    if (selectListItems.Count(x => x.Selected) != 1)
        throw new Exception("Select options not valid!");

    if (selectListItems.Select(x => x.Value).Distinct().Count() != selectListItems.Count())
        throw new Exception("Values are not unique!");

    foreach (var selectListItem in selectListItems)
    {
        var optionTag = new TagBuilder("option");
        optionTag.Attributes["value"] = selectListItem.Value;
        optionTag.InnerHtml = selectListItem.Text;
        if (selectListItem.Selected)
            optionTag.Attributes["selected"] = "selected";
        selectTag.InnerHtml += optionTag.ToString(TagRenderMode.Normal);
    }

    return MvcHtmlString.Create(selectTag.ToString(TagRenderMode.Normal));
}


public static MvcHtmlString CustomDropDownListFor<TModel, TProperty>
    (this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression,
    IEnumerable<SelectListItem> selectListItems, object htmlAttributes = null)
{
    var name = ExpressionHelper.GetExpressionText(expression);

    return htmlHelper.CustomDropDownList(name, selectListItems, htmlAttributes);
}

And then we have to create helper for creating select list:

    public static IEnumerable<SelectListItem> GetSelectListItems
    (object value, Type valueType, IEnumerable<object> values, bool allowNull = false,
    string textIfAllowNull = "", string textIfZero = "")
{
    if (values == null || !values.Any())
        return null;

    if (values.Distinct().Count() != values.Count())
            throw new Exception("Values are not unique!");

    var listItems = new List<SelectListItem>();

    if (Nullable.GetUnderlyingType(valueType) != null && allowNull)
        listItems.Add(NullToOption(textIfAllowNull, value));

    listItems.AddRange(values.Select(x => ObjectToOption(value, valueType, x, textIfZero)));

    if (listItems.Count(x=>x.Selected) != 1)
        throw new Exception("Select options not valid!");

    return listItems;
}

private static SelectListItem NullToOption(string textIfZero, object value)
{
    var item = new SelectListItem();
    item.Value = "";
    item.Text = textIfZero;
    if (value == null)
        item.Selected = true;
    return item;
}

private static SelectListItem ObjectToOption(object value, Type valueType, object optionValue, string textIfZero)
{
    if (valueType == typeof (int) || valueType == typeof (int?))
        return IntToOption(value, optionValue, textIfZero);

    if (valueType == typeof (decimal) || valueType == typeof (decimal?))
        return DecimalToOption(value, optionValue, textIfZero);

    if (valueType.IsEnum ||
        (Nullable.GetUnderlyingType(valueType) != null &&
         Nullable.GetUnderlyingType(valueType).IsEnum))
        return EnumToOption(value, valueType, optionValue);

    throw new Exception("Type of property not supported!");
}

private static SelectListItem IntToOption(object value, object optionValue, string textIfZero)
{
    var item = new SelectListItem();
    item.Value = Convert.ToString((int)optionValue);

    if ((int) optionValue == 0)
        item.Text = textIfZero;
    else
        item.Text = Convert.ToString((int) optionValue);

    if ((int?) value == (int) optionValue)
        item.Selected = true;

    return item;
}

private static SelectListItem DecimalToOption(object value, object optionValue, string textIfZero)
{
    var item = new SelectListItem();
    item.Value = Convert.ToString((decimal)optionValue, CultureInfo.CurrentCulture);

    if ((decimal) optionValue == 0M)
        item.Text = textIfZero;
    else
        item.Text = Convert.ToString((decimal) optionValue, CultureInfo.CurrentCulture);

    if ((decimal?) value == (decimal) optionValue)
        item.Selected = true;

    return item;
}

private static SelectListItem EnumToOption(object value, Type valueType , object optionValue)
{
    var item = new SelectListItem();

    var enumValue = Enum.Parse(
        Nullable.GetUnderlyingType(valueType) ?? valueType, Convert.ToString(optionValue));
    item.Value = Convert.ToString((int)enumValue);

    item.Text = DisplayNameHelper.GetEnumFieldDisplayValue(enumValue);

    if ((value != null) &&
        ((int) Enum.Parse(Nullable.GetUnderlyingType(valueType) ?? valueType,
            Convert.ToString(value)) == (int) enumValue))
        item.Selected = true;

    return item;
}

NOTE: This helper only supports integer, decimal and enum types.

Idea behind this helpers is to compare values by specific types and to check that values are unique and exactly one is selected. This is helpful if there are any possibilities that values can change or are dynamically generated.
If you are using MVC helpers for usual properties like sex (Male, Female) then this helpers are more then enough.