[ASP.NET MVC 小牛之路]09 - Controller 和 Action (1)

时间:2023-12-31 21:09:02

我们知道,在 MVC 中每个请求都会提交到 Controller 进行处理。Controller 是和请求密切相关的,它包含了对请求的逻辑处理,能对 Model 进行操作并选择 View 呈现给用户,对于业务和数据的逻辑代码以及接口和辅助类库等一般都不放到 Controller 中。

Controller 和 Action 的内容较多,我把它分成了两篇,也可能会分成三篇。本篇介绍 Controller 的实现、Controller 对状态数据的获取、ActionResult 和 Action 的数据传递,后续将介绍 Controller 工厂、Action Invoker 和暂时还没想好或正在学习的一些较高级的特性。

本文目录

继承 IController 接口

在本系列前面的文章中,我们添加的 Controller 都是一个继承自抽象类 System.Web.Mvc.Controller 的普通类(请注意:controller(或Controller) 和 Controller 类在本文是两个意思,请在阅读本文时根据上下文理解)。Controller 抽象类封装了很多很实用的功能,让开发人员不用自己去写那些重复烦琐的处理代码。

如果不使用封装的 Controller 抽象类,我们也可以通过实现 IController 接口来创建自己的 controller。IController 接口中只有一个 Exctute 方法:

public interface IController {
void Execute(RequestContext requestContext);
}

IController 接口在 System.Web.Mvc 命名空间下,一个结构非常简单的接口。

当请求送到一个实现了 IController 接口的 controller 类时(路由系统通过请求的URL能够找到controller),Execute 方法被调用。

下面我们创建一个空的 MVC 应用程序,在 Controllers 文件夹下添加一个实现了 IController 的类,并做一些简单的操作,如下:

using System.Web.Mvc;
using System.Web.Routing; namespace MvcApplication1.Controllers {
public class BasicController : IController { public void Execute(RequestContext requestContext) { string controller = (string)requestContext.RouteData.Values["controller"];
string action = (string)requestContext.RouteData.Values["action"]; requestContext.HttpContext.Response.Write(
string.Format("Controller: {0}, Action: {1}", controller, action));
}
}
}

运行应用程序,URL 定位到 /Basic/Index(你可以把 Index 改成其他任意片段名称),结果如下:

[ASP.NET MVC 小牛之路]09 - Controller 和 Action (1)

实现了 IController 的类,MVC就会辨识它为一个 controller 类,根据 controller 名把对应的请求交给这个类处理。

但对于一个稍稍复杂的应用程序,自己实现 IController 接口是要做很多工作的,我们很少会这么做。通过这我们更好地理解了 controller 的运行,controller 中一切对请求的处理都是从 Execute 方法开始的。

继承 Controller 抽象类

MVC 允许我们*地进行自定义和扩展,比如像上面讲的你可以实现 IController 接口来创建对各类请求的各种处理并生成结果。不喜欢 Action 方法或不关心 View,那么你可以自己动手写一个更好更快更优雅的 controller 来处理请求。但像前面说的,自己实现 IController 接口要做很多工作,最重要的是没有经过长期实践测试,代码的健壮性得不到保证, 一般不建议你这么做。MVC 框架的 System.Web.Mvc.Controller 类,提供了足够实用的特性来方便我们对请求的处理和返回结果。

继承 Controller 类的 controller 我们已经使用过很多次了,对它已经有一定的了解,它提供了如下几个关键的特性:

  • Action方法:一个 Controller,它的行为被分为多个方法,通常一个方法对应着一个请求,并且可以通过方法参数来取得请求传递过来的数据。
  • ActionResult:可以返回一个描述了 Action 方法执行结果的对象,这样的好处是想返回什么结果就指定对应的返回对象就行,不用关心怎么去执行并生成结果。
  • Filters:通过C#特性,对某一种行为的处理(比如授权和验证)进行封装,方便了在多个 Controller 和 Action 方法之间进行重用。

所以,如果你不是因为特殊的需求或闲得蛋疼,创建一个满足要求的 Controller 最好的途径是继承 Controller 抽象类。由于之前我们已经使用过多次,这里就不再进行具体的演示了,但我们还是来看一下它的代码结构。

在 Controllers 文件夹下添加一个 Controller,VS 已经把类的结构帮我们生成好了,如果你喜欢整洁,会习惯性地把不需要用到的引用删掉,代码如下:

using System.Web.Mvc;

namespace MvcApplication1.Controllers {

    public class DerivedController : Controller {

        public ActionResult Index() {
ViewBag.Message = "Hello from the DerivedController Index method";
return View("MyView");
}
}
}

我们可以查看 Controller 抽象类的定义,发现它是继承 ControllerBase 类的,在 ControllerBase 类中实现了 IController 接口的 Execute 方法,这个方法是MVC对请求进行处理的各个组件的入口,其中包括通过路由系统找到 Action 方法并调用。

Controller 类内部使用 Razor 视图系统来呈现 View,这里通过 View 方法,指定 View 的名称参数来告诉 MVC 选择 MyView 视图来返回给用户结果。

在 Controller 中获取状态数据

我们经常需要访问客户端提交过来的数据,比如 QueryString 值、表单值和通过路由系统来自 URL 的参数值,这些值都可称为状态数据。下面是 Controller 中获取状态数据的三个主要来源:

  • 一系列的上下文对象。
  • 传递给 Action 方法的参数。
  • 显式的调用框架的模型绑定(Model Binding)特性。

从上下文对象中获取状态数据

获取状态数据最直接的方法就是从上下文对象中提取。当你创建了一个继承自 Controller 类的 Controller 时,可以通过一系列的属性可以方便的访问到和请求相关的数据,这些属性包括 Request、Response、RouteData、HttpContext 和 Server,每一个都提供了请求相关的不同类型的信息。下面列出了最常的上下文对象:

[ASP.NET MVC 小牛之路]09 - Controller 和 Action (1)

在 Action 方法中可以使用任意上下文对象来获取请求相关的信息,如下面在 Action 方法中所演示的:

...
public ActionResult RenameProduct() {
//访问不同的上下文对象
string userName = User.Identity.Name;
string serverName = Server.MachineName;
string clientIP = Request.UserHostAddress;
DateTime dateStamp = HttpContext.Timestamp;
AuditRequest(userName, serverName, clientIP, dateStamp, "Renaming product"); //从POST请求提交的表单中获取数据
string oldProductName = Request.Form["OldName"];
string newProductName = Request.Form["NewName"];
bool result = AttemptProductRename(oldProductName, newProductName); ViewData["RenameResult"] = result;
return View("ProductRenamed");
}
...

这些上下对象不用特意去记,用的时候,你可以通过VS的智能提示来了解这些上下文对象。

使用 Action 方法参数获取状态数据

在本系列的前面的文章中,我们已经知识如何通过 Action 参数来接收数据,这种方法和上面的从上下文对象中获取相比,它更为简洁明了。比如,我们有下面这样一个使用上下文对象的 Action 方法:

public ActionResult ShowWeatherForecast() {
string city = (string)RouteData.Values["city"];
DateTime forDate = DateTime.Parse(Request.Form["forDate"]);
// do something ...
return View();
}

我们可以像下面这样使用 Action 方法参数来重写它:

public ActionResult ShowWeatherForecast(string city, DateTime forDate) {
// do something ...
return View();
}

它不仅易读性强,也方便进行单元测试。

Action 方法的参数不允许使用 ref 和 out 参数,这是没有意义的。

MVC 框架通过检查上下文对象来为 Action 方法的参数提供值,它的名称是不区分大小写的,比如 Action 方法的 city 参数的值可以是通过 Request.Form["City"] 来获取的。

理解 Action 方法的参数是如何被赋值的

Controller 类通过 MVC 框架的 value provider 和 model binder 组件来为 Action 方法获取参数的值。

value provider 提供了一系列Controller中可以访问到的值,在内部它通过从 Request.Form、Request.QueryString、Request.Files 和 RouteData.Values 等上下文对象中提取数据(键值集合),然后把数据传递给 model binder,model binder 试图将这些数据与Action方法的参数进行匹配。默认的 model binder 可以创建和赋值给任何.NET类型对象参数(即 Action 方法的参数),包括集合和自定义的类型。

在这不对 model binder 进行介绍,我将在本系列的后续博文中对其进行专门的介绍。

理解 ActionResult

ActionResult 是描述 Action 方法执行结果的对象,它的好处是想返回什么结果就指定对应的返回对象就行,不用关心如何使用Response对象来组织和生成结果。ActionResult 是一个命令模式的例子,这种模式通过存储和传递对象来描述操作。

当 MVC 框架从 Action 方法中接收到一个 ActionResult 对象,它调用这个对象的 ExecuteResult 方法,其内部是通过 Response 对象来返回我们想要的输出结果。

为了更好的理解,我们通过继承 ActionResult 类来自定义一个 ActionResult。在MVC工程中添加一个Infrastructure文件夹,在里面创建一个名为 CustomRedirectResult 的类文件,代码如下:

using System.Web.Mvc;

namespace MvcApplication1.Infrastructure {

    public class CustomRedirectResult : ActionResult {

        public string Url { get; set; }

        public override void ExecuteResult(ControllerContext context) {
string fullUrl = UrlHelper.GenerateContentUrl(Url, context.HttpContext);
context.HttpContext.Response.Redirect(fullUrl);
}
}
}

当我们创建一个 CustomRedirectResult 类的实例时,我们可以传递想要跳转的 URL。当 Action 方法执行结束时,MVC 框架调用 ExecuteResult 方法,ExecuteResult 方法通过 ControllerContext 对象获得 Response 对象,然后调用 Redirect 方法。

下面我们在 Controller 中使用自定义的 CustomRedirectResult:

public class DerivedController : Controller {
...
public ActionResult ProduceOutput() {
if (Server.MachineName == "WL-PC") {
return new CustomRedirectResult { Url = "/Basic/Index" };
}
else {
Response.Write("Controller: Derived, Action: ProduceOutput");
return null;
}
}
}

运行后我们看到如下结果:

[ASP.NET MVC 小牛之路]09 - Controller 和 Action (1)

当运行在本机(WL-PC)时直接重定向到了指定的/Basic/Index。

上面我们通过自定义 CustomRedirectResult 来实现重定向,我们可以用 MVC 框架提供的方法,如下:

...
public ActionResult ProduceOutput() {
return new RedirectResult("/Basic/Index");
}

为了使用方便,Controller 类中为大部分类型的 ActionResult 提供简便的方法,如上面的可像下面这样简写:

...
public ActionResult ProduceOutput() {
return Redirect("/Basic/Index");
}

MVC框架包含了许多 ActionResult 类型,这些类型都继承自 ActionResult 类,大部分在 Controller 类中都有简便的方法,下面列举了一些:

[ASP.NET MVC 小牛之路]09 - Controller 和 Action (1)

除了该表列出来的,还有ContentResultFileResultJsonResult JavaScriptResult。具体每种ActionResult类型的用法这里就不讲了,大家可以看看蒋老师的了解ASP.NET MVC几种ActionResult的本质系列的文章。

几种从 Action 传递数据到 View 的方式

我们经常需要在 Action 方法中传递数据到一个 View 中,MVC 框架为此提供了一些很方便的操作。下面简单简介几种常用的方式。

View Model 对象

通过 View Model 对象传递数据给View,这是最常用的一种,在 Acton 方法执行结束时通过 View 方法传递 View Model 对象给 View,如下代码所示:

...
public ViewResult Index() {
DateTime date = DateTime.Now;
return View(date);
}

在 View 中我们通过 Model 属性来使用传递过来的 View Model 对象,如下:

@model DateTime 

@{
ViewBag.Title = "Index";
} <h2>Index</h2>
The day is: @Model.DayOfWeek

在 Razor 视图引擎中,@model 的作用是声明 odel 属性的类型,省去了类型转换的麻烦,而 @Model 是V iew Model 对象的引用。

ViewBag、ViewData 和 TempData 属性

ViewBag、ViewData 和 TempData 都是 Controller 和 View 中能访问到的属性,都是用来存储小量的数据,他们的区别如下:

  • ViewBag,是一个动态(dynamic)的弱类型,在程序运行的时候解析,是 MVC3 中新增的特性,只在当前View有效。
  • ViewData,是一个字典集合,也是只在当前View有效,性能比 ViewBag 高,但是使用的时候需要类型转换。
  • TempData,也是字典集合,一般用于两个请求之间临时缓存内容或页面间传递消息,保存在 Session 中,使用完以后则从 Session 中被清除。

下面是三者使用的例子,先在 Controller 中分别用三者存储小数据:

public class DerivedController : Controller {

    public ActionResult Index() {
ViewBag.DayOfWeek = DateTime.Now.DayOfWeek;
ViewData["DayOfMonth"] = DateTime.Now.Day;
return View();
} public ActionResult ProduceOutput() {
TempData["Message"] = "Warning message from Derived Controller.";
return Redirect("/Home/Index");
}
}

在 Views/Derived 目录下的 Index.cshtml 中,取出 ViewBag 和 ViewData 中的存储的数据:

...
Day of week from ViewBag: @ViewBag.DayOfWeek
<p />
Day of month from ViewData: @ViewData["DayOfMonth"]

在 Views/Home 目录下的 Index.cshtml 中,取 TempData 中的数据如下:

...
@TempData["Message"]

当请求 /Derived/ProduceOutput 时,ProduceOutput 方法将一条消息存到 TempData 中,并跳转到 /Home/Index。

下面是分别是将URL定位到 /Derived/Index 和 /Derived/ProduceOutput 时的结果:

[ASP.NET MVC 小牛之路]09 - Controller 和 Action (1)  [ASP.NET MVC 小牛之路]09 - Controller 和 Action (1)

一般在当前 View 中使用 ViewBag 或 ViewData,在两个请求之间传递临时数据用 TempData。由于 TempData 被使用后即被释放,所以如果要二次使用 TempData 中的数据就需要将其存到其他变量中。


参考:《Pro ASP.NET MVC 4 4th Edition》