MVC2 EditorTemplate for DropDownList
- by tschreck
I've spent the majority of the past week knee deep in the new templating functionality baked into MVC2. I had a hard time trying to get a DropDownList template working. The biggest problem I've been working to solve is how to get the source data for the drop down list to the template. I saw a lot of examples where you can put the source data in the ViewData dictionary (ViewData["DropDownSourceValuesKey"]) then retrieve them in the template itself (var sourceValues = ViewData["DropDownSourceValuesKey"];) This works, but I did not like having a silly string as the lynch pin for making this work.
Below is an approach I've come up with and wanted to get opinions on this approach:
here are my design goals:
The view model should contain the source data for the drop down list
Limit Silly Strings
Not use ViewData dictionary
Controller is responsible for filling the property with the source data for the drop down list
Here's my View Model:
public class CustomerViewModel
{
[ScaffoldColumn(false)]
public String CustomerCode{ get; set; }
[UIHint("DropDownList")]
[DropDownList(DropDownListTargetProperty = "CustomerCode"]
[DisplayName("Customer Code")]
public IEnumerable<SelectListItem> CustomerCodeList { get; set; }
public String FirstName { get; set; }
public String LastName { get; set; }
public String PhoneNumber { get; set; }
public String Address1 { get; set; }
public String Address2 { get; set; }
public String City { get; set; }
public String State { get; set; }
public String Zip { get; set; }
}
My View Model has a CustomerCode property which is a value that the user selects from a list of values. I have a CustomerCodeList property that is a list of possible CustomerCode values and is the source for a drop down list. I've created a DropDownList attribute with a DropDownListTargetProperty. DropDownListTargetProperty points to the property which will be populated based on the user selection from the generated drop down (in this case, the CustomerCode property).
Notice that the CustomerCode property has [ScaffoldColumn(false)] which forces the generator to skip the field in the generated output.
My DropDownList.ascx file will generate a dropdown list form element with the source data from the CustomerCodeList property. The generated dropdown list will use the value of the DropDownListTargetProperty from the DropDownList attribute as the Id and the Name attributes of the Select form element. So the generated code will look like this:
<select id="CustomerCode" name="CustomerCode">
<option>...
</select>
This works out great because when the form is submitted, MVC will populate the target property with the selected value from the drop down list because the name of the generated dropdown list IS the target property. I kinda visualize it as the CustomerCodeList property is an extension of sorts of the CustomerCode property. I've coupled the source data to the property.
Here's my code for the controller:
public ActionResult Create()
{
//retrieve CustomerCodes from a datasource of your choosing
List<CustomerCode> customerCodeList = modelService.GetCustomerCodeList();
CustomerViewModel viewModel= new CustomerViewModel();
viewModel.CustomerCodeList = customerCodeList.Select(s => new SelectListItem() { Text = s.CustomerCode, Value = s.CustomerCode, Selected = (s.CustomerCode == viewModel.CustomerCode) }).AsEnumerable();
return View(viewModel);
}
Here's my code for the DropDownListAttribute:
namespace AutoForm.Attributes
{
public class DropDownListAttribute : Attribute
{
public String DropDownListTargetProperty { get; set; }
}
}
Here's my code for the template (DropDownList.ascx):
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IEnumerable<SelectListItem>>" %>
<%@ Import Namespace="AutoForm.Attributes"%>
<script runat="server">
DropDownListAttribute GetDropDownListAttribute()
{
var dropDownListAttribute = new DropDownListAttribute();
if (ViewData.ModelMetadata.AdditionalValues.ContainsKey("DropDownListAttribute"))
{
dropDownListAttribute = (DropDownListAttribute)ViewData.ModelMetadata.AdditionalValues["DropDownListAttribute"];
}
return dropDownListAttribute;
}
</script>
<% DropDownListAttribute attribute = GetDropDownListAttribute();%>
<select id="<%= attribute.DropDownListTargetProperty %>" name="<%= attribute.DropDownListTargetProperty %>">
<% foreach(SelectListItem item in ViewData.Model)
{%>
<% if (item.Selected == true) {%>
<option value="<%= item.Value %>" selected="true"><%= item.Text %></option>
<% } %>
<% else {%>
<option value="<%= item.Value %>"><%= item.Text %></option>
<% } %>
<% } %>
</select>
I tried using the Html.DropDownList helper, but it would not allow me to change the Id and Name attributes of the generated Select element.
NOTE: you have to override the CreateMetadata method of the DataAnnotationsModelMetadataProvider for the DropDownListAttribute. Here's the code for that:
public class MetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
var additionalValues = attributes.OfType<DropDownListAttribute>().FirstOrDefault();
if (additionalValues != null)
{
metadata.AdditionalValues.Add("DropDownListAttribute", additionalValues);
}
return metadata;
}
}
Then you have to make a call to the new MetadataProvider in Application_Start of Global.asax.cs:
protected void Application_Start()
{
RegisterRoutes(RouteTable.Routes);
ModelMetadataProviders.Current = new MetadataProvider();
}
Well, I hope this makes sense and I hope this approach may save you some time. I'd like some feedback on this approach please. Is there a better approach?