一、 学习MVC注意事项
1. 了解不同的项目类型
从ASP.NET 2.0开始,vs针对网站开发区分了两种项目类型,一种是'项目'(Website Project),另一种是'网址'(Web Application Project)。两者最大的差别在于'项目'采用动态编译的架构运作。如:中只要编辑并存储了'App_Code'目录下的类或强类型数据集,就会使整个网站项目在后台重新编译,有时甚至会阻碍在vs中进行操作,若项目规模变大,就很容易拖慢开发速度。而MVC要求项目类型最好是'网址',可以利用自动化辅助功能开发。
2. 初学者常犯的错误
对于习惯'项目'的人来说,当程序被修改保存后,即可直接切换至浏览器进行测试。但由于MVC需以'网址'的项目类型来开发,所以当程序被修改之后,都要先手动生成项目,才能将修改的结果编译进'bin'目录下组建中,这样才可以切换到浏览器测试,否则所做的修改都不会反映到页面上。
3. 小心使用Request对象和Response对象
虽然在Controller中可以访问Request对象,但对于从客户端传来的QueryString或Form数据,建议不要通过Request对象取得,以免MVC项目难以维护。
Response对象也是早期ASP和ASP.NET Web Forms中常用的对象,在controller中建议尽量不要使用。当然,使用它来响应页面内容更要不得。
不过有规定就例外,只是提醒尽量不要用,并非不能用。如自行开发CAPTCHA图片验证功能时,可能需要通过Response对象响应经过动态运算的图片。
4. 不要在视图中编写过多的程序逻辑
在开发MVC视图时,一定要尽可能的简化逻辑。不要将过多的商业逻辑写在代码里。
二、改良MVC项目
1. 使用视图数据模型
上一篇中的留言板中,因为先前在 MvcGuestbook.edmx文件创建的'留言板'数据模型包含了5个字段,但是'id'与'建立时间'这两个字段在存储留言中用不到。这样,通过动作窗口接收数据时会有一定的安全风险。所以一些开发,针对一些有特殊目的的窗口或字段,会采用自定义模型的方式来定义。
TIP : 通过'Entity Framework'生成的'Model.留言板'类当成ViewModel使用,若save动作设定的参数类型为'Model.留言板',而且直接将接收到的对象保存到数据库中的话,原本预期只会收到3个参数,但黑客如果知道其他字段的名称(例如创建时间),就可能会多输入一个Form数据,进而复制进你的数据库,并调过字段的默认值。这样,数据库就被注入数据了。
此类数据模型大多专用于视图中,也称为视图数据模型,即ViewModel。下面是ViewModel的创建过程。
首先,在'Models'目录下新建一个数据类型(也就是一般的C#类),并将其命名为'GuestbookForm.cs',如图:
由于留下足迹窗口中只有3个字段,所以在此类中要定义3个属性。具体如下:
1 namespace MvcApplication1.Models 2 { 3 public class GuestbookForm 4 { 5 public string name { get; set; } 6 public string Email { get; set; } 7 public string content { get; set; } 8 } 9 }
修改'/Views/Guestbook/Write.aspx'页面的"<% @page...%>"语句,将Inherits属性修改成强制类型,并将ViewPage的属性改为输入MvcApplication1.Models.GuestbookForml类,示例如下。注意,控制器的"return View();"语句不需要修改。
Inherits="System.Web.Mvc.ViewPage<MvcApplication1.Models.GuestbookForm>"
接下来修改原程序中 '<%=Html.Label("name") %>'或'<%=Html.TextBox("name") %>'为MVC2.0新建的强制类型HTML辅助方法,如图,在此过程中还可以利用IntelliSense来编写程序,而且不必担心把字段名称写错。
NOTE: "Html.LabelFor(x=>x.name)"语句中的"x=>x.name"又称为Lambda表达式,是C#3.0/4.0提供的一种机制。
至于最后输出到浏览器的页面,还是跟之前一模一样。
接下来我们修改Save()动作。先回忆一下原来的声明,具体如下:
public ActionResult Save(string name,string Email,string content)
将以上代码修改如下:
public ActionResult Save(Models.GuestbookForm data)
我们将原来的3个参数改成只剩下一个,使用的一样是之前提到过的Model Binder机制。使用这个机制,将自动通过QueryString()方法或Form()方法传来的参数找到对应的强制类型中的属性。只要名称对上了,该机制就会把数据放入该对象的属性中。
此时,Write视图和Save动作已经在无形中联系起来了,而且使用的是同一个数据模型。这种为了视图而另外定义的数据模型称为视图数据模型(ViewModel),修改完成的程序代码如下:
1 [HttpPost] 2 public ActionResult Save(Models.GuestbookForm data) 3 { 4 MvcApplication1.Models.GuestbookEntities db = new Models.GuestbookEntities(); 5 6 db.AddTo 留言板(new Models.留言板() 7 { 8 name =data.name, 9 Email=data.Email, 10 content=data.content, 11 dtime=DateTime.Now 12 13 }); 14 15 db.SaveChanges(); 16 ViewData["name"] = data.name; 17 ViewData["Email"] = data.Email; 18 ViewData["content"] = data.content; 19 20 return View(); 21 }
这段程序代码跟之前差不多,只是参数变少了。但Write视图与Save地址共享用一个ViewModel及用强制类型的方式开发项目有着非常高的价值。如果日后数据库字段名称被修改了,通过VS2010内置的重构机制,将能更有效的针对整个项目进行字段名称的替换。
三、使用窗口验证功能
好比避免之前留言为空就直接提交到数据库的情况。MVC内置了数据验证机制,只要搭配ViewModel及NET 5 SP1,或者.NET 4.0提供的Data Annotations的函数库,就能定义数据验证规则,同时实现客户端的验证与服务器端的验证了。
要让GuestbookForm这个ViewModel在使用时能验证'姓名'与'内容'字段为必填项,而且希望当用户输入E-mail数据时能验证其格式,可以通过Data Annotations函数库的声明设定将这些功能应用在ViewModel上。以下是应用Data Annotations函数库的程序代码,可以看出,这里只是多加载了System.ComponentModel.DataAnnotations命名空间和新建了几个属性(Attribute)而已。暂时看不懂,没关系,先看看名字。猜猜用途。
1 using System.ComponentModel.DataAnnotations; 2 3 namespace MvcApplication1.Models 4 { 5 public class GuestbookForm 6 { 7 [Required] 8 public string name { get; set; } 9 [DataType(DataType.EmailAddress)] 10 [RegularExpression(@"^[a-z0-9\._%+-]+@[a-z0-9\.-]+\.([a-z]{2}|com|org|net|edu|gov|mil|biz|info|mobi|name|aero|asia|jobs|museum)$")] 11 public string Email { get; set; } 12 [Required] 13 public string content { get; set; } 14 } 15 }
利用ModelState.IsValid 验证,可以通过数据模型绑定进来的数据是否符合ViewModel字段验证的要求,如果验证失败,ModelState.IsValid验证就会返回"false"值,并转到"留下足迹"页面,如下:
1 [HttpPost] 2 public ActionResult Save(Models.GuestbookForm data) 3 { 4 5 if (!ModelState.IsValid) 6 { 7 return RedirectToAction("Write"); 8 } 9 MvcApplication1.Models.GuestbookEntities db = new Models.GuestbookEntities(); 10 11 db.AddTo 留言板(new Models.留言板()
至此,我们已经完成了服务器验证的设置工作,接着来看MVC是如何让实现"客户端JavaScript验证"机制的,一共有一下3个步骤。
Step01:打开"/View/Shared/Site.Master"文件,在"<head>…</head>"标记之间添加以下3个项目内置的javascript文件。
<script src="http://www.cnblogs.com/Scripts/MicrosoftAjax.js" type="text/javascript"></script> <script src="http://www.cnblogs.com/Scripts/MicrosoftMvcAjax.js" type="text/javascript"></script> <script src="http://www.cnblogs.com/Scripts/MicrosoftMvcValidation.js" type="text/javascript"></script>
Step02:在"<%using (Html.BeginForm("save",RouteData.Values["controller"].ToString())) {%>"语句之前加入以下声明,以启用客户端验证功能。
<%=Html.EnableClientValidation(); %>
TIP:若在一个页面中有两个或者两个以上的窗口需要进行验证," <%=Html.EnableClientValidation(); %>"语句必须出现在第一个Html.BeginForm声明之前,否则验证将无法执行。
若要使用" <%=Html.EnableClientValidation(); %>"语句验证功能,必须使用Html.BeginForm声明窗口,用户自行编写的<form>标签无法正常使用此功能。
Step03: 添加字段验证失败时要显示的错误提示信息。
首先,添加所有验证失败结果的Html.ValidationSummary()方法,示例如下:
<%=Html.ValidationSummary() %>
为每个字段加上ValidationMessageFor()方法,以显示验证失败的错误提示,示例如下:
1 <%=Html.LabelFor(x => x.name) %> 2 <%=Html.TextBoxFor(x => x.name) %> 3 <%=Html.ValidationMessageFor(x=>x.name) %> 4 <br /> 5 <%=Html.LabelFor(x=>x.content) %> 6 <%=Html.TextBoxFor(x=>x.content)%> 7 <%=Html.ValidationMessageFor(x=>x.content) %> 8 <br /> 9 <%=Html.LabelFor(x=>x.Email) %> 10 <%=Html.TextBoxFor(x=>x.Email)%> 11 <%=Html.ValidationMessageFor(x=>x.Email) %> 12 <br />
完成以上3步,调试即可开始测试。
HTML4.01标准规定的命名规则如下:
若有中文名称,可以选中右键然后选取"重建"——"重新命名"选项,或将键盘光标移至该属性名称处,再按下【F2】功能键,都能快速更改属性名称。如图:
四、避免用户重复发送信息
目前留言板中后会停留在"储存留言"页面,如果此时用户按下了浏览器的"刷新"按钮,将导致窗口重复发送信息,那么在数据库中就会有两条重复的数据。为解决这个问题,PRG(Post-Redirect-Get)样式的概念,是许多网站经常使用的技巧之一。当遇到需要传送的窗口数据的情况下,会先通过HTTP POST请求送出数据;在处理完数据之后,立即转向(Redirect)另一个网址;最后通过HTTP GET请求取得跳转后的页面。在此,修改Save动作,让Save动作不再显示结果页面,而是直接转向Result动作。步骤如下:
Step01: 删除下列程序代码。
1 ViewData["name"] = data.name; 2 ViewData["Email"] = data.Email; 3 ViewData["content"] = data.content; 4 5 return View();
Step02: 在删除的程序代码处插入如下语句:
return RedirectToAction("Result");
Step03: 删除"/Views/Guestbook/Save.aspx"页面。
Step04: 新建Result动作,示例如下:
public ActionResult Result() { return View(); }
若要之前Save动作接收到的GuestbookForm类的数据传递给Result视图,以显示用户输入的摘要,这是就必须使用TempData机制来传递这种只会用到"1次"的数据。
TempData是一个Directory对象,它与ViewData很像,但非常特殊。所有存储在TempData中的数据只能被参考一次,用过之后数据就会被清空。可以利用TempData的特性,将Save动作接收到的GuestbookForm类的数据传递给Result动作。修改Save动作的程序代码,如下:
TempData["LastPostGuestbookFormData"] = data;
Step05: 修改Result动作,程序代码如下:
1 public ActionResult Result() 2 { 3 if (TempData["LastPostGuestbookFormData"] == null) 4 { 5 return RedirectToAction("Index"); 6 } 7 var model = (Models.GuestbookForm)TempData["LastPostGuestbookFormData"]; 8 return View(model); 9 }
Step06: 新建Result动作视图。新建一个强类型视图,并选取适当的视图数据类型和视图内容之后,即可选取"Details"选项来显示单笔数据,如图:
通过"视图内容"选项可以创建大部分的程序代码,只稍加修改,就可以得到我们需要的网页,示例如下:
1 <%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<MvcApplication1.Models.GuestbookForm>" %> 2 3 <asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server"> 4 留言成功 5 </asp:Content> 6 <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server"> 7 <h2>留言成功</h2> 8 <fieldset> 9 <legend>Fields</legend> 10 11 <div class="display-label">name</div> 12 <div class="display-field"><%: Model.name %></div> 13 <div class="display-label">Email</div> 14 <div class="display-field"><%: Model.Email %></div> 15 <div class="display-label">content</div> 16 <div class="display-field"><%: Model.content %></div> 17 18 </fieldset> 19 <p> 20 <%: Html.ActionLink("Edit", "Edit", new { /* id=Model.PrimaryKey */ }) %> | 21 <%: Html.ActionLink("回到留言清单", "Index") %> 22 </p> 23 24 </asp:Content>
再次运行,打开浏览器进行测试,留言成功后就会跳转到"http://localhost:1309/Guestbook/Result"页面,并显示上一页中GuestbookForm类的内容。如图:
如果此时我们按下"刷新"按钮,由于"TempData["LastPostGuestbookFormData"]"语句已被删除,所以网页就会指向"http://localhost:1309/Guestbook"显示"留下足迹"的页面。