ASP.NET MVC,实体框架,一对多和多对多插入

拍摄两种新技术 - ASP.NET MVC和实体框架 - 同时势必提供一些"interesting"时刻。通过EF获取数据到目前为止非常简单。但是,当它到建立我网站的后端时,我有一些乐趣尝试管理一对多和多对多关系的插入。

如果您到目前为止还没有保持留下,我的网站非常简单,具有相当简单的数据模型。我有一个属于一个articletype的文章,可以属于许多类别。数据模型与以前仍然相同:

当我添加一个新文章时,我需要选择一个Articletype(我的类型是文章,常见问题,章程表等)并应用一个或多个类别。我会将ASP.NET MVC应用于这个,也可能将实体框架添加为新类别。

我已经向网站添加了一个AdminController,该网站将容纳涉及各种与管理相关任务的互动管理。目前,它只有三种方法:

using System.Web.Mvc;
using MikesDotnetting.Models;

名称 space MikesDotnetting.Controllers
{
  public class AdminController : Controller
  {
    private IAdminRepository repository;

    public AdminController() : this(new AdminRepository()){ }
    

    public AdminController(IAdminRepository rep)
    {
      repository = rep;
    }


    public ActionResult 指数()
    {
      返回 看法();
    }


    public ActionResult AddArticle()
    {
      ViewData["ArticleTypes"] = repository.GetArticleTypes();
      ViewData["类别"] = repository.GetCategoryList();
      返回 看法();
    }


    public ActionResult AddArticle(Article article)
    {


      返回 看法("指数");
    }
  }
}

索引方法是首先调用的方法,并呈现默认视图,该视图将包含与构建迷你内容管理系统的各种页面的链接。 adadlice()方法有两个版本。第一个没有参数,并作为单击索引视图中的添加文章链接的结果调用。它将提出进入新文章的表格。 Tewo项目将添加到ViewData字典中 - 一系列articletypes和一系列类别。这些将用于允许我为文章选择Articletype并将类别应用于它。

我向adadicle视图显示所有代码,因为有几个 gotchas. 我在与之合作时遇到过:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Admin.Master" 
                                        Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="服务器">
    <title>AddArticle</title>
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="服务器">

    <h2>AddArticle</h2>
    <% using (Html.BeginForm()){ %>
    <table>
    <tr>
      <td>Headline</td>
      <td><%= Html.TextBox("Headline", null, new { style = "width:500px" }) %></td>
    </tr>
    <tr>
      <td>Abstract</td>
      <td><%= Html.TextArea("Abstract", new { cols = "60", rows="7" }) %></td>
    </tr>
    <tr>
      <td>MainText</td>
      <td><%= Html.TextArea("MainText", new { cols = "60", rows="15" }) %></td>
    </tr>   
    <tr>
      <td>Article Type</td>
      <td><%=Html.DropDownList(
      "ArticleTypeID", 
      new SelectList(
        (IEnumerable<ArticleType>)ViewData["ArticleTypes"],
        "ArticleTypeID", 
        "ArticleTypeName" 
        ))%></td>
    </tr> 
    <tr>
      <td>Article Categories</td>
      <td><% foreach (var item in (IEnumerable<Category>)ViewData["类别"])
             { %>
      <input type="checkbox" 名称 ="CategoryID" id="CategoryID" value="<%=item.CategoryID %>" />
      <%= item.CategoryName %><br />
      <% } %></td>
    </tr>     
    <tr>
      <td></td>
      <td><input type="submit" 名称 ="action" id="action" value="Submit" /></td>
    </tr>
    </table>
    <% } %>
</asp:Content>


Gotcha 1 - 在为管理区域添加主页页面时出现。现在是非常明显的,我觉得一个正确的Plonker甚至承认这一点,但是当我去添加新项目时,我选择了母版页。我应该选择MVC视图母版页。结果在重新排列的ContentPlaceHolvers和删除runat ="server"标签。这是几天后,我发现了MVC适当的选择。多年的网络形式习惯匍匐,所以可能是你需要观看的东西......

您可能会注意到用于在视图中设置DropDownList的代码卷。还有另一种方法来做到这一点,这就是改变AdminController addarticle()方法,该方法将ViewData的类别获取到此(用于显示目的的行):

public ActionResult AddArticle()
{
  ViewData["ArticleTypes"] = new SelectList(
                                    repository.GetArticleTypes(), 
                                    "ArticleTypeID", 
                                    "ArticleTypeName"
                                    );
  ViewData["类别"] = repository.GetCategoryList();
  返回 看法();
}

然后把它放在视野中:

<tr>
  <td>Article Type</td>
  <td><%=Html.DropDownList(
         "ArticleTypeID",
         (IEnumerable<SelectListItem>)ViewData["ArticleTypes"]
        )%></td>
</tr> 

它会导致同样的事情。

Gotcha 2. 用复选框出现。 HTMLHelper没有复选框扩展名。除了HTML.CheckBox()扩展方法外,这应该没有大量的交易,除了生成具有与可见复选框输入相同名称和ID的隐藏字段的冗余副产品。谷歌曲后,我发现在预览5中存在一个复选辅助者,但被删除。足够公平 - 我可以写自己。但是,隐藏领域的点是什么?显然,他们就在那里 可以检查是否有值"checked"或者不是或建立复选框是否实际呈现给页面. "What the smeg?"正如WorldSpawn所说的那样。为什么会这样做"not"得到渲染?我撞了我的大脑,仍然不能想出任何明智的东西来解释那里的思想。也许有人可以启发我? MVC是否如此不稳定,即我的表单字段可能只是决定不出现?无论如何,Web前表单,普通Web开发涉及当多个输入具有相同时,在表单集合中检查逗号分隔的字符串 名称 属性。这正是我想做的(就像他的帖子中的Worldspawn)。写自己的HTMLHELPER扩展方法或只是诉诸经典ASP样式HTML和Response.Write()?在这种情况下,我选择了后者。

继续前进,我现在有我的添加文章表单所有准备好等待第一篇要添加的文章。现在,我需要OT修改Excincontroller上的超载孤立动作以使用表单输入,并在adminRepository中调用方法,该方法将持续到我的数据库中的新文章。此时,该方法将文章对象作为参数。但是,我在物品对象上有两个属性,这些属性是表示数据库中的关系的集合,用于articletypes和类别。我需要拍摄收集属性的值并单独处理它们。所以addarticle()行动修正如下:

[AcceptVerbs("POST")]
public ActionResult AddArticle([Bind(Exclude = "ArticleID")] Article article)
{
  var articleTypeId = Request.Form["ArticleTypeID"];
  var categoryId = Request["CategoryID"];

  repository.AddArticle(article, articleTypeId, categoryId);

  返回 看法();
}

由于表单发布到控制器操作,因此应用了适当的Accessverbs属性。从那里,从CALLDOWN和来自复选框中的类别中的物流值将从Request.form集合中提取,并将文章对象与存储库的addlyicle方法一起提取。然而, Gotcha 3. 这里被发现了。我的文章包含文本中的HTML标记。提交其中一个时,应用程序抛出异常: 检测到潜在的危险请求。

我在错误消息的描述部分尝试了建议:"您可以通过在页面指令或配置部分中设置validateRequest = false来禁用请求验证"。我添加了validaterequest ="false"两者都既不到视图和主页,但既没有阻止异常。查看web.config文件的视图,我发现validateRequest ="false"设置在页面部分内。但是,我还在Web.config中发现了一个评论,如下所示:

<!--
    Enabling request validation in view pages would cause validation to occur
    after the input has already been processed by the controller. By default
    MVC performs request validation before a controller processes the input.
    To change this behavior apply the ValidateInputAttribute to a
    controller or action.
-->

因此,答案是将另一个属性添加到控制器操作:

[ValidateInput(错误的)]
[AcceptVerbs("POST")]
public ActionResult AddArticle([Bind(Exclude = "ArticleID")] Article article)
{
  var articleTypeId = Request.Form["ArticleTypeID"];
  var categoryId = Request["CategoryID"];

  repository.AddArticle(article, articleTypeId, categoryId);

  返回 看法();
}

但这不是故事的结尾。如果您返回与操作关联的视图,似乎只需要添加此属性。如果我改变了actionResult 返回 View("Index"), 这 validateInput() 不再需要属性。

现在我们将查看实际处理新文章的adminRepository方法:

public void AddArticle(Article article, string articleTypeId, string categoryId)
{
  int id = int.Parse(articleTypeId);
  ArticleType at = de.ArticleTypeSet.Where(a => a.ArticleTypeID == id).First();
  article.ArticleTypes = at;
  article.DateCreated = DateTime.Now;
  article.PostedBy = "mikesdotnetting.";

  var catids = categoryId.Split(',');
  foreach(var catid in catids)
  {
    id = int.Parse(catid);
    Category category = de.CategorySet.Where(ct => ct.CategoryID == id).First();
    article.Categories.Add(category);
  }

  de.AddToArticleSet(article);
  de.SaveChanges();
}

通过纯粹的ADO.NET代码,管理一对多和多对多的关系很容易。对于一对多关系,您只需获取Articletype的主键并将其添加到文章表中作为外键,因此将正确的Articletype与文章相关联。在实体框架的v1.0中,事情并不那么简单 - 虽然下一个释放改变了这一点。对于多对多的关系,您只需迭代作为表单集合的一部分提交的类别,并将其新获得的物品ID插入到坐在文章和类别之间的HTE桥接表中。使用EF,您需要获取完整的Articletype对象,并将其添加到文章对象的Articletype Collect属性。与类别相同。所以,我拍摄了在FormCollection中发布的字符串,它代表ArticletypeID并将其转换为int。然后,我通过符合ID的EF获得了Articlyype对象,并将该对象添加到文章.Articletypes属性。

类别集合以相同的方式处理。首先,使用String.split()方法将逗号分隔的字符串转换为数组,然后在LINQ中使用之前,每个单独的数组元素将转换为int类型"look-up"要将完整的类别对象添加到文章的类别中。

Gotcha 4: 我之前曾经触动过,但数据库内的多对多关系通常由桥接表处理。在我的情况下,文章和类别表通过ArticleCategories表加入,由两个字段组成:物质和类别。这些都是外键,链接回相应父表的主键。该链接表未显示EDMX生成(上图)的可视表示中。但是,刚刚运行代码将导致例外:

"无法更新Entityset'ArticleCategories',因为它有一个决定性和没有<InsertFunction>元素存在于<修改功能贴图>元素支持当前操作。"

当您在设计视图中查看EDMX时,您还可能会看到以下警告:

"表/查看''ArticleCategories'没有定义主键。该密钥已被推断,该定义是作为只读表/视图创建的。"

这基本上意味着实体框架无法确保在表中创建重复条目,因为表中没有禁用措施以防止它们。如果将新列添加到桥接表中作为主键,则模型图将爆炸进入更多次的关系。因此,您需要使用两个现有列创建一个复合密钥。这通过SQL Server Management Studio轻松完成 - 只需在设计模式下打开表,然后按住Ctrl键,选择两个列,然后单击顶部菜单中的Golden键图标。任务完成。添加了一个主要密钥,它适用不重复的约束,并且EF再次开心。您需要重建模型,您甚至可能会关闭vs以防止警告消息(发生在我身上)。

一旦固定,一切都按预期工作。

您可能想知道为什么我没有验证。虽然您通常应该验证所有用户输入,在这种情况下,我将成为唯一使用添加物品功能的人。没有人在我的网站上添加文章,除了我。因此,我确切地知道强制性的字段,以及数据类型应该是什么。简而言之,在这种情况下我不需要它。