ASP.NET MVC 4 (四) 控制器扩展

时间:2021-08-31 11:26:21

MVC的标准流程是请求传递给控制器,由控制器action方法操作数据模型,最后交由视图渲染输出,这里忽略了两个细节,就是MVC是如何创建相应控制器实例,又是如何调用控制器action方法的,这就必须讲到控制器工厂和action调用器。

ASP.NET MVC 4 (四) 控制器扩展

控制器工厂

Controller factory负责创建并初始化控制器,控制器工厂实现IControllerFactory接口:

namespace System.Web.Mvc { 
public interface IControllerFactory {
  IController CreateController(RequestContext requestContext,
string controllerName);
  SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext,
string controllerName);
  
void ReleaseController(IController controller);
  }
}

我们可以从IControllerFactory接口实现自定义的控制器工厂:

public class CustomControllerFactory: IControllerFactory {

public IController CreateController(RequestContext requestContext, string controllerName) {

Type targetType
= null;
switch (controllerName) {
case "Product":
targetType
= typeof(ProductController);
break;
case "Customer":
targetType
= typeof(CustomerController);
break;
default:
requestContext.RouteData.Values[
"controller"] = "Product";
targetType
= typeof(ProductController);
break;
}

return targetType == null ? null :
(IController)DependencyResolver.Current.GetService(targetType);
}

public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName) {

switch (controllerName) {
case "Home":
return SessionStateBehavior.ReadOnly;
case "Product":
return SessionStateBehavior.Required;
default:
return SessionStateBehavior.Default;
}
}

public void ReleaseController(IController controller) {
IDisposable disposable
= controller as IDisposable;
if (disposable != null) {
disposable.Dispose();
}
}
}

这里最重要的方法就是CreateController(),由它根据请求的控制器的名称直接创建所需控制器的实例,这里是直接硬编码控制器名称,当然实际的应用中众多不应该是这样操作。CreateController()必须返回一个实现IController的对象,上面例子中最后调用DependencyResolver.Current.GetService()来负责创建相应控制器的实例。

方法GetControllerSessionBehavior()是用于MVC确定是否需要维护会话信息,稍后详述。方法ReleaseController()用于释放需要的资源,这里只是单纯调用控制器实例的Dispose()方法释放资源。

要使用自定义的控制器工厂我们还必须在应用启动时注册它为当前控制器工厂:

    public class MvcApplication : System.Web.HttpApplication {
protected void Application_Start() {
AreaRegistration.RegisterAllAreas();

WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);

ControllerBuilder.Current.SetControllerFactory(
new CustomControllerFactory());
...

MVC内建控制器工厂DefaultControllerFactory

我们一般不需要自定义控制器工厂而使用MVC默认的DefaultControllerFactory,它根据路径映射在应用程序中搜索符合这些要求的控制器类:

  • 类必须是public
  • 必须是实类,不是abstract
  • 不能带泛型参数
  • 类名必须以Controller结尾
  • 类必须实现IController接口

DefaultControllerFactory维护一个应用程序内符合要求的控制器列表,在请求到达时从列表中搜索相应的控制器。在路径映射一文中讲到搜索控制器时的命名空间优先级,除了在路径映射中指定命名空间,还可以这样添加默认优先搜索的命名空间:

ControllerBuilder.Current.DefaultNamespaces.Add("MyControllerNamespace"); 
ControllerBuilder.Current.DefaultNamespaces.Add(
"MyProject.*");

通过ControllerBuilder.Current.DefaultNamespaces.Add添加的命名空间比未在此添加的命名空间有更高的优先级,但是用此方法添加的所有命名空间是同等对待的,它们之间不分优先级。

DefaultControllerFactory使用DependencyResolver初始化控制器类,我们可以通过controller activator以DI(Dependency injection)的方式调整DefaultControllerFactory创建控制器类,controller activator用到IControllerActivator接口:

namespace System.Web.Mvc { 
using System.Web.Routing;
public interface IControllerActivator {
IController Create(RequestContext requestContext, Type controllerType);
}
}

创建一个controller activator的实现:

public class CustomControllerActivator : IControllerActivator
{

public IController Create(RequestContext requestContext,
Type controllerType)
{
if (controllerType == typeof(ProductController))
{
controllerType
= typeof(CustomerController);
}
return (IController)DependencyResolver.Current.GetService(controllerType);
}
}

和自定义控制器工厂类似,我们需要注册使用它:

ControllerBuilder.Current.SetControllerFactory(new DefaultControllerFactory(new CustomControllerActivator()));

以这种方式可以调整DefaultControllerFactory如何创建控制器类,比完全自定义类工厂更加简单。实际上我们还可以直接从DefaultControllerFactory扩展自定义的控制器工厂,可以重载DefaultControllerFactory的方法CreateController、

GetControllerType、GetControllerInstance来满足自己的需要。

自定义action invoker

如果我们的控制器类是直接从IController接口创建而来的,我们需要自己调用action方法,而如果控制器类是从Controller类扩展的,Controller则已经内建支持如何调用action方法。调用action方法用到接口IActionInvoker:

namespace System.Web.Mvc { 
public interface IActionInvoker {
bool InvokeAction(ControllerContext controllerContext, string actionName);
}
}

从该接口我们可以创建自定义的action invoker:

public class CustomActionInvoker : IActionInvoker {

public bool InvokeAction(ControllerContext controllerContext, string actionName) {
if (actionName == "Index") {
controllerContext.HttpContext.
Response.Write(
"This is output from the Index action");
return true;
}
else {
return false;
}
}
}

这里只是简单的匹配action的名称,如果是index则直接输出结果到响应。我们还必须将它和控制器联系起来才能使用:

public class ActionInvokerController : Controller { 
  
public ActionInvokerController() {
    
this.ActionInvoker = new CustomActionInvoker();
    }
}

这里在ActionInvokerController的构造函数中指定其ActionInvoker为自定义的ActionInvoker,由它负责处理action的调用。

内建Action invoker

和控制器工厂一样,MVC提供内建的默认action invoker:ControllerActionInvoker。它负责在控制器内搜索匹配action方法,只有符合以下要求的控制器方法才被认为是action方法:

  • 方法必须是public
  • 方法必须不是static
  • 方法必须不在System.Web.Mvc.Controller或它的任何子类中出现
  • 方法名必须不是特殊名,比如不是构造函数、不是属性、不是事件访问方法,简单的说就是不带IsSpecialName(System.Reflection.MethodBase)标志。

虽然泛型函数比如MyMethod<T>()满足上面的要求,但是MVC在调用这样的方法时会报出异常。

默认情况下根据请求查找同名的action方法,但是我们可以这样自定义action名称:

[ActionName("Enumerate")]
public ViewResult List() {
return View("Result", new Result {
ControllerName
= "Customer",
ActionName
= "List"
});
}

这里通过ActionName特性将Enumerate action映射到List方法,访问原有的list会得到错误。通过这种方式可以实现不符合c#方法名称的action,比如[ActionName("User-Registration")],还可以将同一个action名称结合其他特性比如[HttpGet]、[HttpPost]将它们映射到不同的控制器方法上。

除了ActionName特性我们还可以使用NonAction特性指示一个控制器方法不能作为action调用:

[NonAction]
public ActionResult MyAction() {
return View();
}

这里MyAction虽然是合法的action方法,但是通过[NonAction]将其标识为非action,对它的请求将得到“Resource not found”错误。

Action方法选择器

ActionName、NoActionAction、HttpPost、HttpGet这些特性都是从MethodSelectorAttribute扩展而来,它们统一称为action方法选择器,当然我们也可以创建自定义的action方法选择器:

public class LocalAttribute : ActionMethodSelectorAttribute {

public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) {
return controllerContext.HttpContext.Request.IsLocal;
}
}

这里LocalAttribute确认所标识的action方法针对请求来自于本地是可以处理的:

public class HomeController : Controller {

public ActionResult Index() {
return View("Result", new Result {
ControllerName
= "Home", ActionName = "Index"
});
}

[Local]
[ActionName(
"Index")]
public ActionResult LocalIndex()
{
return View("Result", new Result
{
ControllerName
= "Home",
ActionName
= "LocalIndex"
});
}
...

比如这里的Home控制器的Index方法和通过ActionName("Index")指定的LocalIndex方法都对应到Index请求,如果LocalIndex不附加[Local]就会产生重名的action错误,通过[Local]就可以将来自于本地的请求映射到LocalIndex上,其他请求则调用Index方法。

未知Action的处理

如果没有找到对应的Action方法,Controller类调用其HandleUnknownAction方法,默认该方法报404-Not found错误。我们可以重载Controller的HandleUnknownAction方法执行其他操作:

protected override void HandleUnknownAction(string actionName) {
Response.Write(
string.Format("You requested the {0} action",actionName));
}

如果没有发现相应的action方法,这里直接输出一条信息到响应。

Sessionless控制器

控制器默认支持会话,在多个客户端请求间保存会话数据,如果客户端同时发出多个请求,这些请求必须排队依次处理,以先后顺序修改会话数据,这会影响服务器的并发性能。对于那些不需要会话的场合,我们可以使用sessionless控制器。

IControllerFactory的GetControllerSessionBehavior方法返回一个SessionStateBehavior枚举,它包含以下枚举值:

  • Default:使用配置文件中HttpContext节定义的默认ASP.NET会话状态
  • Requried:会话状态可写可读
  • ReadOnly:会话状态只可写
  • Disabled:禁用会话状态

如果我们是直接从IControllerFactory自定义控制器工厂,我们可以针对不同的请求设置不同的会话行为:

public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName) {

switch (controllerName) {
case "Home":
return SessionStateBehavior.ReadOnly;
case "Product":
return SessionStateBehavior.Required;
default:
return SessionStateBehavior.Default;
}
}

如果使用的是默认DefaultControllerFactory控制器工厂,我们可以使用SessionState标识控制器会话行为:

[SessionState(SessionStateBehavior.Disabled)]
public class FastController : Controller {

public ActionResult Index() {
return View("Result", new Result {
ControllerName
= "Fast ",ActionName = "Index"
});
}
}

这里将FastController标识未不启用会话状态,如果我们在视图中试图访问会话数据比如@Session["Message"]将抛出异常。

异步控制器

ASP.NET维护一个.NET线程池用于处理客户请求,这些工作线程在完成工作后返回到线程池等待为下一个请求服务。使用线程池可以节约为每个请求创建线程的开销,通过固定线程数量也避免同时请求数超过服务器的负载能力的情况。一种极端的情况是如果我们请求的资源位于远程服务器上,线程等待远程资源被阻塞,造成服务器不能响应新的客户端请求。我们用下面的例子模仿这种情况:

public class RemoteDataController : Controller { 
  
public ActionResult Data() {
    RemoteService service
= new RemoteService();
    
string data = service.GetRemoteData();
  
return View((object)data);
  }
}

RemoteData控制器方法Data请求的数据来自于RemoteService:

public class RemoteService { 
  
public string GetRemoteData() {
    Thread.Sleep(
2000);
    
return "Hello from the other side of the world";
  }
}

我们暂停RemoteService的线程2秒模仿请求远程数据阻塞,如果同时多个请求都访问/RemoteData/Data,比如造成服务器的响应延迟。

创建异步控制器可以直接从System.Web.Mvc.Async.IAsyncController实现,也可以从 System.Web.Mvc.AsyncController扩展,后者内部实现了IAsyncController接口。这里使用AsyncController为例:

public class RemoteDataController : AsyncController{ 
  
public async Task<ActionResult>Data() {
    
string data = await Task<string>.Factory.StartNew(() => {
      
return new RemoteService().GetRemoteData(); });
    
return View((object)data);
  }
}

异步控制器action方法返回一个Task<ActionResult>对象,方法内部使用await等待访问远程数据完成,结果就是请求该action时工作线程不会被阻塞停止,而是返回到线程池相应其他其他请求,在获取到远程数据完成后再启动线程继续后续工作。

 

以上为对《Apress Pro ASP.NET MVC 4》第四版相关内容的总结,不详之处参见原版 http://www.apress.com/9781430242369