4

I have problem with model binding in my ASP.NET MVC 2 RC application that uses NHibernate for data access. We are trying to build the application in a Ruby on Rails way and have a very simple architecture where the domain entities are used all the way from the database to the view.

The application has a couple of domain entities which can be illustrated by the following two classes:

public class Product {
    ...

    public Category Category { get; set; }      
}

public class Category {
    public int Id { get; set; }
    public string Name { get; set; }
}

In the view that renders the edit form has the following statement to display a dropdown list:

<%= Html.DropDownListFor(model => model.Category.Id, 
       new SelectList(ViewData["categories"] as IList<Category>, "Id", "Name"), 
       "-- Select Category --" ) %>

Please disregard the use of "non-typed" view data to hold the category collection.

The action method that receives the form post is similar to to the following. Note that the TransactionFilter attribute adds NHibernate transaction handling and commits the transaction if no exceptions occur and validation succeeds.

[HttpPost]
[TransactionFilter]
public ActionResult Edit(int id, FormCollection collection) {
    var product = _repository.Load(id);

    // Update the product except the Id
    UpdateModel(product, null, null, new[] {"Id"}, collection);

    if (ModelState.IsValid) {
      return RedirectToAction("Details", new {id});
    }
    return View(product);
}

My issue is that the product.Category.Id is set with the value selected in the form, e.g. Category.Id = "2". Using the default model binder results in the following type of NHibernate exception:

identifier of an instance of Name.Space.Entities.Category was altered from 4 to 2

That makes a lot of sense since the product already has a category assigned and only the primary key of that existing category is being changed. Another category instance should have been assigned instead.

I guess a custom ModelBinder can be created to handle this issue but is there a simpler way to make this work? Can (and should) the domain entities be modified to handle this?

HakonB
  • 6,977
  • 1
  • 26
  • 27
  • Have you found a solution to this problem? I am having exactly this same issue. – nabeelfarid Jun 07 '11 at 22:59
  • No, I never found a good solution (however, we did not spend a lot of time looking for it). We handled the issue explicitly outside the binder. – HakonB Jun 08 '11 at 08:04
  • Could you please give me some hint how you actually handling this outside the binder. Are you not using TryUpdateModel/UpdateModel at all ? – nabeelfarid Jun 08 '11 at 09:23
  • It was a long time ago but I think made a quick work-around by using TryUpdateModel but excluding the association and then manually updating that property afterwords. I've added a simple answer that illustrates the way we did it – HakonB Jun 08 '11 at 12:30

3 Answers3

2

I solved a similar problem with combo boxes on my edit page by changing the following line

@Html.DropDownListFor(x => x.Employee.Id, new SelectList(ViewBag.Employees, "Id", "DisplayName"))

by

@Html.DropDownListFor(x => x.Employee, new SelectList(ViewBag.Employees, "Id", "DisplayName"))

So I removed the '.Id' like Bryan suggested. Before the change, the model only contained the Id of the Employee and after the change, the Binder also loaded all the details of the employee into the model.

Björn Boxstart
  • 1,098
  • 1
  • 12
  • 25
0

I've used similar techniques with Linq to SQL classes before with no problems. I don't think you'd need a custom ModelBinder for this. UpdateModel should be updating the Product class you are passing into it - not the Category sub-class attached to it. Check the html generated by the DropDownListFor helper. What is the name of the element? It should be the name of the foreign-key field in your Products table (e.g. "CategoryID" or "Product.CategoryID" not "Category.Id"). If it's "Category.Id" - try changing the first parameter of the DropDownListFor to either "model => model.Category" or "model => model.CategoryID" (or whatever the foreign key field is). This should cause UpdateModel to only update the foreign-key field in the Product class and not the Category class ID.

Bryan
  • 535
  • 1
  • 4
  • 8
  • 1
    The foreign key is not available as the relationship is handled entirely by NHibernate. I ended up just handling the category case manually and not creating a custom model binder – HakonB Mar 11 '10 at 18:55
0

The solution we chose at the time was something similar to this:

TryUpdateModel(product, null, null, new[] {"Category"}, collection);
int categoryId;
if (int.TryParse(collection["Category.Id"], NumberStyles.Integer, CultureInfo.InvariantCulture, out categoryId) && categoryId > 0) {
    product.Category = _categoryRepository.Load(categoryId);
}
else {
    product.Category = null;
}

We simply tell the model binder to exclude the association property and handle that manually. Not pretty but worked at the time....

HakonB
  • 6,977
  • 1
  • 26
  • 27