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

发布于 2024-11-04 06:18:45 字数 3604 浏览 0 评论 0原文

我有一个 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; }
} 

我有一个最初从脚手架生成的 _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)

正如您所看到的,我有一个嵌套循环,它为每个类别创建一个复选框,在创建每个类别时,我会循环遍历并检查模型的“类别”属性中的特定类别。如果存在,我设置复选框的选中属性。如果您选中一个框并单击“保存”,在控制器上的 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);
    }

我能够保存类别一次。如果我返回视图,然后单击“保存”,我会收到以下错误:

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

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

来源错误:

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

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

I have a MVC3 C# project that I have a model of FoodItem and FoodItemCategory. The two models are shown as follows:

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; }
} 

I have a _CreateOrEdit.cshtml view that was initially generated from the Scaffolder and I modified it to include all of the Categories and check the box that the food item belongs to. A food item could have many or all of the categories. The view looks like the following:

@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)

As you can see, I have a nested loop that creates a checkbox for each category, and while it is creating each category, I loop through and check for that particular category in the Categories property of my model. If it exists, I set the checked property of the checkbox. If you check a box and click save, on the HttpPost action on the controller, I am performing the following:

    [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);
    }

I am able to save the categories one time. If I go back into the view, and just click save, I receive the following error:

A duplicate value cannot be inserted into a unique index. [ Table name = >FoodItemCategoryFoodItems,Constraint name = PK_FoodItemCategoryFoodItems_00000000000000A8 ]
Description: An unhandled exception occurred during the execution of the current web request. Please >review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.Data.SqlServerCe.SqlCeException: A duplicate value cannot be inserted into >a unique index. [ Table name = FoodItemCategoryFoodItems,Constraint name = >PK_FoodItemCategoryFoodItems_00000000000000A8 ]

Source Error:

Line 97: context.Entry(foodItem).State = EntityState.Modified;
Line 98: foodItem.Categories = categories;
Line 99: context.SaveChanges();
Line 100: return RedirectToAction("Index");
Line 101: }

Not sure if it matters but I am using SQLServer Compact Edition 4. Am I going about this the right way? What is the normal coding practice for something like this? I know this same situation occurs daily since this same relationship model is used in many situations like blogs, etc.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

避讳 2024-11-11 06:18:45

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

[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 添加了哪些关系,删除了哪些关系
  • 如果您只是添加所有相关类别,您将再次插入关系
  • 您不能说 EF 删除了哪些关系,因为您不传输有关未检查类别的信息

更多关于分离对象图和处理关系此处描述< /a>.它是关于 ObjectContext API 的,但 DbContext API 只是它的包装器,因此相同的限制仍然存在。

Try something like this (untested):

[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);
}

This can look pretty inefficient but because you are dealing with many-to-many relation where relations can be added or removed in the view it is the only way to do that. The reasons are:

  • You must say EF which relations are added and which are deleted
  • If you simply add all related categories you are inserting relations again
  • You can't say EF which relations are deleted because you don't transfer information about unchecked categories

More about detached object graphs and processing relations is described here. It is about ObjectContext API but DbContext API is just wrapper around that so the same limitations still exist.

清欢 2024-11-11 06:18:45

除了Ladislav的答案之外,您还可以使用 http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx 这使得:

[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);
}

In addition to Ladislav's answer, you can get rid of the Request[].split() part by using http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx which makes:

[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);
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文