在上一篇文章中,我们使用Entity Framework 和SQL Server LocalDB创建了一个MVC应用程序,并使用它来存储和显示数据。在这篇文章中,你将对由 MVC框架自己主动创建的CRUD(create, read, update, delete)代码进行改动。

注意:通常我们在控制器和数据訪问层之间创建一个抽象层来实现仓储模式。为了将注意力聚焦在怎样使用实体框架上。这里暂没有使用仓储模式。

在本篇文章中,要创建的web页面:




1.创建一个Details页面

由框架代码生成的Students Index页面暂没有考虑Enrollments属性,由于该属性是一个集合。在Details页面中,我们将在HTML表格中显示集合中的内容。

打开 Controllers\StudentController.cs。能够看到相应Details视图的Details方法使用Find方法来检索单个学生实体:

public ActionResult Details(int?

id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } Student student = db.Students.Find(id); if (student == null) { return HttpNotFound(); } return View(student); }

Details方法的id參数来自Index页面中Details链接,称为路由数据(route data)。

路由数据是指在路由表中指定,通过URL传递,由模型绑定器接收的数据。

例如以下所看到的,默认路由指定了controller, action和 id

 routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
在以下的 URL中。默认路由将Instructor 映射为controller, Index映射为action, 1 映射为 id

http://localhost:1230/Instructor/Index/1?courseID=2021
"?

courseID=2021" 是查询字符串, 假设你将id作为查询字符串,模型绑定器也能正常解析

http://localhost:1230/Instructor/Index?id=1&CourseID=2021
在Razor视图中,由ActionLink语句来创建URL,如以下的代码中id參数匹配默认路由。所以id被作为进路由数据
 @Html.ActionLink("Select", "Index", new { id = item.PersonID  })
以下的代码中courseID參数 不匹配默认路由,所以courseID被作为查询字符串
@Html.ActionLink("Select", "Index", new { courseID = item.CourseID }) 


打开Views\Student\Details.cshtml,每一个字段都使用DisplayFor帮助器来显示数据,如以下的代码所看到的:

<dt>
    @Html.DisplayNameFor(model => model.LastName)
</dt>
<dd>
    @Html.DisplayFor(model => model.LastName)
</dd>

在EnrollmentData字段之后。</dl>标签之前,加入以下的代码

        <dt>
            @Html.DisplayNameFor(model => model.EnrollmentDate)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.EnrollmentDate)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Enrollments)
        </dt>
        <dd>
            <table class="table">
                <tr>
                    <th>Course Title</th>
                    <th>Grade</th>
                </tr>
                @foreach (var item in Model.Enrollments)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Course.Title)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Grade)
                        </td>
                    </tr>
                }
            </table>
        </dd>
    </dl>
</div>
<p>
    @Html.ActionLink("Edit", "Edit", new { id = Model.ID }) |
    @Html.ActionLink("Back to List", "Index")
</p>

假设代码缩进不对,能够使用Ctrl-K-D快捷键来纠正它。 

上面的代码遍历Enrollments导航属性中的实体,对于每个Enrollment实体。显示出Course Title 和Grade。Course Title是从Enrollments实体中的Course导航属性中的Course实体中获取的,全部这些数据豆是在须要时自己主动从数据库检索的。(换句话说,这里使用的是延迟载入。你没有为Courses导航属性指定预先载入。所以在同一次查询中,仅仅检索了Students数据而没有检索enrollments数据。相反。在第一次试图訪问Enrollments导航属性时,会创建一个新的查询并发送到数据库。

执行项目,选择Students 选项卡并点击名为Alexander Carson的Details 链接。(假设你按Ctrl+F5,直接打开Details.cshtml,会得到HTTP 400错误,由于Visual Studio会直接打开Details页面却没有指定不论什么一个studen,路由匹配错误导致程序出错。在这样的情况下。你仅仅须要从URL中删除Student/Details然后重试)

能够看到所选学生的courses 和grades



2.更新Create 页面

打开Controllers\StudentController.cs,使用以下的代码替换HttpPost Create方法

[HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create([Bind(Include = "LastName,FirstMidName,EnrollmentDate")] Student student)
        {
            try
            {
                if (ModelState.IsValid)
                {
                    db.Students.Add(student);
                    db.SaveChanges();
                    return RedirectToAction("Index");
                }
            }
            catch (DataException)
            {
                //Log the error (uncomment dex variable name and add a line here to write a log.
                ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
            }
            return View(student);
        }

上面的代码将ASP.NET MVC模型绑定器创建的Student实体加入到Students 实体集并保存到数据库中。

(模型绑定器能够让你更easy的提交表单数据,能够将提交的表单值转换为CLR值并将它们作为參数传递给Controller中的方法。在本项目中,模型绑定器使用了表单集合中的属性值实例化了一个Student 实体)

这里删除了Bind 属性中的ID參数。由于ID是primary key。SQL Server在插入数据时会自己主动设置该值。

安全注意:ValidateAntiForgeryToken属性有助于防止跨站请求伪造(cross-site request forgery)攻击。可是须要在视图中设置对应的Html.AntiForgeryToken()语句。

Bind属性能够防止过份提交(over-posting)。举例来说,如果Student实体中包括一个Secret 字段,你不希望在Web页面中更新它

 public class Student
   {
      public int ID { get; set; }
      public string LastName { get; set; }
      public string FirstMidName { get; set; }
      public DateTime EnrollmentDate { get; set; }
      public string Secret { get; set; }

      public virtual ICollection<Enrollment> Enrollments { get; set; }
   }

即使在Web页面中没有Secret字段,黑客也能够通过工具比如Fiddler或者JavaScript 将表单数据包含Secret值提交到server。假设不使用Bind属性来限制模型绑器须要的字段。模型绑定器会将接收到的Secret值更新至数据库中。以下的截图是通过Fiddler工具来提交表单数据




OverPost值将会被成功的更新至数据库,这是你不希望看到的。

为了安全起见,最好使用Bind属性的Include參数,也能够使用Exclude參数排除那些你不想要更新的属性。

可是这里推荐使用Include,由于假设你在实体中加入了一个新的属性,Exclude并不会将这个新加入的属性排除在外。

还有一种替代方法是在模型绑定时使用视图模型,视图模型中仅仅包括你想要绑定的属性。

除了Bind属性。上面的代码中仅仅须要增加try-catch块,假设在保存更改时引发DataException异常,就会在页面中显示对应的错误信息。

DataException异常有时是由外部事件引发而不是由于程序错误,所以建议用户重试。记住在生产环境下,全部的应用程序错误都应该被记录下来。

Views\Student\Create.cshtml中的代码和Details.cshtml中的非常相似,除了DisplayFor被EditorFor和ValidationMessageFor帮助器替代

<div class="form-group">
    @Html.LabelFor(model => model.LastName, new { @class = "control-label col-md-2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.LastName)
        @Html.ValidationMessageFor(model => model.LastName)
    </div>
</div>

Create.cshtml也包括了@Html.AntiForgeryToken()方法以防止跨站请求伪造攻击。

执行项目,选择Students选项卡。并点击Create New

输入姓名和无效的日期,然后单击Create查看错误消息


默认情况下使用的是server端验证,以后会教大家通过加入属性来生成client验证,以下的代码展示了Create 方法中的模型验证检查

if (ModelState.IsValid)
{
    db.Students.Add(student);
    db.SaveChanges();
    return RedirectToAction("Index");
}

改动日期为一个有效的值,点击Create,能够看到新加入的Student信息


3.更新Edit HttpPost页面

在Controllers\StudentController.cs中,HttpGet Edit方法(没有使用HttpPost属性的那一个)和Details方法一样使用Find方法来检索所选择的Student实体。

使用以下的代码替换HttpPost Edit方法:

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Edit([Bind(Include = "ID,LastName,FirstMidName,EnrollmentDate")] Student student)
        {
            try
            {
                if (ModelState.IsValid)
                {
                    db.Entry(student).State = EntityState.Modified;
                    db.SaveChanges();
                    return RedirectToAction("Index");
                }
            }
            catch (DataException /* dex */)
            {
                //Log the error (uncomment dex variable name and add a line here to write a log.
                ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
            }
            return View(student);
        }

上面的代码类似于HttpPost Create方法。但不同的是这里在实体中设置了一个标志位来指明它已经被更改,而不是将由模型绑定器创建的实体加入到实体集。

当调用SaveChanges方法时,Modified标志会导致 Entity Framework创建SQL语句并更新数据库。数据库中该行的全部列都将被更新,包含那些用户并没有更改的,并忽略并发冲突。

实体状态、附加和SaveChanges方法

数据库上下文会一直跟踪内存中的实体是否与数据库中的行保持同步。并由此决定当调用SaveChanges方法时会发生什么,比如,当你调用Add方法加入实体时。该实体的状态会被设置为Added,然后当调用SaveChanges方法时。数据库上下文会生成一个SQL Insert命令。

一个实体可能处于下面状态之中的一个:

  • Added,数据库并不存在该实体。SaveChanges方法必须生成一个Insert语句。

  • Unchanged。对该实体,SaveChanges方法什么都不须要做,当从数据库中读取一个实体时,该实体就为这一状态。
  • Modified,某些或全部实体的属性值被更改,SaveChanges方法必须生成一个Update语句。
  • Deleted。

    实体已被标志为删除状态,SaveChanges方法必须生成一个Delete语句。

  • Detached。实体没有被数据库上下文跟踪。

在桌面应用程序中,状态变化一般是自己主动的,当你读取一个实体并更改它的一些属性值。该实体的状态会自己主动更改为Modified,然后当你调用SaveChanges方法时。Entity Framework 会生成一个SQL Update来更新数据库。

DbContext 在读取一个实体并将其呈现到页面上后就会被销毁。当HttpPost Edit方法被调用。此时会生成一个新的请求和DbContext 实例,所以你必须手动设置实体状态为Modified。然后当你调用SaveChanges方法时,Entity Framework 会更新数据库行的全部列,由于数据库上下文没有办法知道你究竟更改了哪些属性。

假设你希望SQL Update语句仅仅更新那些用户实际更改的字段,你能够先将原来的值以某种方法(比方隐藏字段)保存起来,这样在调用HttpPost Edit方法时就能够使用它们,然后你能够使用原来的值来创建一个Student实体,调用Attach方法。并使用新的值更新该实体,最后调用SaveChanges方法。

Views\Student\Edit.cshtml 中的HTML 和Razor代码与Create.cshtml中的非常类似。

执行项目。选择Students选项卡,点击当中一个学生的Edit链接


改动当中的值,点击Save。能够在Index页面中看到已经改动过的数据


4.更新Delete页面

在Controllers\StudentController.cs中,由模板生成的HttpGet Delete方法使用Find方法检索所选的Student实体。

然而,当调用SaveChanges方法失败时为了显示自己定义的错误信息。你须要向该方法和相相应的视图中加入一些功能。

就像update和create操作,delete操作也须要两个动作方法。用于响应Get请求的方法用来显示一个能够让用户<批准或取消delete操作的视图。假设用确认运行delete操作,此时会产生一条POST请求。并调用HttpPost Delete方法,该方法运行真正的delete操作。

在HttpPost Delete方法中加入try-catch块能够用来捕获数据库更新时可能出现的不论什么错误,假设出现了错误。则HttpPost Delete方法会调用HttpGet Delete方法。并向其传递一个參数指明发生了错误,然后HttpGet Delete会显示一个错误信息,并给用户一个取消或重试的机会。

使用以下的代码替换HttpGet Delete方法:

public ActionResult Delete(int? id, bool? saveChangesError=false)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    if (saveChangesError.GetValueOrDefault())
    {
        ViewBag.ErrorMessage = "Delete failed. Try again, and if the problem persists see your system administrator.";
    }
    Student student = db.Students.Find(id);
    if (student == null)
    {
        return HttpNotFound();
    }
    return View(student);
}

上面的代码接受一个可选择參数,指明该方法在保存更改出现错误后是否被调用。

当HttpGet Delete方法不是因为出现错误而被调用的话。该參数值为false。当HttpPost Delete出现了错误而调用HttpGet Delete方法时该參数为true并在对应的视图上显示错误信息。

使用以下的代码替换HttpPost Delete方法(名称为DeleteConfirmed的那个)。此方法用来运行真正的delete操作并捕获不论什么数据库更新错误

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(int id)
{
    try
    {
        Student student = db.Students.Find(id);
        db.Students.Remove(student);
        db.SaveChanges();
    }
    catch (DataException/* dex */)
    {
        //Log the error (uncomment dex variable name and add a line here to write a log.
        return RedirectToAction("Delete", new { id = id, saveChangesError = true });
    }
    return RedirectToAction("Index");
}

上面的代码从数据库中检索要删除的实体。然后调用Remove方法将实体的状态设置为Deleted,最后调用SaveChanges方法并生成一条SQL Delete命令。另外你也能够将方法名DeleteConfirmed改为Delete。框架代码将HttpPost Delete方法命名为DeleteConfirmed是为了为其设置一个独一无二的名称(CLR重载方法须要有不同的參数)。如今遵守MVC的约定。HttpPost和HttpGet delete方法使用了同样的名字,并为它们设置不同的參数。

假设你想提高高訪问量应用程序的性能,你要避免使用不必要的SQL查询。

使用以下的代码替换Find和Remove方法

Student studentToDelete = new Student() { ID = id };
db.Entry(studentToDelete).State = EntityState.Deleted;

上面的代码使用唯一的主键值实例化了一个学生实体并设置实体状态为Deleted。这便是Entity Framework为了删除一个实体所须要做的动作。

如前所述HttpGet Delete方法并不会运行数据删除操作,在一个Get请求响应中运行delete操作(运行不论什么edit操作、create操作或者其他对数据进行更改的操作)将带来安全风险。

在Views\Student\Delete.cshtml中加入错误信息

<h2>Delete</h2>
<p class="error">@ViewBag.ErrorMessage</p>
<h3>Are you sure you want to delete this?

</h3>

执行项目,点击Students选项卡,点击当中一个学生的Delete链接:


点击Delete。你会看到在Index页面中该学生已经被删除。

5.确保数据库连接适时关闭

要确保数据库连接被正确的关闭并释放所占用的资源,在你使用完数据库上下文时,必需要将其销毁。这就是为什么框架代码在StudentController.cs的最后部分提供了一个Dispose方法

protected override void Dispose(bool disposing)
{
    db.Dispose();
    base.Dispose(disposing);
}

Controller类实现了IDisposeable接口,所以上面的代码通过重写Dispose(bool)方法来显式的销毁数据库上下文实例。

6.处理事务

默认情况下,Entity Framework隐式的实现事务处理。当你对多行或者多个表进行更改后调用SaveChanges方法,Entity Framework会自己主动确保所有更改要么所有成功要么所有失败。假设已经做完一些更改后发生了一个错误。那么所有的更改包含已做完的都将自己主动回滚。


欢迎转载。请注明文章出处:http://blog.csdn.net/johnsonblog/article/details/38711659

博客搬家啦。我的小站:MVC5 Entity Framework学习(2):实现主要的CRUD功能

还大家一个健康的网络环境。从你我做起

项目源代码:https://github.com/johnsonz/MvcContosoUniversity

THE END


posted on 2017-08-14 08:49  lxjshuju  阅读(409)  评论(0编辑  收藏  举报