How to insert a model with a Foreign Key in Entity Framework in MVC 3?

asp.net-mvc-3 entity-framework foreign-key-relationship insert

Question

I have a product model that looks like this and has a foreign key to a record of a product category.

public class ProductModel : IProductModel
{
    public int ProductID { get; set; }
    public string ProductName { get; set; }
    public string ProductDescription { get; set; }
    public string ProductImagePath { get; set; }
    public decimal? PricePerMonth { get; set; }
    public bool ProductActive { get; set; }
    public ProductCategory ProductCategory { get; set; }
}

For some reason, whenever a new Product is produced, a new ProductCategory is likewise formed, but this time, all of its database values are set to NULL, with the exception of the ProductCategoryID column, which is generated automatically.

I have a form with a drop list filled with ProductCategories in my Create view. When a form is submitted, the Create(Product product) Action is invoked, which results in the creation of the new Product. Now that the new ProductCategory has been formed, I don't want it to happen.

This is how my Create(Product product) Action looks:

    //
    // POST: /Product/Create
    [HttpPost]
    public ActionResult Create(Product product)
    {
        int productCategoryID = product.ProductCategory.ProductCategoryID;
        product.ProductCategoryID = productCategoryID;
        _productService.InsertProduct(product);
    }

... and these are the InsertProduct methods I use.

    public void InsertProduct(Product product)
    {
        Db.Products.AddObject(product);
        Save();
    }

Why does this code need the creation of a new ProductCategory each time?

EDIT: Added definition of a table in response to a message that requested further details:

TABLE Product (
ProductID INT PRIMARY KEY IDENTITY(1,1),
ProductCategoryID INT, -- FK to ProductCategory 
ProductName NVARCHAR(255),
ProductDescription NVARCHAR(MAX),
ProductImagePath NVARCHAR(1024),
PricePerMonth DECIMAL(7,2), -- ex 11111.11
ProductActive BIT NOT NULL DEFAULT(1)
)

TABLE ProductCategory (
ProductCategoryID INT PRIMARY KEY IDENTITY(1,1),
ProductCategoryName NVARCHAR(255),
ProductCategoryDescription NVARCHAR(MAX),
ProductCategoryActive BIT NOT NULL DEFAULT(1)
)

EDIT 2:

I still don't know why precisely, but by making the following adjustments to my Create(Product product) Action, I was able to get around the issue:

var productCategoryID = product.ProductCategory.ProductCategoryID;
product.ProductCategoryID = productCategoryID;
var newProduct = new Product
        {
            ProductName = product.ProductName,
            ProductDescription = product.ProductDescription,
            PricePerMonth = product.PricePerMonth,
            ProductImagePath = product.ProductImagePath,
            ProductCategoryID = productCategoryID
        };

_productService.InsertProduct(newProduct);

EDIT 3:

I'm still trying to comprehend everything, but I've found that the ProductCategory member of the Product class is somehow related to this issue. I believe ModelBinding is automatically adding a new ProductCategory to that property each time the form is submitted. Setting Product.ProductCategory = null explicitly allows me to consistently avoid the issue I've been seeing here;

1
0
8/14/2011 7:22:19 AM

Accepted Answer

I was interested in your issue, so I sought advice from a renowned author and MVC trainer and discovered the following. Key ideas may be found in a an MSDN blog entry.

The Create action's model is bound to the view's form data using the MVC model binding procedure. It performs the model binding and locates the necessary values in the form data to bind to the Product and ProductCategory objects.

Because you built the Product and ProductCategory objects independently, outside of the EF context, as opposed to creating the entities from the EF context, the issue in your situation is that the Entity Framework is not aware of the objects. Although you are using EF 4.1 with the DbContext in your situation, this would also apply to EF 4.0 with the ObjectContext using other techniques.

Since you haven't yet added it, your new Product object doesn't exist in your EF context or database, but your ProductCategory does. In order to use an existing ProductCategory from your EF entities, you must map the ProductCategory object that hangs off your new Product object. To achieve so, you may either assign an existing ProductCategory to your product or locate it by seeking it up using the main key (ProductCategoryId). You may "connect" your free-floating ProductCategory object to your entities or use ProductCategory instead. When you give it your ProductCategory object, it basically does a primary key search on the entity, which is the same as locating an existing entity. Both methods simply take a ProductCategory object that already exists, notify EF that your object is an entity, and set the entity's "state" to "Unchanged," so when you use one of these methods, the entity'scontext.SaveChanges() Because it doesn't look at the ProductCategory object, it discovers no changes to report to the underlying database.

To find an existing ProductCategory entity that is already attached to EF, to attach the detached ProductCategory object, or to set the product.ProductCategoryId and null out the product.ProductCategory reference so that EF will find the ProductCategory for you, you must do one of the following things before adding your Product to the EF context: A. You went with Option C.

(Find an existing, connected object) Choice A

public ActionResult Create(Product product)
{
    // find an attached entity based on primary key
    var productCat = 
        Db.ProductCategories.Find(product.ProductCategory.ProductCategoryId); 
    product.ProductCategoryId = productCat.ProductCategoryId;
    product.ProductCategory = productCat;
    _productService.InsertProduct(product);
}

(Attach a disconnected object) Choice B

public ActionResult Create(Product product)
{
    // attach an entity - EF will find the entity to attach using the entity's primary key
    Db.ProductCategories.Attach(product.ProductCategory); 
    product.ProductCategoryId = product.ProductCategory.ProductCategoryId;
    _productService.InsertProduct(product);
}

(Null out the entity reference) Choice C

public ActionResult Create(Product product)
{
    product.ProductCategoryId = product.ProductCategory.ProductCategoryId;
    product.ProductCategory = null;
    _productService.InsertProduct(product);
}
4
8/17/2011 4:21:52 AM

Popular Answer

@campbelt: Uncertain about the cause. This is the method I used. Instead of passing the view my domain object, I passed via a view model namedEditGrantApplicationViewModel . I have a grant application form in my situation. The user may choose a bank on this form from a dropdown menu. My partial view model, which includes the information for the bank dropdown, is shown below.

public class EditGrantApplicationViewModel
{
   // Other properties

   public int BankId { get; set; }
   public List<Bank> Banks { get; set; }
}

This is how this dropdown appears in my view:

<td valign="top"><label>Bank:</label> <span class="red">*</span></td>
<td>
   @Html.DropDownListFor(x => x.BankId, new SelectList(Model.Banks, "Id", "Name", Model.BankId), "-- Select --")<br>
   @Html.ValidationMessageFor(x => x.BankId)
</td>

My create action strategy is:

public ActionResult Create(EditGrantApplicationViewModel viewModel)
{
   // Check input parameter

   if (!ModelState.IsValid)
   {
      viewModel.Banks = bankService.GetAll();

      return View("Create", viewModel);
   }

   // Map from view model to domain object
   GrantApplication grantApplication = (GrantApplication)grantApplicationMapper.Map(viewModel, typeof(EditGrantApplicationViewModel), typeof(GrantApplication));

   // Insert to database
   grantApplicationService.Insert(grantApplication);

   return RedirectToRoute(Url.GrantApplicationIndex());
}

GrantApplication partial object:

public class GrantApplication
{
   public int Id { get; set; }

   public int BankId { get; set; }
}

The mapping will add this information to the column in my GrantApplication table and fill the BankId field in my GrantApplication object, which was chosen from the dropdown.



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow