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?
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.