首先声明,在下英语水平不甚熟练(虽6级已过),因需要ASP.NET MVC 2.0 控制器知识,借助Lingoes工具,翻了控制器一章,只为英语水平在在下之下的同志做参考,但鼓励看原版。
每当一个请求进入到你的ASP.NET MVC应用程序,它是与控制器进行处理。控制器是老板:它可以做任何事情就像它服务那条请求。它可以向基本模型层或数据库发出任意指令,它还可以选择为访问者渲染任意的视图。它是一个可处理用户逻辑请求的.NET类。
概览
概括一下控制器在MVC架构中扮演一个什么样的角色。MVC要做的就是使事情变简单和使关注点分离。特别是,MVC的目标就是要保持下面三个主要责任区域的分离:
l 业务或域逻辑和数据存储(model)
l 应用逻辑(controller)
l 表现逻辑(vie)
这个特殊的安排之所以被选择是因为在现在的各种各样的应用中它工作的很好。
控制器负责应用逻辑,包括接收用户输入,发出命令从域模型取回数据,使用户在不同的UI间移动。你可以把控制器认为是Web和你的域模型之间的桥梁,因为你应用程序的所有目标就是完成用户和域模型的相互联系。
域模型逻辑---进程和规则代表你的业务---是一个关注点分离的过程,所以不要把模型逻辑混入到控制器中。如果你这样做了,你将失去跟踪哪些代码是你业务模型真正的体现,而那才是你现在web应用程序的特色。在小程序中你可能不会这样做,但是随着复杂度的提高,关注点分离是解决问题的关键。
与ASP.NET Web Forms的比较
ASP.NET MVC的控制器和Web Forms的传统ASPX页有一些相似点。例如,都是完成与用户的交互,都管理应用逻辑。但在另一方面,它们又是完全不同的—例:
你可以使Web Forms ASPX页和它的代码—隐藏类—分离,它们只是在一起工作,一起实现应用逻辑和表现逻辑 (如:数据绑定),都只是关注每一个独立的按钮或标签。但是ASP.NET MVC控制器不一样:它们是和UI(如视图)完全分离。是一个用户交互的抽象体现,只负表管理应用逻辑。这种抽象帮助你使控制器变得简单,这样你的应用逻辑就变得更容易理解和保持分离。
Web Forms ASPX页(和它的隐藏类)与UI是一对一的。在ASP.NET MVC,一个控制器不是单单对应一个特殊的视图,所以它可以一种请求返回不同的UI—这取决于你的应用逻辑。
当然,MVC框架真正要做的是要把你的工作做的更好和建立伟大的软件。
所有的Controller继承自IController
在ASP.NET MVC,控制器是.NET类。对它们的关键要求就是它们必须继承自IController接囗。这不需多问---这里有所有的接囗定义:
Public interface IController
{
Void Execute(RequestContext requestContext);
}
“Hello World”控制器的例子便是:
Public class HelloWorldController : IController
{
Public void Execute(RequestContext requestContext)
{
requestContext.HttpContext.Response.Write(“Hello,World”);
}
}
如果你的路由配置是{controller/{action/{id}}你就可以这样调用
http://localhost:59071/HelloWorld
控制器基类
在实际应用中,你很少会直继承自IController或写一个Execute()方法。因为MVC框架为控制器提供了一个标准基类,System.Web.Mvc.Controller(继承自IController)。它比原始的IController强大的多。
l 动作方法:你控制器的行为被分配到多种方法中(而不是单一的Execute()方法)。每一个动作都是一个不同的URL,它们被请求的参数调用。
l 动作结果:你可以选择返回一个对象来描述动作执行结果(如:渲染一个视图,或指定不同的URL或动作方法),这将由你说了算。执行和指定结果的分离使单元测试大大简化。
l 过滤:你可以封装可重用的形为(如:证或输出缓存)作为过滤,然后可以在控制器或动作方法通过在源代码上加入[Attribute]进行标记。
[OutputCache(Duration=600, VaryByParam=”*”)]
Public class DemoController : Controller
{
Public ViewResult ShowGreeting()
{
ViewData[“Greeting”] = “Hello,World”;
Return View(“MyView”);
}
}
l 由于它是派生于标准控制器基类,所有的动作方法是public,所以可心以从Web调用。对应动作方法的每个URL由你的路由配置确定。按照默认的路由配置,你可以通过/Demo/ShowGreeting来调用ShowGreeting()。
l ShowGreeting()通过调用View()生成并返回一个动作结果对象。这个特定的ViewResult对象命令框架去渲染一个存储在/Views/Demo/MyView.aspx视图,并提供在ViewData集合中的值。视图将会结合这些值,并提交到模板中,产生和指供一个完成的HTML而完。
l 它拥有一个filter属性,[OutputCache]。这个缓存将会在指定的期限内重用控制器的输出(在这个例子中,是600秒)。由于这个属性只是对应于DeomController它自已,它将会对属于它的所有动作起作用。当然,你也可以把它应用到单独的动作方法中。
除去System.Web.Mvc.Controller,MVC框架还为控制器提供了另一个标准的基类,System.Web.Mvc.AsyncController,从System.Web.Mvc.Controller派生,从这个子类继承你可以使用异步请求。在下一章节有介绍。
接收输入
控制器经常性的需要访问传入的数据,如查询字符串值,表单值,还有通过路由系统URL传入的参数。这是三种传入数据的主要方式。你可以从一个上下文对象提取它,还可以接收做为参数传入到你动作中的数据,你还可以直接调用框架的模型绑定功能。
从上下文对象中获取数据
最直接的获取数据的方式就是直接取出。你的控制器是从框架的控制器基类继承来的,你就可以使用它的生,包括请求,回应,RouteData, HttpContext,和 Server,联系GET和POST数据,HTTP头,cookie信息等框架知道的请求。
一个动作方法可以获取从不同的资源获取的数扰-例如:
public ActionResult RenameProduct()
{
// Access various properties from context objects
string userName = User.Identity.Name;
string serverName = Server.MachineName;
string clientIP = Request.UserHostAddress;
DateTime dateStamp = HttpContext.Timestamp;
AuditRequest(userName, serverName, clientIP, dateStamp, "Renaming product");
// Retrieve posted data from Request.Form
string oldProductName = Request.Form["OldName"];
string newProductName = Request.Form["NewName"];
bool result = AttemptProductRename(oldProductName, newProductName);
ViewData["RenameResult"] = result;
return View("ProductRenamed");
}
使用动作方法参数
我们可能这样写到
Public ActionResult ShowWeatherForecase()
{
String city = RouteData.Values[“city”];
DateTime forDate = DateTime.Parset(Request.Form[“forDate”]);
// …..other code…..
}
可以直接这样写
Public ActionResult ShowWeatherForcast(string city, DateTime forDate)
{
//….other code…..
}
为了从你的参数中获取值,MVC框架扫描几个上下文对象,包括Request.QueryString, Request.Form, 和RouteData.Values,去发现匹配的键值对。由于键是大小写不敏感,所以参数city可以写入Request.Form[“City”](概括一下,RouteData.Values是由路由系统生成传入的一系列的参数提取结果,加上别的默认的路由参数。)
可选和强制参数
如果框架找不到指定参数的匹配,它将尝试为参个参数提供null。对于引用/空类型是可以的,但是对值类型(比如int或DateTime)你将得到一个异常。这有一个别的办法来思考它:
l 值类型参数天性是强制的。把它们设置为可选的,或为它们指定一个默认值,再或把它们改变为可空类型(如 int? DateTime?)。这样当没有值时框架将可以通过空值。
l 引用类型天性是可选的。把它们设置为强制的(例:确保他们非空值传递),你必需在动作方法头部添加一些代码不拒绝空值。例,如果值等于空,就引发一个异常。
指定默认参数值
在前一节中,你学会了在请求中没有提供值时如何配置路由系统为一个参数设置默认值。但是你可能不想只为了配置一个默认值改变整个路由条目。现在提供一个简单的方式:你可以使用[DefaultValue]属性在动作方法参数:
Public ActionResult Search(string query, [DefaultValue(1)] int page)
{
//…
}
现在,如果没有值被发现,由会把值1赋予它,而不是引发一个异常。(如果传入一个非数字值,如aaa这样的字符串,将也会被赋予1)。
值得注意的是,动作方法不允许有out或ref参数。如果那样做了,ASP.NET MVC丢出一个异常。
在动作方法中手动的调用绑定模型
在数据输入场景中,接收一个form值并把他们折分填入到一个模型对象中。
Public ActionResult SubmitEditedProduct()
{
Product product = LoadProductByID(int.Parse(Request.Form[“ProductID”]));
Product.Name = Request.Form[“Name”];
Product.Description = Request.Form[“Description”];
Product.Price = double.Parse(Request.Form[“Price”]);
CommitChanges(product);
Return RedirectToAction(“List”);
}
大多数这样的代码是烦人和平庸的。幸运的是,就像你使用模型绑定接收完全填充做为动作对象的参数,你也可以调用模型绑定明确的更新你已经创建的任意模型对象。
Public ActionResult SubmitEditedProduct(int productID)
{
Produt product = LoadProdutByID(productID);
UpdateModel(product);
CommitChanges(product);
Return RedirectToAction(“List”);
}
下面使用隐事模型绑定方式
Public ActionResult SubmitEditedProduct(Product produt)
{
CommitChanges(product);
Return RedirectToAction(“List”);
}
生成输出
在控制器接收到一个请求并处理它心伯,它通常都要为用户生成一些反应。
l 通过渲染一个视图返回HTML
l 发生一个HTTP重定向
l 向输出流写入一些别的数据(可能是文本,如XML或JSON,还有可能是二进制文件)
理解ActionResult概念
如果你祼Icontroller类(例,你直接从Icontroller继承,并且没有从System.Web.Mvc.Controller派生),这样你就可以以喜欢的任何方式通过controllerContext.Httptext.Response生成一个回应。
Public class BareMetalController : Icontroller
{
Public void Execute(RequestContext requestContext)
{
requestContext.HttpContext.Response.Write(“I <b>love</b> HTML”);
//or
requestContext.HttpContext.Response.Redirect("http://www.google.com.hk”);
}
}
它简易并且工作。你可以做精确的事情从Controller基类继承。
Public calss SimpleController : Controller
{
Public void MyActionMethod()
{
Response.Write(“hello, Allan”);
}
}
ASP.NET MVC内置ActionResult类型
通过渲染视图返回HTML
很多动作方法需要为浏览器返回一些HTML。为了实现它,你可以渲染一个视图模板,意味着你将返回一个ViewResult类型
Public class AdiminController : Controller
{
Public ActionResult Index()
{
return View(“Homepage”);
}
}
调用View()将会生成一个ViewResult对象。当执行这个结果,MVC框架的内置引擎,WebFormViewEngine,将会在合适的地方找到视图模板。
通过路径渲染视图
Public class AdminController : Controller
{
Public ViewResult Index()
{
Return View(“~/path/to/some/view.aspx”);
}
}
传递一个ViewData字典和模型对象
你知道,控制器和视图完全是不同,独立的东西。MVC 框架在应用逻辑和表现逻辑做了强制的分离。控制器向它的视图提供数据,但是视图不能直接与控制器对话。
控制器向视图传递值的原理就是ViewData。控制器基类里有一个叫做ViewData的属性,是ViewDataDictionary类型。
把ViewData看做松散类型字典
使用ViewData的第一个方式就是字典(如键值对)。
Public class Person
{
Public string Name{get;set;}
Public int Age{get;set;}
}
Pubic ViewResult ShowPerson()
{
Person someguy = new Person{Name=”Steve”, Age = 108};
ViewData[“person”] = someguy;
ViewData[“message”] = “Hello”;
Return View();
}
你使用键值对填充控制器的ViewData。然后你渲染一个视图。框架将会传递这个ViewData集,所以,你可以在视图模板中这样使用:
<%=ViewData[“message”]%>, world!
Ther person’s name is <%=((Person)ViewData[“person”]).Name %>
The person’s age is <%=((Person)ViewData[“person”]).Age %>
使用ViewData这个松散类型的一个弱点就是不会获得智能提示。
向ViewData.Model传递强类型对象
ViewDataDictionary有一个叫做Model的特殊的对象。你可以为它赋予任意的.NET对象。
Public ViewResult ShowPersonDetails()
{
Person someguy = new Person{Name=”Steve”, Age=108};
Return View(someguy);
}
这样在视图中就可以这样使用:
The person’s name is <%=((Person)Model).Name%>
The person’s age is <%=((Person)Model).Age%>
作为一个C#程序员,毫无疑问你将感激强类型的好处。但是缺点是,你被限制只能向ViewData.Model传递一个对象。如果要传递几个强类型的对象,你可以把它们包装在一起,做为一个类:
Public class ShowPersonViewModel
{
Public Person Person{get;set;}
Public string StatusMessage{get;set;}
Public int CurrentPageNumber{get;set;}
}
然后就可以使用这个类型的强类型视图。
比较两种方式
ViewDataDictonary给了你最大的可定制性在同一时间去使用松散类型和强类型。这可以使在视图中避开使用模型类。
Public ViewResult ShowPersonDetails()
{
Person someguy = new Person{Name=”Steve”, Age = 108};
ViewData[“message”] = “Hello”;
ViewData[“currentPageNumber”] = 6;
Return View(someguy); //隐式把’someguy’赋予ViewData.Model
}
然后在视图模板中这样使用
<%= ViewData["message"] %>, world!
The person's name is <%= Model.Name %>
The person's age is <%= Model.Age %>
You're on page <%=ViewData["currentPageNumber"] %>
传递一个动态对象成为ViewData.Model (.NET 4)
执行重定向
经常,你并不需要一个动作方法返回HTML。你可能想把控制权提交给别的动作方法。
试想像一种情况,当你执行了一些SaveRecord()动作方法把一些数据保存到数据库,然后显示记录列表(一个叫做Index()的动作方法),你将有三个选择:
l 把已存在于Index()中的代码复制到SaveRecord()中。(…狗屎)
l 从你的SaveRecord()方法中,真接调用Index()方法。
public ViewResult SaveRecord(int itemID, string newname)
{
DomainModel.SaveUpdateRecord(itemID, newname);
return Index();
}
这减少了代码重复,但是,这将引发一些负作用。例如:如果Index()方法试图去渲染一个默认的视图,它将会渲染SaveRecord动作的默认视图,因为这时RouteData.Values[“acton”]仍然等于SaveRecord
l 从SaveRecord()方法直接定向到Index动作
public RedirectToRouteResult SaveRecord(int itemID, string newName)
{
DomainModel.SaveUpdateRecord(itemID, newName);
return RedirectToAction(“Index”);
}
这将引发一个到Index动作的HTTP 302 重定向。导致一个到/ControllerName/Index的GET请求,地址栏中改变了URL。
在头两个方式中,用户的浏览器只把它当做单一的请求,在地址栏中只会保持/ControllerName/SaveRecord。用户如果按F5刷新,将会重新提交。
重定向到不同的动作方法
就像你看到的那样,下面重定向到不同的动作方法
Return RedirectToAction(“someaction”);
这个返回一个RedirectToResult 对象,根据你的路由系统生成一个目标URL。
如果你不指定一个控制器(就像前面的一样),就会被理解为“在同一个控制器”,如果你原意,还可以加上一些参数去影响URL的生成:
Return RedirectToAction(“Index”, “Products”, new{color=”Red”, page=2});
重定向到不同的URL
如果你想重定向到一个文本URL(不是使用生成的外传URL),则通过调用一个Redirect()返回一个RedirectResult对象
Return Redirect(“http://www.example.com”);
你可以传递一个相对应用程序的虚拟目录
Return Redirect(“~/Some/Url/In/My/Application”);
使用 TempData 在重定向中保存数据
重定向导致浏览器提交一个新的HTTP请求。在新的请求中,你将不能再使用一些原来创建暂时对象。如果在重定向中你想要保存值怎么办呢?你应该使用TempData。
TempData是MVC中的一个新定义,和Web Forms完全不一样。就像Session集合,它对现在的用户保存任意.NET对象。但是不像Session,当你读取到这个值后,它自动的删除。这非常适合短数在重定向请求中的保存。
public RedirectToRouteResult SaveRecord(int itemId, string newName)
{
// Get the domain model to save the data
DomainModel.SaveUpdatedRecord(itemId, newName);
// Now redirect to the grid of all items, putting a status message in TempData
TempData["message"] = "Your changes to " + newName + " have been saved";
return RedirectToAction("Index");
}
在随后的请求,在Index的视图中,渲染那个值
<% if(TempData["message"] != null) { %>
<div class="StatusMessage"><%: TempData["message"] %></div>
<% } %>
注:只能显示一次,然后被自动的从TempData中弹出
在TempData之间,传统的方法是把这个标记信息写入到query string值里。然而,TempData是更好的方式:它不是大块的,丑陋的URL,它还可以存储任意的.NET对象。
TempData在哪里存储它的值
默认情况下,TempData的数据存储在Session,所以如果你想使用TempData,就不能禁用Session,
控制TempData项的生命周期
TempData注意着你读取或写入它的键,所以当你读取TempData[“myKey”],它将把myKey写入keys列表,然后它就在当前的请求中失效。如果在一个请求中你没有读取它,那么他直到第一次被读取才消失。
如查你想保持TempData[“message”]的值,而使它不被弹出,你可以使用TempData的Keep()方法
TempData.Keep(“message”);
返回原文本数据
除去HTML,你的web应用程序或许还想得到许多别的格式的文本。
l XML
l RSS and ATOM
l JSON
l CSV
l Plain text
ASP.NET MVC有一个特殊的,内置的生成JSON数据的支持。但是对别的格式,你可以使用通用ContentResult动作结果类型。这里有三点需要明确说明:
l 数据本身作为一个字符串
l 传送内容类型头(例:面向XML的text/xml,面向CSV的text/csv,面向RSS的application/rss+xml)浏览器依靠它做为输出的依据。
l 文本编码格式对象以System.Text.Encoding指定。它描述怎样转换一个以网络传输的.NET string实例为一个字节序列。如UTF-8,ASCII,ISO-8859-1。如果你不指定一个值,那框架将会尝试去选择一个浏览器声明支持的编码。
一个ContentResult可使你指定所有上面指到的类型
Public ActionResult GiveMePlainText()
{
Return Content(“This is plain text”, “text/plain”);
//”text/plain”可以MediaTypeNames.Text.Plain代替
}
如果你只是返回一个text而不关心别的content-type头,你可以使用动作方法的简写形式-string 。框架将会把它转换为ContentResult
Public string GiveMePlainText()
{
Return “This is plain text”;
}
实际上,如果你的动作方法返回一个不是派生于ActionResult的对象,MVC 框架将会使你的动作方法返回一个字符串(使用Convert.ToString(yourReturnValue, CulturreInfo.InvariantCulture)),然后把这个值构造成一个ContentResult。这可以在某些Ajax场景下得心应手。例如,如果你只是想返回一个GUID或别的标记到浏览器。请注意,它不会使用任何contentType参数,这样默认的(text/html)将会被使用。
返回JSON数据
Javascript Object Notation(JSON)是一个常用,轻量级,基于文本的数据格式。它可以描述任意的层级结构。它就是JavaScript代码,所以他可以被任意浏览器支持(比XML更简易)。
使用Ajax从服务器向浏览器传递数据经常被用到(包括集合和所有的图对象)。ASP.NET MVC 有一个内置的JsonResult类来序列化你的.NET对象。你可以使用调用Json() 来生成一个JsonResult。
Class CityData{public string city; public int temperature;}
[HttpPost]
Public JsonResult WeatherData()
{
Var citiesArray = new[] {
New CityArray {city=”London”, temperature=68},
New CityArray {city=” Hong Kong”, temperature=68}
};
Return Json(citiesArray);
}
它将把citiesArray转换为JSON格式
[{“city”:”London”, “temperature”:68},{“city”:”Hong Kong”, “temperature”:84}]
提示:因为安全性的原因,JsonResult默认不会为GET请求传递任何数据,因为那可能是来自跨站请求。这个ASP.NET MVC 1.0 的默认情况是不一样的。
返回JavaScript命令
动作方法应付Ajax请求就和应付一般请求一样容易。你刚才看到,一个动作方法使用可以返回任意的JSON 数据结构,然后客户端就可以对这些数据为所欲为。
有时候,你可能想要直接接受一个Ajax请求指示浏览器执行一段JavaScript代码。你可以使用JavaScript()方法。返回一个JavaScriptResult类型值。
Public JavaScriptResult SayHello()
{
Return JavaScript(“alert(‘hello, world’);”);
}
要想使它工作,你的视图或母版页必须引用MicrosoftAjax.js和MicrosoftMvcAjax.js。
<%=Ajax.ActionLink(“Click me”, “SayHello”, null) %>
和Html.ActionLink()不同的是,Ajax.ActionLink()不是页面全部刷新,它是是请求了一个异步请求。当用户点击Ajax链接,就执行从服务器取得的JavaScript代码。
相对于使用JavaScript显示友好的提示信息,你更多的是使用它来更新HTML DOM。例,当你的动作方法删除文件数据库的一个记录,你或许想让你的浏览器把对应的记录从list删除。
返回文件和二进制数据
你向浏览器发送一个文件怎么样?你或许想要浏览器显示一个保存或打开对话框,例如发送一个ZIP文件,或许你想要浏览器直接显示内容,就像原来做过的从数据库中读取并显示一个图片。
FileResult是所有向浏览器传递动作结果的抽象基类。ASP.NET MVC提供了三种内置的实体子类:
l FilePathResult传递一个直接从服务器文件系统读取的文件
l FileContentResult传递一个内存中字节数组(byte[])的内容
l FileStreamResult传递一个你在别的地方已经打开的System.IO.Stream对象的内容。
通常情况下,你可以忘掉你使用的是哪种FileResult子类,因为所有的这三种都可以调用File()重载方法来实例化。只用挑选File()的哪种重载。
直接从磁盘传递文件
Public FilePathResult DownloadReport()
{
String filename=@”c:\files\somefile.pdf”;
Return File(filename, “application/pdf”, “AnnualReport.pdf”);
}
这将使浏览器找开一个保存找开窗囗。