前段时间因项目需要对ASP.NET MVC3进行了系统的学习,完成项目后感觉对原书中有些知识点还有一些理解不够透彻的地方,因此将《Freeman A. Sanderson S. - Pro ASP.NET MVC 3 Framework Third Edition - 2011》这本书再重新学习了一遍,结合作者的体会,将一些知识点记录下来,希望能对遇到相同问题的朋友有所帮助。在此之前需要跟读者说明的是本文并不是一篇合适的MVC入门指导,更不敢妄论进阶文章。本文主要目的是提供一些我们用MVC开发WEB过程中常见应用场景对应的解决方案,作者希望遇到这些问题的读者能在本文中得到帮助。如有其它MVC有关的问题,也欢迎读者留言讨论。同时作者水平有限,文中难免有纰漏,希望读者指出共同交流进步。
1.类扩展
MVC最大的一个特点就是可扩展,几乎MVC提供的任何内置功能都可以自定义和扩展,你可以定义自己的Controller factory、自己的model binder、甚至自己的view engine,而不使用MVC3提供的razor。
这里要说的是MVC提供的最实用的类扩展,很多时候我们使用了一些第三方的类库,而且我们无法获取源代码,但是想扩展某个类时就可以采用MVC提供的类扩展功能:
public static class MyExtensionMethods {
public static decimal TotalPrices(this ShoppingCart cartParam) {
decimal total = 0;
foreach (Product prod in cartParam.Products) {
total += prod.Price;
}
return total;
}
}
MyExtensionMehods是我们新定义的一个类,我们为shoppingCart类扩展了一个TotalPrices方法,该方法将原类中products成员的price相加得出总价。
需要注意的是这个MyExtensionMethods类必须是static类型的,而扩展方法第一个参数必须是this关键字加上原类的形式,而且这个新扩展的方法是无法访问原类里private成员的。
2.yield关键字
该关键字只能用在迭代器中,为了更好的说明该关键字的使用方法,我们来假设一个应用场景:
我们定义了一个方法,用迭代器获取我们超市里所有某种商品的位置信息,return一个list。所有位置对我们来说是不分先后的。我们在前台调用这个方法询问时必须等到所有位置被返回,而实际上我们需要的可能只是几个而已。这是如果在方法里的迭代器中采用yield return,就可以找到一个位置就返回,再继续寻找下一个位置。
因此yield的作用就是不等到迭代完毕就返回当前的内容,而实现原理就是在迭代器中每次return一个值后,下次再在退出迭代的地方继续迭代下去。
3.View中WebMail的用法
MVC中发送mail逻辑可以在controller里通过smtpclient类去实现,我们也可以在view中用webmail helper来实现:
@{
try {
WebMail.SmtpServer = "smtp.example.com";
WebMail.SmtpPort = 587;
WebMail.EnableSsl = true;
WebMail.UserName = "mySmtpUsername";
WebMail.Password = "mySmtpPassword";
WebMail.From = "rsvps@example.com";
WebMail.Send("party-host@example.com", "RSVP Notification",
Model.Name + " is " + ((Model.WillAttend ?? false) ? "" : "not")
+ "attending");
} catch (Exception) {
@:<b>Sorry - we couldn't send the email to confirm your RSVP.</b>
}
}
我们给webmail提供如smtp server、port等信息,然后调用send方法将特定内容发送出去。
4.lambda expression
相信很多刚接触C#的同学跟我一样,对于lambda如此简洁的形式不容易理解,其实只是系统编译时会根据上下文给x=>...中的x指定类型。
我个人觉得函数代理也许是lambda表达式得到最好应用的地方,比如我们有一个函数根据产品类别返回一个bool值:
Func<Product, bool> categoryFilter = delegate(Product prod) {
return prod.Category == "Soccer";
};
说到函数代理,就拿上面这个例子来说,比如我们某个地方要用到bool变量,但是这个bool是受其他变量控制的动态变化的,我们就可以写成Func<T,bool>的形式(T是控制bool值的某种类型,当然也可以传入多个参数),然后具体使用时再具体实现这个Func<>,这样就带来了很大的灵活性。
回到正题,这个函数代理如果用lambda表达式则可以这样写:
Func<Product, bool> categoryFilter = prod => prod.Category == "Soccer";
因为系统根据上下文知道这个prod就是Product,如果有多个参数则可以这样写:
(prod, count) => prod.Price > 20 && count > 0
5.购物车的实现
相信大家都有网购的经验,我们的购物车里的东西不会因我们url的跳转而消失,很多时候这就是session在发挥作用,session很适合这种应用场景:
1.数据不会只存在于一次http请求中,类似的诸如viewbag viewdata tempdata则会在请求结束后自动vanish
2.无需长久保存下来,这样就无需用到db去存储
6.在action中获取提交form的Url
大家在购物车里如果点击继续购物,是否会跳转到之前的页面?实现这个我们就需要保存之前的url以便跳转。
我们在进入购物车的form加入如下代码:
@Html.Hidden("returnUrl", Request.Url.PathAndQuery)
然后在action里提取这个returnUrl就可以实现上述功能
7.注册特定类的model binder
我们来回顾一下model binding的过程:action有一些参数,每个参数都去跟controllercontext类里合适的property做匹配,找到合适的就赋值。没找到就抛出exception
有时候我们希望给action提供一个参数,这个参数不在controllercontext里(比如session),我们就需要自定义一个model binder,而且把他与参数类别绑定起来,我们只需
在global.asax 中添加(T是session中的一个对象):
ModelBinders.Binders.Add(typeof(T), new CartModelBinder());
8.model attribute
我们知道MVC里的M指的是model,就是存储我们的数据格式,很多时候就是业务逻辑的部分,而model attribute就是帮助我们细化业务逻辑的利器,要想使用model attribute,我们需要在model类里引入DataAnnotations:
using System.ComponentModel.DataAnnotations
我举几个常用的例子:
[Display(Name="Date of Birth")]
[HiddenInput(DisplayValue=false)]
[Required(ErrorMessage="pleas input right content!")]
分别表示在页面上显示的名字,隐藏输入,必须值
9.在action里发送mail
前面提到在view里用webmail helper发送mail,现在我介绍一下在action里用SmtpClient类来发送mail的方法,其实可以预见,如webmail一样,我们只需提供from、to、subject、content及其他一些settings就可以完成一封mail的发送:
//设置发送email所需的一些基本信息
public class EmailSettings {
public string MailToAddress = "orders@example.com";
public string MailFromAddress = "sportsstore@example.com";
public bool UseSsl = true;
public string Username = "MySmtpUsername";
public string Password = "MySmtpPassword";
public string ServerName = "smtp.example.com";
public int ServerPort = 587;
}
//获取smtp server等信息
emailSettings = new EmailSettings()
using (var smtpClient = new SmtpClient()) {
smtpClient.EnableSsl = emailSettings.UseSsl;
smtpClient.Host = emailSettings.ServerName;
smtpClient.Port = emailSettings.ServerPort;
smtpClient.UseDefaultCredentials = false;
smtpClient.Credentials
= new NetworkCredential(emailSettings.Username, emailSettings.Password);
//获取email内容
StringBuilder body = new StringBuilder()
.AppendLine("A new order has been submitted")
.AppendLine("---")
.AppendLine("Items:");
//生成mailMessage类对象
MailMessage mailMessage = new MailMessage(
emailSettings.MailFromAddress, // From
emailSettings.MailToAddress, // To
"New order submitted!", // Subject
body.ToString());
//发送mail
smtpClient.Send(mailMessage);
10.对用户修改个人资料等行为做反馈
我们如果在论坛中修改自己的个人资料后,一般网站都会在返回页面的某个地方提示我们修改结果(如:恭喜,修改成功!)。而如果我们再刷新页面的话这个提示文字就会消失
要想在MVC里实现这个功能,我们就需要用到TempData,因为TempData的数据只存在于一次http request中,再刷新页面就会消失。
我们只需在action中根据操作结果定义tempdata的内容,然后再返回页面的顶部插入一个<div>并嵌入TempData的数据即可。
11.在MVC中用数据库存取图片
存取图片的一个典型应用就是社交应用的用户头像,用户可以自己上传图片,甚至在web进行简单的编辑。在用户信息处也会显示用户的头像来标识。
我们先从上传保存图片说起,先定义一个form让用户上传图片:
<form action="@Url.Action("Upload")" method="post" enctype="multipart/form-data">
Upload a photo: <input type="file" name="photo" />
<input type="submit" />
</form>
这里需要注意的是,不同于其他form,上传图片我们需要将form的enctype属性设为multipart/form-data,input的type设为file.接下来是我们的upload action:
[HttpPost]
public ActionResult Edit(Product product, HttpPostedFileBase image) {
if (ModelState.IsValid) {
if (image != null) {
product.ImageMimeType = image.ContentType;
product.ImageData = new byte[image.ContentLength];
image.InputStream.Read(product.ImageData, 0, image.ContentLength);
}
// save the product
repository.SaveProduct(product);
// add a message to the viewbag
TempData["message"] = string.Format("{0} has been saved", product.Name);
// return the user to the list
return RedirectToAction("Index");
} else {
// there is something wrong with the data values
return View(product);
}
}
在上面的代码中用到的Entity Framework,简单来说就是将数据库的每个表对应映射为MVC中的一个类,每一列映射为类的一个属性,repository继承dbcontext类代表了这两者之间的桥梁。在这里我们通过repository将类的一个对象存入数据库。在数据库中关于图片有两列:ImageData和ImageMimeType,前者用来存储图片的二进制数据,后者用来存储图片类型以便合理的展示图片。
接下来说一说图片展示,以下是view中展示图片的代码:
@if (Model.ImageData == null) {
@:None
} else {
<img width="150" height="150"
src="@Url.Action("GetImage", "Product", new { Model.ProductID })" />
}
GetImage方法根据传入的productid参数来获取对应的product.imagedata:
public FileContentResult GetImage(int productId) {
Product prod = repository.Products.FirstOrDefault(p => p.ProductID == productId);
if (prod != null) {
return File(prod.ImageData, prod.ImageMimeType);
} else {
return null;
}
}
12.MVC route
route是MVC很有特色的一部分,灵活多变的route意味着我们可以定义各种URL来对应到我们的action。
我们先从默认路由说起,在global.asax里我们可以找到这样一条代码:
routes.MapRoute("MyRoute", "{controller}/{action}",
new { controller = "Home", action = "Index" });
}
我详细解释一下maproute的几个参数(因为这是理解后面内容的基础):MyRoute是route名,{controller}/{action}意思是在收到一个诸如http://hostname:port/a/b 的http请求时,route系统会将a翻译为controller name,b会被翻译成action name。new { controller = "Home", action = "Index" }这样一个匿名类的意思是如果我们收到一个诸如http://hostname:port/的url 请求,我们会将这个请求引导到Home controller下的Index action,也即是默认主页的意思。
MVC的route提供了非常灵活的配置:
我们可以在action之后在定义一个如{id}一样的变量
我们可以定义某部分固定的URL的路由(static route),甚至可以通过正则表达式来匹配URL
我们也可以为如controller action 提供optional值。
当MVC发现定位到两个同名的controller而无法准确定位时,我们可以在route里定义哪个namespace有优先被检索的权力。
我们可以限制能匹配那些controller和action
总之你能想到的MVC都已经提前帮你实现了,考虑到实际应用过程中很多时候只需用到默认路由,而本文也不是一篇重点介绍route的文章,本文就不详细讨论route的具体细节了
13.自定义authorization Filter
filter算的上是MVC里我觉得最不容易掌握的部分,filter是MVC里用来给正常request processing pipeline添加其余步骤的一种手段,因此根据所起作用的不同步骤,MVC里有四种filter:
authorize filter:在所有filter之前生效,在action执行前生效
exception filter:在所有的action 或者filter抛出异常时生效
action filter:在执行应用了该filter的action时生效
result filter:在应用了该filter的action对应的result被执行前生效
上述四种filter每种都有MVC默认的实现类,但是很多时候我们都需要自定义一个新的filter,这里介绍一个常用场景:
有时我们需要给某个action提供较为复杂的authorization策略,我们需要在一个列表里的用户能访问:
public class CustomAuthAttribute : AuthorizeAttribute {
private string[] allowedUsers;
public CustomAuthAttribute(params string[] users) {
allowedUsers = users;
}
protected override bool AuthorizeCore(HttpContextBase httpContext) {
return httpContext.Request.IsAuthenticated &&
allowedUsers.Contains(httpContext.User.Identity.Name,
StringComparer.InvariantCultureIgnoreCase);
}
}
在使用时只需给CustomAuthAttribute构造函数提供一个string数组即可:
[CustomAuth(John","Tom","Lisa")
publica ActionResult Get()
{
//
}
14.让razor不编码你的内容
默认razor为了防止跨站攻击和其他目的会将你 action中返回的内容先编码,浏览器在展现时再解码。但是如果我们定义了一个helper method,我们希望直接展示其内容,我们可以采用MvcHtmlString类构造你的内容或者用Html.Raw()方法,这样你的helper method返回的内容就不会被razor编码而会直接展现出来。
15.给partial view传递model
大部分时候我们新建了一个partial view,我们并不会去实现一个action来对应这个partial view,所以向view传递数据就无法从用action直接构造含model参数viewresult的方法
但是我们可以采用直接从页面传递的方法:
@Html.Partial("MyStronglyTypedPartial", new [] {"Apples", "Mangoes", "Oranges"})