多对多关系基本示例(MVC3)

时间:2011-05-01 19:09:12

标签: c# asp.net-mvc-3 entity-framework-4 scaffolding sql-server-ce-4

我有一个MVC3 C#项目,我有一个FoodItem和FoodItemCategory模型。这两个模型如下所示:

public class FoodItem
{
    public int ID { get; set; }
    [Required]
    public string Name { get; set; }
    public string Description { get; set; }
    public virtual ICollection<FoodItemCategory> Categories { get; set; }
    public DateTime CreateDate { get; set; }
}

public class FoodItemCategory {
    public int ID { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public virtual ICollection<FoodItem> FoodItems { get; set; }
    public DateTime CreateDate { get; set; }
} 

我有一个最初从Scaffolder生成的_CreateOrEdit.cshtml视图,我将其修改为包含所有类别并检查食品所属的框。食品可以具有许多或所有类别。该视图如下所示:

@model StackOverFlowIssue.Models.FoodItem
<div class="editor-label">
    @Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
    @Html.EditorFor(model => model.Name)
    @Html.ValidationMessageFor(model => model.Name)
</div>
<div class="editor-label">
    @Html.LabelFor(model => model.Description)
</div>
<div class="editor-field">
    @Html.EditorFor(model => model.Description)
    @Html.ValidationMessageFor(model => model.Description)
</div>
<div class="editor-label">
    @Html.LabelFor(model => model.Categories)
</div>
<div class="editor-field">
    @foreach (var FoodItemCategory in (IEnumerable<StackOverFlowIssue.Models.FoodItemCategory>)ViewBag.Categories){
        <input type="checkbox" name="FoodItemCategoryId" value="@FoodItemCategory.ID" 
        @foreach(var c in Model.Categories){
            if(c.ID == FoodItemCategory.ID){ 
                @String.Format("checked=\"checked\"")
            } 
        } 
        />
        @FoodItemCategory.Name 
        <br />
    } 
</div>
@Html.Hidden("CreateDate", @DateTime.Now)

正如您所看到的,我有一个嵌套循环,为每个类别创建一个复选框,当它创建每个类别时,我循环并检查我的模型的Categories属性中的特定类别。如果存在,我设置复选框的checked属性。如果您选中一个框并单击保存,则在控制器上的HttpPost操作上,我将执行以下操作:

    [HttpPost]
    public ActionResult Edit(FoodItem foodItem)
    {
        if (ModelState.IsValid)
        {
            var cList = Request["CategoryId"].Split(',');
            List<FoodItemCategory> categories = new List<FoodItemCategory>();

            foreach (var c in cList) {
                var ci = Convert.ToInt32(c);
                FoodItemCategory category = context.FoodItemCategories.Single(x => x.ID == ci);
                categories.Add(category);
            }

            context.Entry(foodItem).State = EntityState.Modified;
            restaurant.Categories = categories;
            context.SaveChanges();
            return RedirectToAction("Index");
        }
        return View(foodItem);
    }

我可以保存一次类别。如果我回到视图中,只需单击“保存”,我会收到以下错误:

  

无法将重复值插入唯一索引。 [表名=&gt; FoodItemCategoryFoodItems,约束名称= PK_ FoodItemCategoryFoodItems _00000000000000A8]   描述:执行当前Web请求期间发生未处理的异常。请>查看堆栈跟踪,以获取有关错误及其在代码中的起源位置的更多信息。

     

异常详细信息:System.Data.SqlServerCe.SqlCeException:无法将重复值插入到&gt;唯一索引中。 [表名= FoodItemCategoryFoodItems,约束名称=&gt; PK_ FoodItemCategoryFoodItems _00000000000000A8]

     

来源错误:

     

第97行:context.Entry(foodItem).State = EntityState.Modified;   第98行:foodItem.Categories =类别;   第99行:context.SaveChanges();   第100行:返回RedirectToAction(“Index”);   第101行:}

不确定是否重要,但我使用的是SQLServer Compact Edition 4.我是否正确地采用了这种方式?对于这样的事情,正常的编码实践是什么?我知道同样的情况每天都会发生,因为在博客等许多情况下使用相同的关系模型

2 个答案:

答案 0 :(得分:4)

尝试这样的事情(未经测试):

[HttpPost]
public ActionResult Edit(FoodItem foodItem)
{
    if (ModelState.IsValid)
    {
        int id = foodItem.Id;
        // Load food item with related categories first
        var item = context.FoodItems
                          .Include(f => f.Categories)
                          .Single(f => f.Id == id);

        // Process changed scalar values
        context.Entry(item).CurrentValues.SetValues(foodItem);

        // Brute force processing of relations
        // This can be optimized - instead of deleting all and adding all again
        // you can manually compare which relations already exists, add new and
        // remove non existing but let's make that as a homework
        item.Categories.Clear();

        var cList = Request["CategoryId"].Split(',');

        foreach (var c in cList) 
        {
            var ci = Convert.ToInt32(c);
            // Use find - if category was already loaded in the first query, it will
            // be reused without additional query to DB
            var category = context.Categories.Find(ci);
            // Now add category to attached food item to create new relation
            item.Categories.Add(category);
        }

        context.SaveChanges();
        return RedirectToAction("Index");
    }

    return View(foodItem);
}

这看起来效率很低但是因为你正在处理多对多关系,在视图中可以添加或删除关系,这是唯一的方法。原因是:

  • 您必须说EF添加哪些关系以及哪些关系被删除
  • 如果您只是添加所有相关类别,那么您将再次插入关系
  • 您不能说因为您不传输有关未选中类别的信息而删除了哪些关系

有关分离对象图和处理关系的更多信息是described here。它与ObjectContext API有关,但DbContext API只是包装,因此仍然存在相同的限制。

答案 1 :(得分:0)

除了Ladislav的答案,你可以使用http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx删除Request []。split()部分,这使得:

[HttpPost]
public ActionResult Edit(FoodItem foodItem, ICollection<int> CategoryId)
{
    if (ModelState.IsValid)
    {
        int id = foodItem.Id;
        // Load food item with related categories first
        var item = context.FoodItems
                          .Include(f => f.Categories)
                          .Single(f => f.Id == id);

        // Process changed scalar values
        context.Entry(item).CurrentValues.SetValues(foodItem);

        // Brute force processing of relations
        // This can be optimized - instead of deleting all and adding all again
        // you can manually compare which relations already exists, add new and
        // remove non existing but let's make that as a homework
        item.Categories.Clear();

        foreach (var id in CategoryID) 
        {
            // Use find - if category was already loaded in the first query, it will
            // be reused without additional query to DB
            var category = context.Categories.Find(id);
            // Now add category to attached food item to create new relation
            item.Categories.Add(category);
        }

        context.SaveChanges();
        return RedirectToAction("Index");
    }

    return View(foodItem);
}