本文是电子书ASP.NET MVC 1.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}) %>&nbsp

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) %>&nbsp

        <% if (Model.PreSymbol!=null) %>

            <%= Html.RouteLink(Model.PreSymbol, Model.Url, Model.PreObject)%>&nbsp

        <% if (Model.NextSymbol!=null) %>

            <%= Html.RouteLink(Model.NextSymbol, Model.Url, Model.NextObject)%>&nbsp

            <%= Html.RouteLink(Model.LastSymbol, Model.Url, Model.LastObject)%>&nbsp

    <% } %>

注意我们使用的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就是;分号。