views:

145

answers:

1

Here's a little background on my solution:

  • ASP.Net MVC app
  • Using Linq-to-SQL with table-per-hierarchy inheritance
  • Using DataAnnotationsModelBinder as default

So I have a Device abstract class and then a series of derived classes (ServerDevice, DiskDevice, PSUDevice, etc) that inherit from it in the proscribed Linq-to-SQL way. I have one controller that handles all these different related model types, and it renders different partials based on the type and a handy drop down to select them. My (GET) Create method looks like this:

// GET: /Devices/Create/3
public ActionResult Create(int? deviceTypeID)
{
    return View(DeviceFactory(deviceTypeID);
}

Where DeviceFactory is a static method returns a new instance of one of the derived classes based on an int discriminator. The POST Create method looks like this:

// POST: /Devices/Create
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create([ModelBinder(typeof(DeviceModelBinder))]Device device)
{
    if (!ModelState.IsValid)
            return View(device);

    _repository.Add(device);
    _repository.Save();
    TempData["message"] = string.Format("Device was created successfully.");
    return RedirectToAction(Actions.Index);
}

and my custom model binder looks like this:

public class DeviceModelBinder : DataAnnotationsModelBinder
{
    private readonly Dictionary<string, Type> _deviceTypes = 
                   new Dictionary<string, Type>
                  {
                      {"1", typeof (ServerDevice)},
                      {"2", typeof (DiskDevice)}
                      // And on and on for each derived type
                  };

    protected override object CreateModel(ControllerContext controllerContext,
        ModelBindingContext bindingContext, Type modelType)
    {
        return base.CreateModel(controllerContext, bindingContext,
        _deviceTypes[bindingContext.ValueProvider["deviceTypeID"].AttemptedValue]);
    }
}

So after trying to hook this up all day, reading about ActionInvoker, custom ActionFilters, and all sorts of other MVC stuff, I'm wondering if the solution I've arrived at is a good one. Help allay my fears that I'm missing some hugely obvious concept and re-inventing the wheel. Is there a better or more concise way?

Thanks!

+1  A: 

My POV is it's "smelly" to bind entity/domain types to a UI at all. I explained this in considerably more detail in this answer. IMHO you should nearly always use dedicated presentation models. It is a nice side benefit that model binding do a presentation model is considerably easier, but the more important benefits are discussed in the linked answer.

Craig Stuntz
What if model is POCO? It's still as bad as it sounds?
Arnis L.
It's important to consider the Single Responsibility Principle. If you need to add a field to a view or reorganize a view (e.g., from a tabular format into a hierarchy), then the view model probably needs to change. But if you refactor your database, then your entity/domain models need to change. Since a type should have only one reason to change, your entity/domain models and your view models should therefore be different types. So it's the use of the types which dictates that they should be different, not what their ancestor type happens to be.
Craig Stuntz
Thing that i knew before - view model adds a nice extensibility point for validation and formating. Anyway - you convinced me. Going to do some serious plumbing.
Arnis L.
Actually I'm using a ViewModel composed of several different entities in my actual code-- I just removed it in the examples for brevity. Unfortunately, a presentation model won't solve the issue because the DefaultModelBinder still won't know which inheritor of the base class to bind to. Still, I agree with your assessment that one should almost always use a presentation model.
Kelly Adams
Kelly, I reported this as a bug a while back: https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=483001 Unfortunately, the team calls it "as designed." The workaround is a custom TryUpdateModel which looks at model.GetType() instead of typeof(TModel)
Craig Stuntz
Turns out I can make it a little less stinky by adding a type-specific binding in RegisterModelBinders, and overriding BindModel instead of CreateModel (to support edit actions as well as create).
Kelly Adams