.net, C# tip, Clean Code, MVC

MVC – Enhanced DropDownListFor – Part #2

In Part #1, I described a method signature for the Html.DropDownListFor static HtmlHelper method, which was:

@Html.DropDownListFor(m => m.UserId, m => m.UserNames, m => m.Id, m => m.Name)

In this part. I’ll write more about HtmlHelper extension method code to make this work.

That’s how you use it in Razor – but what does this method signature look like in the source code?

Each of the lambda expressions in the above method signature is an expression which is represented by Expression<Func<T1, T2>> expr. The first parameter will represent the name of the form field rendered, i.e. what the Id and Name values are for the Html <select> element. The second parameter represents the items in the <select> list. The third parameter is the property in the items from the above list which should be rendered in the value attribute of each of the <option> elements of the <select> list. The fourth parameter is the property in the items from the above list which should be rendered in the body, i.e. the innerHTML of each of the <option> elements of the <select> list. So the static extension method signature will have the this HtmlHelper<TModel> as the first parameter, with four expression parameters following, each representing one of the four lambdas in the code snippet above.

public static MvcHtmlString DropDownListFor<TModel, TListItemType, TItemId, TItemName, TSelectedValue>(
            this HtmlHelper<TModel> htmlHelper,
            Expression<Func<TModel, TSelectedValue>> formFieldName,
            Expression<Func<TModel, List<TListItemType>>> items,
            Expression<Func<TListItemType, TItemId>> optionValueProperty,
            Expression<Func<TListItemType, TItemName>> optionInnerHTMLProperty)
        {
            ...
        }

Already this is starting to look quite complicated. But this is just a cleaner façade around something that is already possible. The body of this method really just gets the data required for the existing DropDownList function (which is within the System.Web.Mvc.Html.SelectExtensions namespace.) So we just need to code a conversion to the signature below.

return SelectExtensions.DropDownList(htmlHelper, formFieldName, selectList);

So let’s look at each of these parameters in turn, and how to populate them from our improved method signature. The HtmlHelper We already have this as a parameter to our overload, so we just pass it in without any modification.

The Form Field Name

Getting the property name as text is really easy – just use the ExpressionHelper on the formFieldName expression (in our example, this is m => m.UserId)

var formField = ExpressionHelper.GetExpressionText(formFieldName);

The Select List

It’s just as easy to get the selected model value from the formFieldName expression as well.

var formFieldValue = ModelMetadata.FromLambdaExpression(formFieldName, htmlHelper.ViewData).Model;

And we perform the same operation on the items expression, but just cast it as List<TListItemType>.

var itemsModel = ModelMetadata.FromLambdaExpression(items, htmlHelper.ViewData).Model as List<TListItemType>

So now we have the list, we need to convert it to a SelectList. To do this, we need to get the names of the methods used for the value and text fields. We can do this using the ExpressionHelper again.

var itemIdPropertyName = ExpressionHelper.GetExpressionText(optionValueProperty);
var itemNamePropertyName = ExpressionHelper.GetExpressionText(optionInnerHTMLProperty);

From this, populating the SelectList is a very simple operation:

var selectList = new SelectList(listItemsModel, itemIdPropertyName, itemNamePropertyName, selectedValueObject);

Final Things

The standard HTMLHelper extensions have some optional parameters, such as htmlAttributes. The DropDownList is no different – so I’ve added in the optionLabel and htmlAttributes parameters for completeness.

The Finished Code

/// <summary>
/// Returns a single-selection HTML &lt;select&gt; element for the expression <paramref name="name" />,
/// using the specified list items.
/// </summary>
/// <typeparam name="TModel">The type of the model.</typeparam>
/// <typeparam name="TListItemType">The type of the items in the list.</typeparam>
/// <typeparam name="TItemId">The type of the item identifier.</typeparam>
/// <typeparam name="TItemName">The type of the item name.</typeparam>
/// <typeparam name="TSelectedValue">The type of the selected value expression result.</typeparam>
/// <param name="htmlHelper">The HTML helper instance that this method extends.</param>
/// <param name="formFieldName">Name of the form field.</param>
/// <param name="items">The items to put in the  HTML &lt;select&gt; element.</param>
/// <param name="optionValueProperty">The item identifier property.</param>
/// <param name="optionInnerHTMLProperty">The item name property.</param>
/// <param name="optionLabel">The text for a default empty item. Does not include such an item if argument is <c>null</c>.</param>
/// <param name="htmlAttributes">An <see cref="object" /> that contains the HTML attributes for the &lt;select&gt; element. Alternatively, an
/// <see cref="IDictionary{string, object}" /> instance containing the HTML attributes.</param>
/// <returns>A new MvcHtmlString containing the &lt;select&gt; element.</returns>
public static MvcHtmlString DropDownListFor<TModel, TListItemType, TItemId, TItemName, TSelectedValue>(
    this HtmlHelper<TModel> htmlHelper,
    Expression<Func<TModel, TSelectedValue>> formFieldName,
    Expression<Func<TModel, List<TListItemType>>> items,
    Expression<Func<TListItemType, TItemId>> optionValueProperty,
    Expression<Func<TListItemType, TItemName>> optionInnerHTMLProperty,
    [Optionalstring optionLabel,
    [Optionalobject htmlAttributes)
{
    var formField = ExpressionHelper.GetExpressionText(formFieldName);
    var itemIdPropertyName = ExpressionHelper.GetExpressionText(optionValueProperty);
    var itemNamePropertyName = ExpressionHelper.GetExpressionText(optionInnerHTMLProperty);
 
    var listItemsModel = GetModelFromExpressionAndViewData(items, htmlHelper.ViewData) as List<TListItemType>;
 
    // if the list is null, initialize to an empty list so we display something
    if (listItemsModel == null)
    {
        listItemsModel = new List<TListItemType>();
    }
 
    var selectedValueObject = GetModelFromExpressionAndViewData(formFieldName, htmlHelper.ViewData);
 
    var selectList = new SelectList(listItemsModel, itemIdPropertyName, itemNamePropertyName, selectedValueObject);
 
    return SelectExtensions.DropDownList(htmlHelper: htmlHelper, name: formField, selectList: selectList, optionLabel: optionLabel, htmlAttributes: htmlAttributes);
}
 
/// <summary>
/// Gets the model from expression and view data.
/// </summary>
/// <typeparam name="TModel">The type of the model.</typeparam>
/// <typeparam name="TSelectedValue">The type of the selected value expression result.</typeparam>
/// <param name="expressionThatDefinesTheModel">The expression that defines the model.</param>
/// <param name="viewDataDictionary">The view data dictionary.</param>
/// <returns>System.Object.</returns>
private static object GetModelFromExpressionAndViewData<TModel, TSelectedValue>(Expression<Func<TModel, TSelectedValue>> expressionThatDefinesTheModel, ViewDataDictionary<TModel> viewDataDictionary)
{
    var metaData = ModelMetadata.FromLambdaExpression(expressionThatDefinesTheModel, viewDataDictionary);
 
    return metaData.Model;
}