ASP.NET MVC 1.0 学习笔记(随时更新)
2011-05-03 作者注:现在已经MVC3.0了,本帖不再更新。请参考本人在asp.net: MVC、Razor 分类中的新文章。
本文是电子书ASP.NET MVC 1.0的学习笔记,记录了阅读和使用本书中案例时遇到的问题和经验。
某些问题是低级的(所以才记录下来免得大家浪费时间呵呵),见笑。
免费电子书:ASP.NET MVC 1.0
http://aspnetmvcbook.s3.amazonaws.com/aspnetmvc-nerdinner_v1.pdf
推荐:★★★★★
除了是英文版之外,基本上是完美的。如果阅读没有问题,非常建议自学本书。作者通过一个完整的实例NerdDinner(完整到连单元测试项目都建了),展示了使用MVC1.0的整个过程。
遇到问题或思考(页码就是PDF中的页码)
P17 问题:按下右键时居然没有Add New Item
差点以为是用的VS版本不对重新下载安装一个,后来才发现正在调试程序(F5),不允许这个操作。按Shift+F5就好了。低级错误。
P26 关于是否使用LINQ
一直没有仔细学习LINQ。
原来希望的编程风格时:使用Table存储原始数据,然后大量使用View来创建实际的业务场景所需展示的内容,然后用一个ASP控件把View中的数据全部读出;使用View避免了大量的拼装SQL,实在不行的时候再拼装SQL。这样所有逻辑在View的SQL中而非LINQ中完成。
选择LINQ做一个忘记SQL的C++/C#程序员?还是选择拼装SQL做一个忘记C++/C#的SQL程序员?这是一个问题。
注:在读完以下到P100的时候,由于使用LINQ而节省的SQL代码行还是非常可观的;所以使用LINQ应该是以后的趋势。
我会另写一个文章来比较LINQ使用与否的优劣(LINQ在拼装动态语句的时候比较费力)。
P27~32 的LINQ示例令人难忘
简洁和清晰程度超过之前看过的任何示例,一个头脑清醒的C++?C#程序员看完之后可以可以清晰理解LINQ的价值。
P44 编译错误、运行错误
在P44提到的We can then run the application...的时候会遇到几个编译错误:
一个是ChangeAction不存在,先注释掉整个OnValidate函数。
一个是IENumerable不存在,需要在前面加上:
using System.Collections; // for IEnumerable
using System.Collections.Generic; // for IEnumerable<>
using System.Data.Linq; // for ChangeAction
一个是DinnersController里边已经有一个返回ActionResult 而非void index()了,得先注释掉。别删除,真正该删除的是void的。
运行时偶然会遇到一个“资源被占用”的错误,关闭IE窗口重新来就可以了。
P48 代码编译错误
增加using NerdDinner.Models;好了。
P56 界面显示内容与教材不同
多了个IsValid字段,这个属性也被显示出来了,当然可以通过更改P55的代码删除之。
P60 运行后列表为空
还真愣了一会,后来想起来了,在public IQueryable<Dinner> FindUpcomingDinners()里边我们只选择了“晚于今天”的聚餐~
进数据库表修改数据,好了。
P65~P66 大段文字解说
这几段爆长的话还是挺重要的。大致解决的就是下面的问题:
public ActionResult Index()
{
var dinners = dr.FindUpcomingDinners().ToList();
// return View("Index", dinners); //与下面一行功能相同
return View(dinners);
}
public ActionResult Details(int id)
{
Dinner dinner = dr.GetDinner(id);
if (dinner == null)
return View("NotFound");
else
return View(dinner);
}
public ActionResult Edit(int id)
{
Dinner dinner = dr.GetDinner(id);
return View(dinner);
}
这三个return View(dinner);写的一模一样,为什么第一个(包括被注释掉的那行)总是去index View,第二个总是去Detail View,第三个总是去Edit View?原因就是MVC总是做名字匹配,来判断去那个View。
但是笔者没有找到名字匹配记录在哪里了,但肯定不是在View里边,因为DetailsView和EditView的第一行是完全相同的。
从文中可以看出我们其实可以让Details()总是去MyDetails View,但由于会出现命名混乱,还是不要为好。
P70 Description字段是TextBox还是TextArea?
我实际生成的代码时TextBox(因此很窄小),而文中代码是TextArea,可以手动改,但显然应该用Area更好。
另外反射生成的时候不应该把DinnerID也生成出来,因为明明可以通过数据库确认这是一个PK的。
对于错误的信息,只用一个*显然不太友好。比如这里的日期只到分钟。编辑状态还好,但后面的创建页面谁能知道有这样一个规则呢(注意Details里边使用的是更容易误导人的“3/1/2009 @ 5:00PM”,所以如果这真是一个要上线的软件,建议使用下面这样的错误提示:
<%= Html.ValidationMessage("EventDate", "Format: YYYY/MM/DD HH:MM") %>
P76 RedirectToAction的用法
下面这三行是否等同?
public ActionResult Edit(int id, FormCollection formValues)
{
......
return RedirectToAction("Details", new { id = dinner.DinnerID });
return View("Details", dinner);
return Details(id);
}
我试验了一下,第二行和第一行(原书上的代码)结果相同。
但一个重要区别是,上面一行其实去了Dinners/Details/X (X就是ID的值),也就是调用了Details(id)函数而不是简单的产生一个View,在我们这里Details函数里边只判断了是否为空就显示出来了,所以两者结果相同。
但如果还有更复杂的操作(比如如果想发现是过期的,就调用一个过期的Details页面显示一些Dinner照片,这样就会出错误了,因此用RedirectToAction是正解)。
很诡异的是:第三行直接调用Details(id)是错误的!将停留在Edit页面。Details()里边返回了一个View(dinner),按理说应该是Details View,结果却不是。不知道是BUG还是应当如此。
总之:RedirectToAction是正解,虽然写法最长最费劲。
P82 如何显示红色的Summary错误信息
因为public IEnumerable<RuleViolation> GetRuleViolations()中没有对EventDate进行验证,所以记住别用这个字段测试就可以了。我设置了断点才发现……
P83 本页最上面代码放在哪里?
文中没有交代(我一点点敲的,没下载源码),经过分析,下面是最好的选择:
这个方法还是很常用的,而且只用到了标准类,应该被复用。其实前面的RuleVoilation也应该被复用的,所有后来在Models同级建了一个SFC目录(我自己的基本类库),加入类RuleVoilation,把几个应该被复用的内容都放进来了,内容如下:
……(省略若干Using)
using System.Web.Mvc;
using System.Collections;
using System.Collections.Generic;
namespace SFC.RuleViolation
{
public class RuleViolation
{
public string ErrorMessage { get; private set; }
public string PropertyName { get; private set; }
public RuleViolation(string errorMessage)
{
ErrorMessage = errorMessage;
}
public RuleViolation(string errorMessage, string propertyName)
{
ErrorMessage = errorMessage;
PropertyName = propertyName;
}
}
public class PhoneValidator
{
public static bool IsValidNumber(string PhoneNumber, string Country)
{
return true;
}
}
public static class ControllerHelpers
{
public static void AddRuleViolations(this ModelStateDictionary modelState, IEnumerable<RuleViolation> errors)
{
foreach (RuleViolation issue in errors)
{
modelState.AddModelError(issue.PropertyName, issue.ErrorMessage);
}
}
}
}
当然要在编译后,在几个报错的文件中补充
using SFC.RuleViolation;
P88 下面代码的函数声明有误
因为前面已经有一个无参数的Create了,这里应该为:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(FormCollection formValues)
否则会得到一个编译错误。
P93 删除时没有“取消”按钮
看下面的删除确认截图没有取消按钮,Nerd们可能知道按Backspace可以取消,其他人够呛,比如老太太很可能要关电源才能取消。
不过问题是,取消后回哪呢?可能是从Detail来的,也可能是从index来的(本例中不是因为index里边原来那几个Delete按钮被删掉了),所以暂时我在P93上面代码末尾加了两行:
<br />
<%=Html.ActionLink("Back to Upcoming Dinners", "Index") %>
<%=Html.ActionLink("Back to Details", "Details", new { id = Model.DinnerID}) %>
</div>
如果能直接Back,从哪里来回哪里去就更好了。
P100 做个总结
100页之前的内容,可以总结为:V认识M,C认识V和M,M谁都不认识,详细如下:
V通过Html和Model认识M,比如:
单个dinner:
<%= Html.TextBox("Title", Model.Title) %>
多个dinner:
<% foreach (var dinner in Model) { %>
<li>
<%= Html.ActionLink(dinner.Title, "Details", new {id = dinner.DinnerID}) %> 
C通过View()认识Wiew,比如:
return View("NotFound");
return View(dinner); //单个dinner,自动识别
return View(dinner是); //多个dinner,自动识别
C通过Repository认识M,C里边有一个成员变量:
public class DinnersController : Controller
{
DinnerRepository dr = new DinnerRepository();
C对M的操作包括:
Dinner dinner = dr.GetDinner(id); //单个读
var dinners = dr.FindUpcomingDinners().ToList(); //多个读
UpdateModel(dinner);dr.Add(dinner);dr.Save(); //添加
UpdateModel(dinner); dr.Save(); //编辑
dr.Delete(dinner); dr.Save(); //删除
想把M检查出来的Voilation传送给V,需要这2行:
ModelState.AddRuleViolations(dinner.GetRuleViolations()); //C天生认识ModelState
return View(dinner);
C想去某个V,方法包括(下面四行有点含糊,先略过,弄明白了我再回来补充):
return View(dinner); //打开Edit View,看dinner
return View(dinners); //打开List View(Index),看多个。这里本来也是去一个能多个Edit、Delete的页面的,后来被人工改成了只读的。
return View("NotFound"); //打开某个名字的V,都是没有dinner等数据内容的
return RedirectToAction("Details", new { id = dinner.ID }); //去Details方法看dinner,它会重新定位到某个View。
P102 代码运行出现InvalidOperationException:
The ViewData item with the key 'Country' is of type 'System.String' but needs to be of type 'IEnumerable<SelectListItem>'.
检查后原因是打字错误,在声明ViewData的时候写成了“Contries”而非“Countries”。
关于这类打字错误如何更早发现(使用ViewData在运行的时候才知道),引出了后来P103对ViewModel的讨论。
P103 catch中突然出现的AddModelErrors是什么?
此处是第一次出现,原来是AddRuleViolations。编译不通过(没有单个参数的构造器)。
P104以下DinnerFormViewModel设计感觉得不偿失。
做这个设计的目的在P103,包括:
1. ViewData后面的key写错了(见前面笔者打字错误),只有运行的时候才能知道。
2. 如果软件相当大,ViewData这个字典中会出现很多数据,难免会出现重复(设想:在NerdDinner中有一个“Countries”,如果再有其他地方有Countries,则不得不区别命名,但很可怕的是:很可能两个程序员不知道对方已经使用Countries了)。
但书中的设计难免太复杂了:创建了新的Model,修改aspx中对应的字段,每次调用还要new一个新的Model名字还爆长……把MVC变成MMVC了,而目的只是处理一个DDL问题。
笔者尝试了一下,如果不考虑P107的目的,下面这个设计是最好的(强烈推荐):
1. 在Dinners.cs中增加一个函数:
public partial class Dinner
{
public SelectList ValidCountry()
{
return new SelectList(PhoneValidator.Countries, Country);
}
2. 在Edit.aspx及Create.aspx中改为:
<%= Html.DropDownList("Country", Model.ValidCountry()) %>
测试,结果完全相同,代码也更加简洁。
特别是我们再也不需要改动Edit()、Create()这几个无辜的函数了,这件事情本来就与他们无关。
几个aspx里边的Model也不用改成Model.dinner了。
当然可以使用Coutries, Countries(), ValidCountries, ValidCountries()等各种形式,看个人喜好了。
如果采用上面的方法,书中104~106所有改动均不需要执行。
P107 关于ViewModel模式
回到P104的讨论,如果一开始就使用DinnerFormViewModel,其实给几个aspx引来的麻烦并不大,主要还是Edit、Create这两个函数需要每次都new一个新类送出。这时候使用ViewModel模式还是有意义的。
在这个例子中,因为Dinner过于简单,可能没有前后文让我们选择使用或者不使用ViewModel模式。
从敏捷的角度看,当无法判断的时候,应该优先选择简单的实现方法,日后复杂到一定程度的时候在进行重构。否则为所有Model都创建一个ViewModel的工作量是非常可观的。
P107的第二段文字可以说比较好地给出了一个何时需要使用ViewModel的标准。
P116 此处有文字错误
三个Menu应该为:(我保留了Home因为上面有测试链接)
<li><%= Html.ActionLink("Home", "Index", "Home")%></li>
<li><%= Html.ActionLink("Find Dinner", "Index", "dinners")%></li>
<li><%= Html.ActionLink("Host Dinner", "Create", "dinners")%></li>
<li><%= Html.ActionLink("About", "About", "Home")%></li>
文中出错的是FindDinner,应该是dinners,而非home
P117图片错误
此处应该是想显示Create dinner页面。
P121 为何new { controller = "Dinners", action = "Index"}中没有 page = ""
在中间代码中我试验了一下,new { controller = "Dinners", action = "Index", page = "" }和没有page结果相同。
这里可以不使用page = ""原因是我们已经约定了可以使用int?。
同理,由于那些Edit、Details等不允许id为空,所以必须指定id=""。如果删除id="",将在运行初期就得到一个HttpException。
P123 以下关于Page Navigation UI(PNUI)的设计不好
做PNUI显然是非常常见的事情,但书中的实现存在这样几个问题:
1. 在每个aspx中,都要拷贝粘贴P125的几行代码,但这几行代码不是通用的,因为“UpcomingDinners”这个字符串需要每次改动。可怕的是,如果你忘记了改动某个,也可能会工作(会跑到另外一个地方去)。
2. 在每个aspx中,需要保持<<<和>>>的一致,比如有人偶然使用了<<和>>或者<和>,都会导致风格不一致。
3. 如果有一天数据量大了想使用|< >|,需要找到以前所有这些地方添加上。
所以笔者尝试做了一个全局统一的PNUDI。设计思路如下:
1. 当然希望使用ascx来实现,这样上面的三个问题全部解决了,比如:
在任何aspx中添加下面的代码,就会产生导航按钮:
<% Html.RenderPartial("DataPages", Model.DataPages); %>
注意这个地方使用的就是P112红框上面文字中提到的:
Alternatively, there are overloaded versions of Html.RenderPartial() that enable you to pass an alternate
Model object and/or ViewData dictionary for the partial view to use. This is useful for scenarios where
you only want to pass a subset of the full Model/ViewModel.
DataPages就是这个subset.
2. 设计ascx,下面就是内容:
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<SFC.DataPages>" %>
<% using (Html.BeginForm()) {%>
<%= Html.RouteLink(Model.FirstSymbol, Model.Url, Model.FirstObject) %> 
<% if (Model.PreSymbol!=null) %>
<%= Html.RouteLink(Model.PreSymbol, Model.Url, Model.PreObject)%> 
<% if (Model.NextSymbol!=null) %>
<%= Html.RouteLink(Model.NextSymbol, Model.Url, Model.NextObject)%> 
<%= Html.RouteLink(Model.LastSymbol, Model.Url, Model.LastObject)%> 
<% } %>
注意我们使用的Model是SFC.DataPages,和Dinner等特定实体无关的,这样保证了任何aspx都可以调用这个ascx.
DataPages封装了原来的:
<%= Html.RouteLink("<<<", "UpcomingDinners", new { page=(Model.PageIndex-1) }) %>
3. 设计DataPages,这个稍微复杂一点。
先从index看起:
public ActionResult Index(int? page)
{
var upcommingDinners = dr.FindUpcomingDinners();
var paginatedDinners = new PaginatedList<Dinner>(upcommingDinners, "UpcommingDinners", (page ?? 0), 5); // 5 is the pageSize.
return View(paginatedDinners);
}
这里和书里边区别不大,但是我们把"UpcommingDinners“传进去了,自然是让PaginatedList里边的DataPages记住它。
PaginatedList实现是这样的(里边的代码直接用不用动):
using System.Collections.Generic;
namespace SFC
{
public class PaginatedList<T> : List<T>
{
public DataPages DataPages { get; private set;}
public PaginatedList(IQueryable<T> source, string url, int pageIndex, int pageSize)
{
this.AddRange(source.Skip(pageIndex * pageSize).Take(pageSize));
DataPages = new DataPages(source.Count(), url, pageIndex, pageSize);
}
}
public class DataPages
{
public int PageIndex { get; private set; }
public int PageSize { get; private set; }
public int TotalCount { get; private set; }
public int TotalPages { get; private set; }
public string Url { get; private set; }
public string FirstSymbol { get; private set; }
public string PreSymbol { get; private set; }
public string NextSymbol { get; private set; }
public string LastSymbol { get; private set; }
public object FirstObject { get; private set; }
public object PreObject { get; private set; }
public object NextObject { get; private set; }
public object LastObject { get; private set; }
public DataPages(int TotalCount, string url, int pageIndex, int pageSize)
{
Url = url;
PageIndex = pageIndex;
PageSize = pageSize;
TotalPages = (int)Math.Ceiling(TotalCount / (double)PageSize);
FirstSymbol = "|<";
PreSymbol = (PageIndex > 0) ? "<<" : null;
NextSymbol = (PageIndex+1 < TotalPages)? ">>":null;
LastSymbol = ">|";
FirstObject = new { page = 0 };
PreObject = new { page = (PageIndex - 1 < 0)? 0:PageIndex - 1 };
NextObject = new { page = (PageIndex + 1 > TotalPages - 1)? TotalPages - 1:PageIndex + 1 };
LastObject = new { page = TotalPages - 1 };
}
}
}
配合DataPages.ascx中的代码,你一定会理解这样设计的好处了:我们只需要2行实质性的代码,就能为任何页面产生一个PNUI!
这三行代码我用红色字体在上面标注出来了。
有另外几个问题:
如果有一天,Dinners太多需要显示|< << 1 2 3 4 5 ... >> >|,但是RSVP不多只需要显示|< << >> >|怎么办?(其中<<>>有可能因为到头或者尾而不显示)
答案是可以在DataPages中设置Type,表明是显示哪种(或者用TotalCount自动处理)。然后,把应该显示什么(比如开始是12345到后面是10 11 12 13了)放到一个List中而非在DataPages.ascx中。DataPages.ascx中直接做一个for each把应该显示的东西放出来就可以了。
当然在本例中这也太复杂了,就不是即实现了。
生词和缩略表
P10 convention 习惯。这里指良好的目录结构习惯。
P14 paranoid 多疑的。这里指要编写开放的IE程序,必须保持对“用户”的警惕性。
P14 bogus 假的。这里指用于攻击的假数据。
P72 curly brace 就是{}花括号
P76 verbose 冗长的。
P107 aggregate 集合。aggregate properties指如果你的某个View显示的不只是一个来自于一个Model二十多个时,所需要展示的属性集合。
P108 DRY Dont' Repeat Yourself,不要重复你自己。就是不要CVS(Ctrl+C,Ctrl+V,Ctrl+S)。
P109 render 表现。rendering指view中的表现层代码。
P45、P118 SEO(Search Engine Optimization)搜索引擎优化
P112 subtle 微妙的。
P113 semi-colon就是;分号。