I know this question is already answered, but it got me thinking.
So I think I found a way of doing this with some conventions.
First, I made the entities inherit from a base class like this:
public abstract class Entity
{
}
public class Question : Entity
{
[Required]
public int QuestionId { get; set; }
[Required]
public string QuestionText { get; set; }
[Required]
public string Answer { get; set; }
public virtual Category Category { get; set; }
}
public class Category : Entity
{
[Key]
[Required]
public int CategoryId { get; set; }
[Required]
public string CategoryName { get; set; }
public virtual List<Question> Questions { get; set; }
}
So, I also changed the Question model to not have an extra property called CategoryId.
For the form all I did was:
@Html.DropDownList("CategoryId", (IEnumerable<SelectListItem>)ViewBag.Categories, "Select a Category")
So here's the second convention, you'd have to have a property field be named with an Id suffix.
Finally, the CustomModelBinder and CustomModelBinderProvider
public class CustomModelBinderProvider : IModelBinderProvider
{
private readonly IKernel _kernel;
public CustomModelBinderProvider(IKernel kernel)
{
_kernel = kernel;
}
public IModelBinder GetBinder(Type modelType)
{
if (!typeof(Entity).IsAssignableFrom(modelType))
return null;
Type modelBinderType = typeof (CustomModelBinder<>)
.MakeGenericType(modelType);
// I registered the CustomModelBinder using Windsor
return _kernel.Resolve(modelBinderType) as IModelBinder;
}
}
public class CustomModelBinder : DefaultModelBinder where T : Entity
{
private readonly QuestionsContext _db;
public CustomModelBinder(QuestionsContext db)
{
_db = db;
}
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var model = base.BindModel(controllerContext, bindingContext) as T;
foreach (var property in typeof(T).GetProperties())
{
if (property.PropertyType.BaseType == typeof(Entity))
{
var result = bindingContext.ValueProvider.GetValue(string.Format("{0}Id", property.Name));
if(result != null)
{
var rawIdValue = result.AttemptedValue;
int id;
if (int.TryParse(rawIdValue, out id))
{
if (id != 0)
{
var value = _db.Set(property.PropertyType).Find(id);
property.SetValue(model, value, null);
}
}
}
}
}
return model;
}
}
The CustomModelBinder will look for properties of type Entity and load the data with the passed Id using EF.
Here I am using Windsor to inject the dependencies, but you could use any other IoC container.
And that's it. You have a way to make that binding automagically.