Sunday, March 6, 2011

A useful ASP.NET MVC HTML extension method for rendering a DropDownList

I really like ASP.NET MVC, but understanding the usage of the DropDownListFor HTML helper method is not trivial. Therefore I decided to simplify it by introducing the following HTML extension methods for rendering a DropDownList. The idea is that the data for a dropdownlist is encapsulated in an IDictionary object.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Mvc;
using System.Linq.Expressions;
using System.Diagnostics.CodeAnalysis;
using System.Web.Routing;
using System.Web.Mvc.Html;

namespace My.Helpers.HTML
{
    public static class CuSelectExtensions
    {
        /// <summary>
        /// returns an HTML select element for an IDictionary 
        /// </summary>
        /// <typeparam name="TModel"></typeparam>
        /// <typeparam name="TKey"></typeparam>
        /// <typeparam name="TValue"></typeparam>
        /// <param name="htmlHelper"></param>
        /// <param name="funcForDict">a function that returns an IDictionary</param>
        /// <param name="expForSelectedKey">a expression which returns the selected Key of the IDictionary</param>
        /// <returns></returns>
        public static MvcHtmlString DropDownListFor<TModel, TKey, TValue>(this HtmlHelper<TModel> htmlHelper, Func<TModel, IDictionary<TKey, TValue>> funcForDict, Expression<Func<TModel, TKey>> expForSelectedKey)
        {
            return htmlHelper.DropDownListFor(expForSelectedKey, funcForDict.Invoke(htmlHelper.ViewData.Model).ToSelectList(expForSelectedKey.Compile().Invoke(htmlHelper.ViewData.Model)));
        }

        /// <summary>
        /// Converts a dictionary to a SelectList
        /// </summary>
        /// <typeparam name="TKey"></typeparam>
        /// <typeparam name="TValue"></typeparam>
        /// <param name="dictionary"></param>
        /// <returns></returns>
        public static SelectList ToSelectList<TKey, TValue>(this IDictionary<TKey, TValue> dictionary)
        {
            //call with default value
            return dictionary.ToSelectList(default(TKey));
        }

        /// <summary>
        /// Converts a dictionary to SelectList
        /// </summary>
        /// <typeparam name="TKey"></typeparam>
        /// <typeparam name="TValue"></typeparam>
        /// <param name="dictionary"></param>
        /// <param name="selectedItem"></param>
        /// <returns></returns>
        public static SelectList ToSelectList<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey selectedItem)
        {
            return new SelectList(dictionary, "Key", "Value", selectedItem);
        }
    }
}


Emphasizing the importance to divide view model from data model is fundamental for creating a good MVC project. Keeping in mind this aspect you will notice the importance and the goodness of the extension methods I introduced above.
Let’s suppose we have the following data model and we want to render a form for collecting this kind of data.


public class Address
{
    public virtual int? ID { get; set; }
    public virtual State State { get; set; }
    public virtual string Province { get; set; }
    public virtual string City { get; set; }
    public virtual string Street { get; set; }
    public virtual string PostCode { get; set; }
}

public class State
{
    public virtual int? ID { get; set; }
    public virtual string Desc { get; set; }
}

Then our view model will look like this:


public class AddressView
{
    public IDictionary<int, string> AvailableStates { get; set; }
    public int SelectedState { get; set; }
    public string Province { get; set; }
    public string City { get; set; }
    public string Street { get; set; }
    public string PostCode { get; set; }
}

In the following controller, which access the Business-Logic (BL) for retrieving the Address object and which again is built on top of the DataAccess-Logic (DAL), we convert an Address object into an AddressView object and we pass it to a View for rendering it. For simplifying this example I am going to hide the implementation of the BL and DAL.


public ActionResult Edit(int id)
{
    Address ad = AddressBL.GetByID(id);
    IList<State> states = StateBL.GetAll();
    //Convert datamodel to viewmodel
    AddressView av = new AddressView();
    av.AvailableStates = states.ToDictionary(x => x.ID, x => x.Desc);
    dv.selectedState = ad.State.ID;
    av.Province = ad.Province
    // . . .
    return View(av);
}

Finally, using our proposed implementation for DropDownlistFor in a view is very easy. It is enough to specify an IDictionary object and a Key for the selected element. Here is an example:


<%@ Page Title="" Language="C#" MasterPageFile="~/Site.Master" Inherits="ViewPage<AddressView>" %>

<%@ Import Namespace="My.Helpers.HTML" %>
<asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
    <%: Html.DropDownListFor(x => x.States, x => x.AvailableStates)%>
</asp:Content>

2 comments:

  1. Very nice posting but I am having issues implementing.

    I already had a State IDictionary defined in my app so I included AvailableStates in my model view and then simply assigned my existing static IDictionary to the model in the controller:
    model.AvailableStates = States.StateDic;

    My MVC 3 View code is similar to yours:
    @Html.DropDownListFor(m => m.VMAddress[0].State, m => m.AvailableStates)

    The "m => m.AvailableStates" portion of the above statement is flagged with the following error:
    "Cannot convert lambda expression to type 'System.Collections.Generic.IEnumerable' because it is not a delegate type"

    Any ideas why this would be encountered?

    ReplyDelete
  2. Got error

    Description: An error occurred during the compilation of a resource required to service this request. Please review the following specific error details and modify your source code appropriately.

    Compiler Error Message: CS1660: Cannot convert lambda expression to type 'System.Collections.Generic.IEnumerable' because it is not a delegate type

    Source Error:

    Line 17: @Html.DropDownListFor(x => x.SelectedValue, x => x.PossibleValues)

    ReplyDelete