ASP.NET MVC: Loading data for select lists into edit model using attributes

After reading ASP.NET MVC 2 in Action and watching a presentation from MvcConf by Jimmy Bogard, I decided to implement some of their ideas in my current project. They use the concept of AutoMapViewResults, which are pretty neat. They use AutoMapper to map the model to a displaymodel or inputmodel. If you want to see exactly, what's going on there, you should watch Jimmy's presentation. The result is a really tiny controller:

public ActionResult Show(Event id)
{
  return AutoMapView<EventsShowModel>(id);
}

or

public ActionResult Edit(Event id)
{
  return AutoMapView<EventsEditModel>(id);
}

One problem now is that in editmodels you often need some additional data, for example for select lists. I found no satisfying solution achieving this with AutoMapper. So after asking about this on Stack Overflow, I decided to try it with a custom attribute. And here is what I came up with. It works for every entity type I have in my database, so there's only this one attribute for all of my select lists in my project.

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class LoadSelectListDataAttribute : Attribute
{
    public Type DataType { get; set; }  // defines the entity type I want to populate the select list with
    public string TextPropertyName { get; set; }  // defines the property of the entity, that is used for the text in the select box
    public string ValuePropertyName { get; set; }  // defines the property of the entity, that is used for the value in the select box
    public string SelectedValuePropertyName { get; set; }  // defines some other property on the editmodel, that contains the selected value
    public object SelectedValue { get; set; }  // defines a fixed selected value (if SelectedValuePropertyName is set, then this will be overriden)

    public LoadSelectListDataAttribute() { }
    public LoadSelectListDataAttribute(Type dataType, string textPropertyName, string valuePropertyName) : this(dataType, textPropertyName, valuePropertyName, null) { }
    public LoadSelectListDataAttribute(Type dataType, string textPropertyName, string valuePropertyName, string selectedValueProperty)
    {
        DataType = dataType;
        TextPropertyName = textPropertyName;
        ValuePropertyName = valuePropertyName;
        SelectedValuePropertyName = selectedValueProperty;
    }
}
// the class that actually handles the editmodel/attribute
public static class LoadSelectListDataHandler
{
    public static void Handle(object objectToHandle)
    {
        var properties = objectToHandle.GetType().GetProperties();
        foreach (var property in properties)  // iterate through each property of the editmodel
        {
            if (typeof(IList<SelectListItem>).IsAssignableFrom(property.PropertyType))  // checks if the property is of type IList<SelectListItem>
            {
                var attribute = (LoadSelectListDataAttribute)Attribute.GetCustomAttribute(property, typeof(LoadSelectListDataAttribute));  // checks LoadSelectListDataAttribute
                if (attribute != null)
                {
                    string selectedValue = (string)attribute.SelectedValue;
                    if (attribute.SelectedValuePropertyName != null)
                    {
                        // if SelectedValuePropertyName is set on the attribute then load the appropriate value
                        selectedValue = objectToHandle.GetType().GetProperty(attribute.SelectedValuePropertyName).GetValue(objectToHandle, null).ToString();
                    }
                    var service = (IService)IoC.Resolve(typeof(IService<>).MakeGenericType(attribute.DataType));  // get the Service for the specified data type using IoC (IoC.Resolve is just a wrapper about my actual IoC container, currently StructureMap)
                    var items = service.All();
                    var data = (from x in items
                                let text = x.GetType().GetProperty(attribute.TextPropertyName).GetValue(x, null).ToString()  // get text from entity
                                let value = x.GetType().GetProperty(attribute.ValuePropertyName).GetValue(x, null).ToString()  // get value from entity
                                select new SelectListItem
                                {
                                    Text = text,
                                    Value = value,
                                    Selected = value == selectedValue
                                }).OrderBy(x => x.Text).ToList();
                    property.SetValue(objectToHandle, data, null);  // set the value of the editmodel's property (with the LoadSelectListDataAttribute) to the generated list
                }
            }
        }
    }
}

How to use this attribute?

// decorate your viewmodel/editmodel
public class EventsEditModel
{
	public string Name {get; set;}

	[LoadSelectListData(typeof(Location), "Name", "Id", "LocationId")]
	public IList<SelectListItem> Locations {get; set;}
	public int LocationId {get; set;}
}


// handle viewmodel/editmodel in AutoMapViewResult
public class AutoMapViewResult : ViewResult
{
    public AutoMapViewResult(string viewName, string masterName, object model)
    {
        ViewName = viewName;
        MasterName = masterName;

        var viewModel = Mapper.Map(model, model.GetType(), typeof(TDestination));  // map model to viewmodel/editmodel
        LoadSelectListDataHandler.Handle(viewModel);  // load select list data, if LoadSelectListDataAttribute is applied
        ViewData.Model = viewModel;
    }
}

What do you think about this approach? Feedback is welcome, or even better other ideas, solutions etc.

Code download: LoadSelectListDataAttribute.zip (950,00 bytes)


Posted by: Dave
Posted on: 8/19/2010 at 9:54 PM
Tags: , , Categories: Actions: E-mail | Kick it! | DZone it! | del.icio.us
Post Information: Permalink | Comments (1) | Post RSSRSS comment feed
Administration: