views:

824

answers:

3

I'm having trouble trying to think what the best way is to recreate a database object in a controller Action.

I want to make use of ModelBinders so in my action I have access to the object via a parameter, rather than having to repeat code to get an object from the database based on an identifier parameter. So I was thinking of having a ModelBinder that performs a call to the dataaccess layer to obtain the original object (or creates a new one if it doesn't exist in the database), then binds any properties to the database object to update it. However I've read that the ModelBinders shouldn't make database queries (first comment of this article).

If the ModelBinder shouldn't perform a database query (so just using the DefaultModelBinder) then what about database objects that have properties that are other db objects? These would never get assigned.

Saving an object after the user has edited it (1 or 2 properties are editable in the view) the ModelBinded object would be missing data, so saving it as it is would result in data in the database being overwritten with invalid values, or NOT-NULL constraints failing.

So, whats the best way to get an object in a controller action from the database bound with the form data posted back from the view?

Note im using NHibernate.

+2  A: 

I get the model object from the database, then use UpdateModel (or TryUpdateModel) on the object to update values from the form parameters.

public ActionResult Update( int id )
{
     DataContext dc = new DataContext();
     MyModel model = dc.MyModels.Where( m => m.ID == id ).SingleOrDefault();

     string[] whitelist = new string[] { "Name", "Property1", "Property2" };

     if (!TryUpdateModel( model, whitelist )) {
        ... model error handling...
        return View("Edit");
     }

     ViewData.Model = model;

     return View("Show");
}
tvanfosson
I've found that if I have a property on my model that is assigned to some value and that doesn't exist in the FormCollection, when doing TryUpdateModel the property is set to null even if I include/exclude properties. So basically TryUpdateModel is causing data loss, which isn't good.
Luke Smith
It should only replace the properties that are in the whitelist if you've specified one. I know that this works as I have some edit views that can only update some values not others. You can look at the actual code for it at http://www.codeplex.com/aspnet to verify.
tvanfosson
+2  A: 

Unfortunately you don't have control over the construction of the model binder, so you can't inject any repository implementation.

You can reach out directly into a service locator to pull in your repository & fetch the item:

public class ProductBinder : DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext, 
        ModelBindingContext bindingContext, Type modelType)
    {
        if(modelType != typeof(Product))
            return null;

        var form = controllerContext.HttpContext.Request.Form;
        int id = Int32.Parse(form["Id"]);
        if(id == 0)
            return base.CreateModel(controllerContext, bindingContext, modelType);

        IProductRepository repository = ServiceLocator.Resolve<IProductRepository>();

        return repository.Fetch(id);                                    
    }       
}

You might even make this work for all of your entities if you can use a base class or interface that provides the Id of the class.

You'll have to set this up in Global.asax:

ModelBinders.Binders.Add(typeof(Product), new ProductBinder());

and then you can do this:

public ActionResult Save([Bind] Product product)
{
    ....

    _repository.Save(product);
}
Ben Scheirman
This is what I was doing originally, as it makes sense to me to do it here. But having read in several places that the ModelBinder shouldn't query the database it's made me think twice.
Luke Smith
It is a bit smelly, but if it makes you feel any better, [ARDataBind] from the Castle project does exactly this.
Ben Scheirman
Hitting the database is the ModeBinder isn't a great idea. Also, the binder executes BEFORE ActionFilters, so keep that in mind when accessing the database from binders
Chris Canal
Yeah, that's a good point, you might access the database before a different action filter halts the request...
Ben Scheirman
A: 

You don't actually have to hit the database. Simply setting the Id of the objects will be enough to set the relationship up, but watch your cascades. Make sure your cascde settings won't update the related object as it will clear the values.

Chris Canal
That's really dangerous advice. You should always select before update, because there are many areas where you might not be updating all the records in the database, which would end up overwriting with blank values.
Ben Scheirman
Very true, was a stupid answer because I didn't read the question properly!
Chris Canal