以下内容摘自:http://www.cnblogs.com/r01cn/archive/2012/02/19/2358753.html
感谢作者的翻译,这里只是译文。原书名:Pro ASP.NET MVC 3 Framework
第十四章 控制器可扩展性 PART1
在本章中,我们打算向你演示MVC用控制器进行工作的一些高级特性。我们将从考查引导动作方法执行过程的请求处理管道开始,并演示你可以对这种过程进行控制的不同方式。
本章第二部分演示两种类型的应用程序控制器,即无会话控制器和异步控制器。这些可以用来增强服务器的能力。我们演示如何生成和使用这些控制器类型,并解释你应该什么时候考虑在MVC应用程序中使用它们。
请求处理管道组件
图14-1显示了组件之间的基本控制流程。图中的有些元素你此刻应该是熟悉的。我们第11章讨论了路由系统,第12章我们描述了Controller类和动作方法之间的关系。
![图14-1](https://i-blog.csdnimg.cn/blog_migrate/8acea22320a3b64ab66cf64f89c6126e.png)
图14-1. 调用一个动作方法
本章第一部分我们关注的是控制器工厂(Controller Factory)和动作调用器(Action Invoker)。这些组件的名称暗示了它们的目的。控制器工厂负责生成对一个请求进行服务的控制器实例,动作调用器负责查找并调用控制器类中的动作方法。MVC框架含有这两个组件的默认实现,我们将向你演示如何配置它们,以控制它们的行为。我们也将向你演示如何完全替换这些组件而使用自定义逻辑。
生成一个控制器工厂
像MVC框架的大部分情况一样,要理解控制器工厂如何进行工作,最好的办法是生成一个自定义实现。我们不建议你在实际项目中这样做,因为,通过扩展内建的工厂,可以更容易地生成自定义行为。但这是演示MVC框架如何生成控制器实例的一种很好的办法。
定义一个自定义控制器工厂
控制器工厂是由IControllerFactory接口定义的,如清单14-1所示。
清单14-1. IControllerFactory接口
using System.Web.Routing;
using System.Web.SessionState;
public interface IControllerFactory {
IController CreateController(RequestContext requestContext, string controllerName);
SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext,
string controllerName);
void ReleaseController(IController controller);
}
}
这个接口中最重要的方法是CreateController,当MVC框架需要一个控制器对一个请求进行服务时,框架便会调用这个方法。该方法的参数是一个RequestContext对象,它允许该工厂检测该请求的细节;和一个字符串,它包含了通过路由的URL所得到的controller值。
我们不建议这样生成自定义控制器的原因之一是,查找web应用程序中的控制器类、并对它们实例化是复杂的。为了使我们的演示简单,我们将仅支持两个控制器,叫做FirstController和SecondController。作为CustomControllerFactory类的一部分,清单14-2演示了这个方法的实现。
清单14-2. CustomControllerFactory类
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.SessionState;
using ControllerExtensibility.Controllers;
namespace ControllerExtensibility.Infrastructure {
public class CustomControllerFactory : IControllerFactory {
public IController CreateController(RequestContext requestContext,
string controllerName) {
Type targetType = null;
switch (controllerName) {
case " Home ":
requestContext.RouteData.Values[ " controller "] = " First ";
targetType = typeof(FirstController);
break;
case " First ":
targetType = typeof(FirstController);
break;
case " Second ":
targetType = typeof(SecondController);
break;
}
return targetType == null ?
null : (IController)Activator.CreateInstance(targetType);
}
public SessionStateBehavior GetControllerSessionBehavior(
RequestContext requestContext, string controllerName) {
return SessionStateBehavior.Default;
}
public void ReleaseController(IController controller) {
IDisposable disposable = controller as IDisposable;
if (disposable != null) {
disposable.Dispose();
}
}
}
}
CreateController方法的目的,是生成一个能够对请求进行处理的控制器实例。控制器工厂如何做这种事是完全公开的。存在一些你在本书的例子中一直看到的约定,因为,这是默认控制器工厂的工作方式。完成我们这个自定义工厂之后,我们将讨论默认控制器工厂。对于清单14-2的工厂,我们忽略了所有这些约定,而是实现了我们自己的逻辑。这是所做的一件相当奇怪的事情,但它确实演示了MVC框架提供的充分的灵活性。
如果我们接收到controller值是First或Second的一个请求,我们就会生成FirstController或SecondController类的一个新实例。我们用System.Activator类生成实例,它让我们根据对象的类型生成对象的实例,像这样:
如果我们收到controller值是Home的一个请求,我们把这个请求映射到FirstController类。同样,这是要做的一件奇怪的事情,但它表明请求与控制器之间的映射是控制器工厂自身的职责。我们无论如何不能把它说成是请求到视图的映射。
MVC框架基于路由数据中的controller值来选择视图,而不是基于控制器类的名字。例如,如果我们想把对Home控制器的请求映射到First控制器的一个实例,我们也需要修改请求中的controller值,像这样:
因此,不仅控制器工厂要独自地负责把请求匹配到控制器,而且它可以对请求进行修改,以改变请求处理管道中后继步骤的行为。这是MVC框架相当有力的要素和关键组件。
IControllerFactory接口中的另外两个方法是:
· GetControllerSessionBehavior方法由MVC框架用来确定是否应该为控制器维护会话数据。我们将在本章稍后的“使用无会话控制器”小节中回到这一论题。
· ReleaseController方法,当不再需要CreateController方法生成的控制器对象时,调用这个方法。在我们的实现中,我们查看这个类是否实现IDisposable接口。如果是,我们调用Dispose方法以释放那些可以被释放的资源。
注册一个自定义控制器工厂
我们通过ControllerBuilder类来告诉MVC框架要使用我们的自定义控制器工厂,如清单14-3所示。
清单14-3. 注册一个自定义工厂
AreaRegistration.RegisterAllAreas();
ControllerBuilder.Current.SetControllerFactory( new CustomControllerFactory());
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
用内建的控制器工厂进行工作
对于大多数应用程序,内建的控制器工厂,名为DefaultControllerFactory,是完全足够的。当它从路由系统接收到一个请求时,这个工厂查找路由数据,以找到controller属性的值(这是我们在第11章所描述的),并试图在这个web应用程序中找到满足以下条件的一个类:
· 这个类必须是一个public类。
· 这个类必须是具体类(而不是抽象类)。
· 这个类必须没有泛型(generic)参数。
· 类名必须以Controller结尾。
· 这个类必须实现IController接口。
DefaultControllerFactory类维护着应用程序中这些类的一个列表,因此,每次一个请求到达时,它并不需要每次都执行一个搜索。如果找到一个合适的类,那么便用控制器激活器(controller activator)生成一个实例(我们将在马上要描述的“定制DefaultControllerFactory控制器的生成”小节中回到这一论题),控制器的工作便完成了。如果没有匹配的控制器,那么该请求便不能作进一步处理。
要注意DefaultControllerFactory类是如何遵循“约定优于配置”模式的。你不需要在配置文件中注册你的控制器,因为这个工厂会为你查找它们。你需要做的全部事情是,生成满足这个工厂查寻条件的类。
如果你想生成自定义控制器工厂的行为,你可以对默认工厂的设置进行配置,或重写它的一些方法。这样,你便能够建立有用的“约定优于配置”的行为,而不需要重新生成它。我们将在以下小节中向你演示定制控制器生成的不同方式。
安排命名空间优先级
在第11章中,我们向你演示了,在构建一条路由时,如何安排一个或多个命名空间的优先级。用它解决控制器的多义性问题,即,同名控制器类位于不同命名空间的情况。我们在第11章提到,这种信息(指命名空间优先级信息 — 译者注)被沿途传递给控制器工厂,而处理命名空间列表并对之排序的,正是这个DefaultControllerFactory。
如果你的应用程序有很多路由,指定全局命名空间优先级可能是更方便的,以使它们能用于你的所有路由。清单14-4演示了如何做这种事情。
清单14-4. 全局命名空间优先级
AreaRegistration.RegisterAllAreas();
ControllerBuilder.Current.DefaultNamespaces.Add( " MyControllerNamespace ");
ControllerBuilder.Current.DefaultNamespaces.Add( " MyProject.* ");
RegisterGlobalFilters(GlobalFilters.Filters);
RegisterRoutes(RouteTable.Routes);
}
我们用静态的ControllerBuilder.Current.DefaultNamespaces.Add方法来添加应该给予优先的命名空间。我们添加的命名空间的顺序并不暗示某种搜索顺序。所有默认命名空间都被用来搜索候选的控制器类,而且重复(指有多个相同的包含命名空间在内的控制器 — 译者注)仍然会引发异常,就像我们直接在路由定义中执行同样的事情那样。
■ Tip Global prioritization is overridden by route-specific prioritization. This means you can define a global policy, and then tailor individual routes as required.
提示:Global优先级会被路由优先级所重写(override)。这意味着你可以定义一个全局策略,然后在必要时裁剪个别路由(原文如下:Global prioritization is overridden by route-specific prioritization. This means you can define a global policy, and then tailor individual routes as required.) 。 详见第11章的为个别路由指定命名空间。
如果这个控制器工厂在已经指定的命名空间中找不到一个合适的控制器类,那么将检测应用程序的其余部分。注意,我们在清单14-4中以黑体显示的第二条语句中使用了星号(*)。这允许我们指定控制器工厂应该查找MyProject命名空间及其任意子命名空间。(警告:这看上去像正则表达式语法,但它却不是,你可以用*作为命名空间的结尾,但你在这里不能使用任何其它正则表达式语法。)
定制DefaultControllerFactory的控制器构建
定制DefaultControllerFactory类如何生成控制器对象有许多种方式。但到目前为止,对控制器工厂进行定制最通常的理由是为了添加对DI(依赖注入)的支持。做这件事有几种不同的办法,最适合的技术取决于你在应用程序的其它地方如何使用DI。
使用依赖解析器(Resolver)
DefaultControllerFactory类在依赖性解析器可用时,将用它来创建控制器。我们在第10章涉及了依赖解析器,并给你演示了我们的NinjectDependencyResolver类,它实现了IDependencyResolver接口,以提供Ninject的DI支持。
DefaultControllerFactory将调用IDependencyResolver.GetService方法来生成一个控制器实例,这给你解析并注入依赖提供了机会。
使用控制器激活器(Activator)
你也可以通过生成一个控制器激活器的办法,把DI引入到控制器中。通过实现IControllerActivator接口,你可以生成这个激活器,如清单14-5所示。
清单14-5. IControllerActivator接口
using System.Web.Routing;
public interface IControllerActivator {
IController Create(RequestContext requestContext, Type controllerType);
}
}
该接口含有一个方法,名为Create,它传递一个描述请求的RequestContext对象和一个指定应该实例化哪个控制器的类型(Type)。清单14-6演示了这个接口的一个简单实现。
清单14-6. 实现IControllorActivator接口
using System.Web.Mvc;
using ControllerExtensibility.Controllers;
namespace ControllerExtensibility.Infrastructure {
public class CustomControllerActivator : IControllerActivator {
public IController Create(System.Web.Routing.RequestContext requestContext,
Type controllerType) {
if (controllerType == typeof(FirstController)) {
controllerType = typeof(SecondController);
}
return DependencyResolver.Current.GetService(controllerType) as IController;
}
}
}
我们的实现便把请求传递给依赖性解析器,除非请求的是FirstController类型。在这种情况下,我们便请求SecondController类的一个实例。
只当你也使用一个依赖性解析器时,才可以使用这个IControllerActivator接口。这是因为DefaultControllerFactory通过调用IDependencyResolver.GetService方法查找(finds)一个IControllerActivator类型的控制器激活器。因此,我们需要用这个依赖性解析器来直接注册我们的激活器。在这个例子中,意味着使用NinjectDependencyResolver类的AddBindings方法,如清单14-7所示。
清单14-7. 注册一个控制器激活器
// put bindings here
Bind<IControllerActivator>().To<CustomControllerActivator>();
}
几乎总是简单地根据一个依赖解析器来创建控制器。然而,如果你想拦截并操纵请求,那么使用一个控制器激活器是一个有用的特定的特性(niche feature),在清单14-6中我们就是这样做的。
重写DefaultControllerFactory方法
你可以重写(override )DefaultControllerFactory类中的方法来定制控制器的构建。表14-1描述了你可以重写的三个方法,每一个都起着略有不同的作用。
表14-1. 可重写的DefaultContollerFactory方法 | ||
方法 | 结果 | 描述 |
CreateController | IController | IControllerFactory接口的CreateController方法的实现。默认地,这个方法调用GetControllerType来确定应该实例化哪个类型,然后把结果传递给GetControllerInstance方法来获得一个控制器对象。 |
GetControllerType | Type |
|
GetControllerInstance | IController | 生成指定类型的一个实例。 |
GetControllerInstance方法是项目中最典型地要重写的一个方法。我们在第7章中用它把DI引入到SprotsStore应用程序中。
生成一个自定义动作调用器(Action Invoker)
一旦控制器工厂已经生成了一个类的实例,框架就需要一种办法来调用这个实例上的一个动作。如果你是从Controller类来派生你的控制器的,那么,这便是动作调用器的责任。如果你是通过直接实现IController接口来生成你的控制器,那么,你要负责直接调用动作。参见第12章关于生成控制器的两种办法的例子。
动作调用器实现IActionInvoker接口,如清单14-8所示。
清单14-8. IActionInvoker接口
public interface IActionInvoker {
bool InvokeAction(ControllerContext controllerContext, string actionName);
}
}
该接口只有一个单一的成员:InvokeAction。其参数是一个ControllerContext对象(你在第12章看到过)和一个要调用的动作名称的字符串。其返回值是一个布尔型。返回值true表示找到并调用了这个动作。false表示控制器没有匹配的动作。
注意,在这个描述中我们并未使用“方法”这个单词(指上一小节中使用的是“动作”,而不是“动作方法” — 译者注)。动作与方法之间的关联是严格可选的。当这是内建的动作调用器所采取的办法时,你可以采取你所选择的任何方式随意地处理动作。清单14-9演示了另一种不同办法的IActionInvoker接口的实现。
(关于“动作”和“动作方法”:按译者的理解,说得直白一点,动作是一种行为,而动作方法是实现这种行为的代码。动作调用器的作用是实现对一个动作的调用,而控制器中才是实现这个动作的动作方法。根据这一含义,动作名和动作方法名是可以不同的,参见稍后的“使用自定义动作名”小节。 — 译者注)
清单14-9. 一个自定义动作调用器
namespace ControllerExtensibility.Infrastructure {
public class CustomActionInvoker : IActionInvoker {
public bool InvokeAction(ControllerContext context, string actionName) {
if (actionName == " Index ") {
context.HttpContext.Response.Write( " This is output from the Index action ");
return true;
} else {
return false;
}
}
}
}
这个动作调用器并不关心控制器类中的方法。事实上,它自己处理动作。如果这是对Index动作的请求,那么这个调用器直接把一条消息写到Response。如果是其它动作的请求,那么它返回false,这会引发一个显示给用户的404 — 未找到错误。
与控制器关联的动作调用器是通过Controller.ActionInvoker属性获得的。意即,同一个应用程序中的不同控制器可以使用不同的动作调用器。清单14-10演示了使用清单14-9动作调用器的一个控制器。
清单14-10. 在一个控制器中使用自定义动作调用器
using ControllerExtensibility.Infrastructure;
namespace ControllerExtensibility.Controllers {
public class CustomActionInvokerController : Controller {
public CustomActionInvokerController() {
this.ActionInvoker = new CustomActionInvoker();
}
}
}
在这个控制器中没有动作方法。它依赖动作调用器来处理请求。
我们并不建议你实现自己的动作调用器。而且如果你这么做,我们并不建议你遵循这种办法。为什么?首先,内建的支持有一些非常有用的特性(features),正如你马上就要看到的。其次,我们的例子有一些问题:缺乏可扩展性、贫乏的职责分离,而且缺乏对各种视图的支持。但这个例子演示了MVC框架如何组合和演示。再一次重申,请求处理管道的几乎每一个方面都是可以定制或完全替换的。
使用内建的动作调用器
ControllerActionInvoker是内建的动作调用器,它有一些把请求与动作相匹配的非常完善的技术。而且,与我们前小节的实现不同,ControllerActionInvoker通过方法进行操作。
为了具备一个动作的资格,一个方法必须满足以下条件:
· 该方法必须是public的。
· 该方法必须不是static的。
· 该方法必须不在System.Web.Mvc.Controller、或它的任何基类中。
· 该方法必须没有专用名。
前两个条件很简单。对于下一个条件,排除了在Controller类或其基类中出现的方法,这意味着不包括ToString及GetHashCode这样的方法,因为这些是IController接口实现的方法。这是有意义的,因为我们不希望把我们控制器的内部工作暴露给外部世界。最后一个条件意味着排除构造器、属性、以及事件访问器。实际上是指,不可以用具有System.Reflection.MethodBase的IsSpecialName标志的类成员来处理一个动作。
注意:具有泛型参数的方法(如MyMethod<T>())满足所有条件,但如果你试图调用这样的方法来处理一个请求,MVC框架会弹出一个异常。
默认地,ControllerActionInvoker找到一个具有与请求的动作同名的方法。例如,如果路由系统产生的action的值为Index,那么ControllerActionInvoker将查找符合这个动作条件名为Index的方法。如果找到这样一个方法,将调用它来处理这个请求。这种行为差不多总是恰好是你所想要的,但正如你所期望,MVC框架提供了一些微调这一过程的机会。
使用自定义动作名
通常,动作方法的名称确定了它所表示的动作。Index动作方法对Index动作进行服务。你可以用ActionName属性来重写这种行为,如清单14-11所示。
清单14-11. 使用自定义动作名
namespace ActionInvokers.Controllers {
public class HomeController : Controller {
[ActionName( " Index ")]
public ActionResult MyAction() {
return View();
}
}
}
在这个清单中,我们把这个特性(attribute)应用到MyAction方法,把Index参数值传递到其中。当动作调用器接收一个对Index动作的请求时,它现在将使用MyAction方法。运用这个特性(attribute)来重写动作的名字。这意味着MyAction方法不再用来对MyAction动作进行服务。
用这种方式重写一个方法名的原因主要有二:
· 你后来接收一个不合法的C#方法名(例如,[ActionName("User-Registration")])。
· 如果你希望有两个不同的C#方法接收同一组参数,并处理同样的方法名,但却要对不同的HTTP请求类型进行响应(例如,一个为[HttpGet],而另一个为[HttpPost]),你可以对这些方法用不同的C#名来满足编译器的要求,然后用[ActionName]把它们映射到同一个动作名。
One oddity that arises when using this attribute is that Visual Studio will use the original method name in the Add View dialog box. So, if you right-click the MyAction method and select Add View, you see the dialog box shown in Figure 14-2.
当使用这个特性(attribute)时,出现的一个奇怪的现象是Visual Studio将在“添加视图”对话框中使用原先的方法名。因此,如果你右击MyAction方法,并选择“添加视图”,你会看到如图14-2所示的对话框。
![图14-2](https://i-blog.csdnimg.cn/blog_migrate/a59bd6133a0c70fabd5a93fd83676f4e.png)
图14-2. Visual Studio并不检测ActionName特性(attribute)
这是一个问题,因为MVC框架将查找基于动作名的默认视图,在我们的例子中是由这个特性(attribute)定义为Index。当生成使用ActionName性质的动作方法的默认视图时,你必须确保该名字与此性质的值匹配,而不是C#的方法名。
使用动作方法选择
通常的情况是,一个控制器含有几个同名的动作。这可能是因为有多个方法,每一个带有不同的参数,或是因为你使用ActionName特性(attribute)以使多个方法表示同一个动作。
在这些情况下,MVC框架需要一些选择相应动作以处理一个请求的辅助办法。做这种事情的机制称为动作方法选择。它允许你定义不同种类的请求被一个动作处理。当我们在第8章用HttpPost性质约束一个动作时,你已经看到了一个动作方法选择的例子。我们有两个名为Checkout的方法,并且,通过使用HttpPost性质,我们指示其中一个只用于HTTP POST请求,如清单14-2所示。
清单14-12. 使用HttpPost性质
[HttpPost]
public ViewResult Checkout(Cart cart, ShippingDetails shippingDetails) {
// action method body
}
public ViewResult Checkout() {
// action method body
}
...
该动作调用器使用了一个动作方法选择器,以便在选择一个动作时消除不明确性。在清单14-12中,对Checkout动作有两个候选。该调用器把优先赋给了具有选择器的那个动作。在这种情况下,首先评估HttpPost选择器,以考查是否可以处理该请求。如果可以,那么这就是将被使用的方法。如果不行,那么将使用另一个方法。
对不同的HTTP请求,有一些内置的特性(attributes)选择器:HttpPost用于POST请求、HttpGet用于GET请求、HttpPut用于PUT请求等等。另一个内置的特性(attributes)是NonAction,它向动作调用器指出,不要使用一个看上去有效的动作方法,如清单14-13所示。
清单14-13. 使用NonAction选择器
[NonAction]
public ActionResult MyMethod() {
return View();
}
...
在该清单中的方法将不被视为一个动作方法。这对确保你的控制器类不作为动作是有用的。当然,通常这种方法应该被简单地标记为private,这会阻止它们作为动作被调用。然而,如果出于某些原因,你必须把方法标记为public的,则[NonAction]是有用的。
生成一个自定义的动作方法选择器
动作方法选择器派生于ActionMethodSelectorAttribute类,它如清单14-14所示。
清单14-14. ActionMethodSelectorAttribute类
using System;
using System.Reflection;
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public abstract class ActionMethodSelectorAttribute : Attribute {
public abstract bool IsValidForRequest(ControllerContext controllerContext,
MethodInfo methodInfo);
}
}
ActionMethodSelectorAttribute是一个抽象类,并定义了一个抽象方法:IsValidForRequest。该方法的参数是一个让你检测请求的ControllerContext对象,和一个MetholdInfo对象,你可以用它来获取应用了选择器的方法的信息。如果该方法能够处理请求,返回true,否则返回false。清单14-15演示了一个自定义的动作方法选择器。
清单14-15. 自定义动作方法选择器
using System.Web.Mvc;
namespace ActionInvokers.Infrastructure {
public class LocalAttribute : ActionMethodSelectorAttribute {
public override bool IsValidForRequest(ControllerContext context,
MethodInfo methodInfo) {
return context.HttpContext.Request.IsLocal;
}
}
}
我们的动作方法选择器将只有在该请求来源于本地机时,才从IsValidForRequest方法返回true。
清单14-16演示如何使用这个选择器来区分两个同名的动作方法。
清单14-16. 使用自定义动作方法选择器
[ActionName( " Index ")]
public ActionResult FirstMethod() {
return View(( object) " Message from FirstMethod ");
}
[Local]
[ActionName( " Index ")]
public ActionResult SecondMethod() {
return View(( object) " Message from SecondMethod ");
}
...
当这个控制器接收到一个对Index动作的请求时,将评估Local选择器。如果该请求是本地发生的,那么该选择器的IsValidForRequest方法将返回true,而SecondMethol将被用来处理这个请求。如果该请求来源于另一台计算机,那么这个选择器的IsValidForRequest方法将返回false,而这个动作方法调用器将使用FirstMethod。
注,我们不建议你把动作方法选择器用作为一种安全检查,除非你很喜欢调试。要使用预定的方法来处理用户的访问控制,请使用上一章所涉及的authorization性质。
THE ACTION METHOD DISAMBIGUATION PROCESS
动作方法的去歧义过程
现在,你已经深入了解了动作方法选择器基类,你应该能够理解动作调用器是如何选择一个动作方法的了。动作调用器从一个可能的候选方法列表(满足动作条件的控制器方法)开始进行处理。然后经历以下过程:
· 首先,该调用器丢弃任何基于名字的方法。只有与目标动作同名或与ActionName性质相配的方法被保留在这个列表中。
· 其次,该调用器丢弃那些动作方法选择器性质对当前请求返回false的动作方法。
· 如果恰好留下一个带有选择器的动作方法,那么这就是要用的方法。如果有多个带有选择器的方法,那么便弹出一个异常,因为该动作调用器不能消除可用方法之间的歧义。
· 如果不存在带有选择器的动作方法,那么该调用器便查找不带选择器的那些方法。如果恰好有一个这样的方法,那么这就是要调用的方法。如果有多个不带选择器的方法,便弹出一个异常,因为调用器不能在它们之间作出选择。
处理未知动作
如果动作调用器找不到一个要调用的动作方法,便从它的InvokeAction方法返回false。当这种情况发生时,Controller类便调用它的HandleUnknownAction方法。默认地,这个方法返回一个“404 — 未找到”对客户端作出响应。这是控制器对大多数应用程序所能做的最有用的事情,但是,如果你想做一些特殊的事情,你可以在你的控制器类中选择采用重写(override)这个方法。清单14-17提供了一个演示。
清单14-17. 重写HandleUnknownAction方法
using ActionInvokers.Infrastructure;
namespace ActionInvokers.Controllers {
public class HomeController : Controller {
public ActionResult Index() {
return View();
}
protected override void HandleUnknownAction( string actionName) {
Response.Write( string.Format( " You requested the {0} action ", actionName));
}
}
}
在这个例子中,我们的控制器有一个Index动作方法。它将在其它动作被请求时,把一个消息写到Response对象,如图14-3所示。
![图14-3](https://i-blog.csdnimg.cn/blog_migrate/0eabd743878b3b638e0f5f0ee5d74338.png)
图14-3. 处理未知动作的请求
使用动作方法选择器以支持REST服务
在过去几年中,许多开发者都选择采用“表现式状态传输(REST)”的风格来实现web服务,而不是采用更复杂的那些方式,如“简单对象访问协议(SOAP)”等。
一个REST操作是通过一个URL和一个HTTP方法的组合来指定的。该URL具有应用程序的特定含义。例如,你可能有一个诸如/Staff/1之类的URL,它指向staff数据库的第一条记录。不同的HTTP方法表示了可以在这条记录上进行的不同操作。GET请求接收数据、POST请求修改它、DELETE请求删除它,等等。
你可以通过用ActionName性质与动作方法选择器组合的办法来提供对REST的支持,如清单14-18所示。
清单14-18. 支持REST Web服务
[HttpGet]
[ActionName( " Staff ")]
public ActionResult StaffGet( int id) {
// ... logic to get and return data item
}
[HttpPost]
[ActionName( " Staff ")]
public ActionResult StaffModify( int id, StaffMember person) {
// ... logic to modify or create data item
}
[HttpDelete]
[ActionName( " Staff ")]
public ActionResult StaffDelete( int id) {
// ... logic to delete data item
}
}
假设我们像下面这样把一个路由条目添加到Global.asax:
routes.IgnoreRoute( " {resource}.axd/{*pathInfo} ");
routes.MapRoute( null, " Staff/{id} ",
new { controller = " Staff ", action = " Staff " },
new { id = @" \d+ " /* Require ID to be numeric */ });
routes.MapRoute(
" Default ", // Route name
" {controller}/{action}/{id} ", // URL with parameters
new { controller = " Home ", action = " Index ", id = UrlParameter.Optional }
);
}
现在,我们应用程序中的每一个StaffMember实体都可以用Staff/123这种格式的URL进行唯一定位,而客户端可以用GET、POST、及DELETE的HTTP方法对这些地址进行操作。这称为REST化的API,而声称自己是REST专家的那些人,除了他们自己的代码之外,他根本说不出真正的REST化是什么(我们有点开玩笑了)。
重写HTTP方法
只要整个客户端能够使用所有HTTP方法,REST化的API是很棒的。例如,以.NET语言、Java、或Ruby编写的服务器端程序使用你的服务将没有问题。类似地,从当前版本的主浏览器(如Chrome、Firefox、以及Internet Explorer)形成的JavaSctipt的AJAX调用也没有任何困难。不幸的是,有些主流web技术只支持GET和POST方法。这包括HTML表单(HTML的form)以及Flash应用程序。此外,许多防火墙也只允许GET和POST请求通过。
为了解决这种缺陷,有一个约定,在这里你可以纳入所需的HTTP方法,即使该请求不是用这个方法形成的。这是通过在请求中包含一个“键/值”对实现的,这里,“键”是X-HTTP-Method-Override,“值”是所期望的方法。键可以被指定为一个HTTP报头、HTML表单中的一个隐藏的input字段、或者是查询字串的一部分。例如,一个POST请求可以含有一个报头,以指示MVC框架应该像接收了一个DELETE请求那样进行操作。其背后要考虑的事情是,大多数web技术允许任意报头或表单的input设置,即使它们不支持相应的HTTP方法(比如,大多数web技术不支持DELETE,但仍然可以把报头或表单中的input字段设置为DELETE — 译者注)。
重写MVC HTML表单中的HTTP方法
MVC框架在视图和控制器中都提供了对重写HTTP方法的支持。视图支持意味着你可以根据你的HTML表单使用REST化的API,而控制器支持意味着MVC框架将会适当地处理含有X-HTTP-Methold-Override的请求。清单14-19演示了如何在一个视图中重写HTTP方法。
清单14-19. 在视图中重写HTTP方法
@using (Html.BeginForm()) {
@Html.HttpMethodOverride(HttpVerbs.Delete)
<input type= " submit " value= " Delete " />
}
...
Html.HttpMethodOverride辅助方法允许你指定处理这个请求的HTTP方法。在这个例子中,我们指定了DELETE方法。这个辅助方法把一个input字段添加到我们的表单,像下面这样:
<input name="X-HTTP-Method-Override" type="hidden" value="DELETE" />
MVC框架在处理表单时会查找这个input字段,于是,通过Request.GetHttpMethodOverride方法,这个被重写的HTTP方法生效了。
MVC框架支持只对POST请求进行重写的HTTP方法。这与GET请求总是除数据检索而无其它副作用的这种安全交互性是一致的(详见第11章)。
注意:在一个重写的HTTP方法被使用时,由Request.HttpMethod返回的值并未改变。因此,如果一个表单被如清单14-19所示的隐藏input字段所递交,其HttpMethod属性仍返回POST,而GetHttpMethodOverride方法将返回DELETE。
未完待续。。。