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)
dc692aa6-a384-49e1-aeab-d4a9681f04d1|0|.0