Search code examples
c#asp.net-mvchtml-helper

Type issue writing a short HtmlHelper DropDownList overloaded method


I am trying to overload the DropDownListFor HtmlHelper method.

Typically, the parameter 'htmlAttributes' would be an anonymous object. If the htmlAttributes was not already a RouteValueDictionary, I would convert it to this type for the purpose of modifying the collection as needed, based upon custom logic that I would add, such as handling disabling the control based on the value of the "disabled" parameter value, as just one example.

    public static MvcHtmlString DropDownListFor<TModel, TProperty>(
            this HtmlHelper<TModel> htmlHelper, 
            Expression<Func<TModel, TProperty>> expression, 
            IEnumerable<SelectListItem> selectList, 
            string optionLabel, 
            object htmlAttributes,
            bool disabled)
    {
        if (!(htmlAttributes is RouteValueDictionary))
        {
            htmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
        }
        else
        {
            htmlAttributes = (RouteValueDictionary)htmlAttributes;
        }

        if (disabled)
        {
            //manipulate the htmlAttributes collection here                     
        }
        return htmlHelper.DropDownListFor(expression, selectList, optionLabel, htmlAttributes);
    }

I am running into a problem when I call the "native" System.Web.Mvc.Html.DropDownListFor in the last line. It does not correctly handle the htmlAttributes being of type "RouteAttributes" when it expected an Anonymous object. As a result, I get HTML that does not properly convert the name/value html attributes of the routingAttributes collection to nanme/value HTML attribute pairs. I get this (note the Keys and Values properties):

For example, when I call it with this:

@Html.DropDownListFor(model => model.FrequencyCode, Model.FrequencyCodes.ToListOfSelectListItem(Model.FrequencyCode), "", new { @class = "form-control" }, true)

I get this:

<input data-val="true" data-val-required="Frequency is required" id="FrequencyCode" name="FrequencyCode" type="hidden" value="" />
<select Count="1" Keys="System.Collections.Generic.Dictionary`2+KeyCollection[System.String,System.Object]" Values="System.Collections.Generic.Dictionary`2+ValueCollection[System.String,System.Object]" id="FrequencyCode" name="FrequencyCode">
<option value=""></option>
<option value="A">ANNUAL</option>
<option value="M">MONTHLY</option>
<option value="Q">QUARTERLY</option>
</select>

It seems one way to fix this would be if I could convert the object of type RouteDictionary back to an anonymous object. How do I do this? Is there a better way?


Solution

  • The best method for solving this is to have multiple overloads, just as the in-build HtmlHelper methods do (refer the source code for the DropDownListFor() methods).

    public static MvcHtmlString DropDownListFor<TModel, TProperty>(
            this HtmlHelper<TModel> htmlHelper, 
            Expression<Func<TModel, TProperty>> expression, 
            IEnumerable<SelectListItem> selectList, 
            string optionLabel, 
            IDictionary<string, object> htmlAttributes,
            bool disabled)
    
    public static MvcHtmlString DropDownListFor<TModel, TProperty>(
            this HtmlHelper<TModel> htmlHelper, 
            Expression<Func<TModel, TProperty>> expression, 
            IEnumerable<SelectListItem> selectList, 
            string optionLabel, 
            object htmlAttributes,
            bool disabled)
    

    However, generating a disabled <select> element does not make a lot of sense - you generating a lot of extra unnecessary html, and disabled controls do not submit there value so model binding may fail, and if the method is binding to a property which is value type, or has validation attributes, ModelState will be invalid. A better approach would be to display just the selected text value and a hidden input (assuming you want to post the value).

    public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, IEnumerable<SelectListItem> selectList, string optionLabel, object htmlAttributes, bool disabled)
    {
        if (disabled)
        {
            StringBuilder html = new StringBuilder();
            // add a hidden input
            html.Append(htmlHelper.HiddenFor(expression).ToString());
            // Get the display text
            ModelMetadata metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
            string value = metaData.Model.ToString();
            var displayText = selectList.Where(x => x.Value == value).Select(x => x.Text).FirstOrDefault();
            TagBuilder div = new TagBuilder("div");
            div.AddCssClass("readonly");
            div.InnerHtml = displayText;
            html.Append(div.ToString());
            return MvcHtmlString.Create(html.ToString());
        }
        return htmlHelper.DropDownListFor(expression, selectList, optionLabel, htmlAttributes);
    }
    

    and then use css to style .readonly to look like a <input /> or <select> element if that's what you want.