Ever feel like the HTML
element just didn't get enough lovin' in ASP.NET MVC? The HtmlHelper.DropDownList
helper is a definite plus, but mapping
data to it from a view model tends to be a bit too ridged as it depends upon a MVC specific class, System.Web.Mvc.SelectList
. No worries, the great thing about programming is that we can take it upon ourselves to craft solutions that fit our needs. Let's take a look at how we can handle extending the DropDownList
helper and make it easier to work with data in a view model to populate our HTML
elements.
The current overload methods for
System.Web.Mvc.HtmlHelper.DropDownList
:
We can see that all the methods that provide a means for populating the
elements work off of an IEnumerable
object. The sticker here is the SelectListItem
type. In order to pass data into the helper we need to package it up in SelectListItem
objects. So our code that handles prepping data for the view will need to know about an MVC specific construct. Not that there is necessarily anything wrong with that, but it can be limiting to your decoupling efforts if you are into that sort of thing (which can be a very good thing to be into).
We can work around this by writing our own extension methods on the
HtmlHelper
class. Let's begin by writing an extension method in a static class forHtmlHelper.DropDownList
. This extension method will allow us to pass in a string to represent the name to use for the HTML select
element and a Dictionary
collection object for the HTML option
elements. The method will handle mapping the dictionary to a System.Web.Mvc.SelectList
object that can then be passed to one of the existing DropDownList
methods.using System.Collections.Generic;
using System.Web.Mvc;
using System.Web.Mvc.Html;
namespace Website.Models
{
public static class ExtensionMethods
{
public static MvcHtmlString DropDownList(this HtmlHelper helper,
string name, Dictionary<int, string> dictionary)
{
var selectListItems = new SelectList(dictionary, "Key", "Value");
return helper.DropDownList(name, selectListItems);
}
}
}
The
SelectList
constructor supports passing in an IEnumerable
object and two strings that represent the property names of the value field and text field to use from the objects in the enumerable collection. This makes it easy for us to pass in the incoming dictionary object and the name of the properties on the objects in the dictionary. Enumerating over a Dictionary
object results in KeyValuePair
objects that have properties named Key
and Value
, so those are the string values we would use to tell the SelectList
object how to map the data. With the SelectList
created, we can call the existing DropDownList
method on theHtmlHelper
and be on our way.
With the extension method in place we can turn our attention to crafting a view model and loading up data for populating a select list. Imagine we needed a select list for days of the week where the text is the name of the day and the value is an integer representing the day number in the week. If we were to create a class named
PageWithSelectList
as our view model, we could fill it out with the following:using System.Collections.Generic;
namespace Website.Models
{
public class PageWithSelectList
{
public Dictionary<int, string> DaysOfWeek { get; set; }
public int DayOfWeek { get; set; }
public PageWithSelectList()
{
this.DaysOfWeek = new Dictionary<int, string>
{
{1, "Sunday"},
{2, "Monday"},
{3, "Tuesday"},
{4, "Wednesday"},
{5, "Thursday"},
{6, "Friday"},
{7, "Saturday"}
};
}
}
}
The
DaysOfWeek
would represent the data for the option
elements. The DayOfWeek
would represent the selected day key value (if any). The constructor handles loading up the DaysOfWeek
with some data.
Moving to the controller, we can initialize an instance of the
PageWithSelectList
class and pass that to a view. Using a HomeController
as an example, the Index
action method could look like so:using System.Web.Mvc;
using Website.Models;
namespace Website.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new PageWithSelectList();
return View(model);
}
}
}
Before we turn our attention to the view file we need to address some details in the way the Razor view engine works. In previous versions of MVC you were able to add namespaces to the
pages
node in your main Web.config
file to make them available to your view files without needing to add using statements within your views. With the Razor view engine there is a new node where these need to go (
) and they need to go into the Web.config
file located in theViews
directory.<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0,
Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<pages pageBaseType="System.Web.Mvc.WebViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
<add namespace="Website.Models" />
</namespaces>
</pages>
</system.web.webPages.razor>
With the default MVC 3 projects in Visual Studio 2010 this node will automatically be added to the
Web.config
files in the Views
directories. In the example above we have added the Website.Models
namespace to the list so our views will have access to it and thus be able to call our extension method.Note
If you use Areas you will need to add namespace nodes to eachWeb.config
in eachAreas/
directory if you want them available. Right now it doesn't look like adding the/Views node to the top level
Web.config
is supported.
With those minor details handled we can build our view content. We make the view strongly typed to our
Website.Models.PageWithSelectList
class and add a call to our new extension method, passing in the name of our DayOfWeek
property to align the html element id with our view model field and the DaysOfWeek
object from our view model for populating the select options.@model PageWithSelectList
@{
ViewBag.Title = "Index";
}
@Html.DropDownList("DayOfWeek", Model.DaysOfWeek)
Since we added the
Website.Models
namespace to the Web.config
file in the Views
directory we do not need to use the namespace in our @model
declaration, nor do we need to include a @using
declaration. However, if we didn't add the namespace to the Web.config
our view would look like:@using Website.Models
@model PageWithSelectList
@{
ViewBag.Title = "Index";
}
@Html.DropDownList("DayOfWeek", Model.DaysOfWeek)
The resulting html that gets rendered from the view looks like so:
<select id="DayOfWeek" name="DayOfWeek">
<option value="1">Sunday</option>
<option value="2">Monday</option>
<option value="3">Tuesday</option>
<option value="4">Wednesday</option>
<option value="5">Thursday</option>
<option value="6">Friday</option>
<option value="7">Saturday</option>
</select>
With that, we have added some decoupling love to the
HtmlHelper.DropDownList
method! But the "out of the box" helper is not completely without love. In fact, it is tricked out to make our life extremely easy when it comes to pre-selecting an option in our view. Since our extension method is handling the mapping of our data structure into a structure the existing HtmlHelper.DropDownList
method supports we get all the benefits of the existing method. The method will look for an existing property in the view model that matches the name value passed into the DropDownList
method and will set the Selected
property on the SelectListItem
that has a matching value. By adding the DayOfWeek
property to our PageWithSelectList
view model we can set that property in our HomeController.Index
action method and the HtmlHelper.DropDownList
method will take care of things from there prior to rendering the html.
Setting the property on our model in the action method:
using System.Web.Mvc;
using Website.Models;
namespace Website.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new PageWithSelectList();
model.DayOfWeek = 3;
return View(model);
}
}
}
With no changes needed to our view, the resulting html that gets rendered:
<select id="DayOfWeek" name="DayOfWeek">
<option value="1">Sunday</option>
<option value="2">Monday</option>
<option selected="selected" value="3">Tuesday</option>
<option value="4">Wednesday</option>
<option value="5">Thursday</option>
<option value="6">Friday</option>
<option value="7">Saturday</option>
</select>
Maybe a
Dictionary
is not ideal for what your application needs. No problem. Simply create extension methods that take in data structures that you need. If they implement IEnumerable
you can use the same example code logic we have already set up. If they don't, just handle mapping your data structure manually in your extension method logic.
Don't like the notion of working with "magic strings"? Create an extension method for the
HtmlHelper.DropDownListFor
method:using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Web.Mvc;
using System.Web.Mvc.Html;
namespace Website.Models
{
public static class ExtensionMethods
{
public static MvcHtmlString DropDownList(this HtmlHelper helper,
string name, Dictionary<int, string> dictionary)
{
var selectListItems = new SelectList(dictionary, "Key", "Value");
return helper.DropDownList(name, selectListItems);
}
public static MvcHtmlString DropDownListFor<TModel, TProperty>(this HtmlHelper<TModel> helper,
Expression<Func<TModel, TProperty>> expression, Dictionary<int, string> dictionary)
{
var selectListItems = new SelectList(dictionary, "Key", "Value");
return helper.DropDownListFor(expression, selectListItems);
}
}
}
Then in your view you can use a strongly typed expression to reference the view model property for the html select element name:
@model PageWithSelectList
@{
ViewBag.Title = "Index";
}
@Html.DropDownListFor(i=>i.DayOfWeek, Model.DaysOfWeek)
Now get back to work and show those html select elements who's boss by bending your MVC code to your will!
No comments:
Post a Comment