Apress Pro ASP.NET MVC 4


ASP.NET MVC 4 (一)路径映射

RouteCollection.MapRoute()等同于:

Route myRoute = new Route("{controller}/{action}", new MvcRouteHandler()); 
routes.Add("MyRoute", myRoute); 

其他一些URL映射的例子:

routes.MapRoute("", "Public/{controller}/{action}",new { controller = "Home", action = "Index" }); //URL可以包含静态的部分,这里的public
routes.MapRoute("", "X{controller}/{action}"); //所有以X开头的控制器路径,比如mydomain.com/xhome/index映射到home控制器
routes.MapRoute("ShopSchema", "Shop/{action}",new { controller = "Home" }); //URL可以不包含控制器部分,使用这里的默认Home控制器
routes.MapRoute("ShopSchema2", "Shop/OldAction",new { controller = "Home", action = "Index" }); //URL可以是全静态的,这里mydomain.com/shop/oldaction传递到home控制器的index方法

一个比较特殊的例子:

routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", new { controller = "Home", action = "Index", id = UrlParameter.Optional }); 

这可以映射任意多的URL分段,id后的所有内容都被赋值到cathall参数,比如/Customer/List/All/Delete/Perm,catchall = Delete/Perm。

需要注意的是路径表的注册是有先后顺序的,按照注册路径的先后顺序在搜索到匹配的映射后搜索将停止。

命名空间优先级

MVC根据{controller}在应用程序集中搜索同名控制类,如果在不同命名空间下有同名的控制类,MVC会给出多个同名控制类的异常,我们可以在注册路由的时候指定搜索的命令空间:

routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", 
new { controller = "Home", action = "Index", id = UrlParameter.Optional , 
new[] { "URLsAndRoutes.AdditionalControllers" }); 

这里表示我们将在"URLsAndRoutes.AdditionalControllers"命名空间搜索控制类,可以添加多个命名空间,比如:

routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", 
new { controller = "Home", action = "Index", id = UrlParameter.Optional }, 
new[] { "URLsAndRoutes.AdditionalControllers", "UrlsAndRoutes.Controllers"}); 

"URLsAndRoutes.AdditionalControllers", "UrlsAndRoutes.Controllers"两个命名空间是等同处理没有优先级的区分,如果这两个空间里有重名的控制类一样导致错误,这种情况可以分开注册多条映射:

routes.MapRoute("AddContollerRoute", "Home/{action}/{id}/{*catchall}", 
new { controller = "Home", action = "Index", id = UrlParameter.Optional }, 
new[] { "URLsAndRoutes.AdditionalControllers" }); 
routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}", 
new { controller = "Home", action = "Index", id = UrlParameter.Optional }, 
new[] { "URLsAndRoutes.Controllers" }); 

路由限制

除了在注册路由映射时可以指定控制器搜索命名空间,还可以使用正则表达式限制路由的应用范围,比如:

routes.MapRoute("MyRoute", "{controller}/{action}/{id}/{*catchall}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional },
new { controller = "^H.*", action = "^Index$|^About$", httpMethod = new HttpMethodConstraint("GET")},
new[] { "URLsAndRoutes.Controllers" }); 

这里限制MyRoute路由仅用于映射所有H开头的控制类、且action为Index或者About、且HTTP请求方法为GET的客户端请求。

如果标准的路由限制不能满足要求,可以从IRouteConstraint接口扩展自己的路由限制类:

public class UserAgentConstraint : IRouteConstraint {

    private string requiredUserAgent;

    public UserAgentConstraint(string agentParam) {
        requiredUserAgent = agentParam;
    }

    public bool Match(HttpContextBase httpContext, Route route, string parameterName,
                      RouteValueDictionary values, RouteDirection routeDirection) {

        return httpContext.Request.UserAgent != null &&httpContext.Request.UserAgent.Contains(requiredUserAgent);
    }
}

在注册路由时这样使用:

routes.MapRoute("ChromeRoute", "{*catchall}",
    new { controller = "Home", action = "Index" },
    new { customConstraint = new UserAgentConstraint("Chrome")},
    new[] { "UrlsAndRoutes.AdditionalControllers" });

这表示我们限制路由仅为浏览器Agent为Chrome的请求时使用。

路由到磁盘文件

除了控制器方法,我们也需要返回一些静态内容比如HTML、图片、脚本到客户端,默认情况下路由系统优先检查是否有和请求路径一致的磁盘文件存在,如果有则不再从路由表中匹配路径。我们可以通过配置颠倒这个顺序:

public static void RegisterRoutes(RouteCollection routes) { 
routes.RouteExistingFiles = true; 
... ....

还需要修改web配置文件:

<add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" preCondition=""/> 

这里设置preCondition为空。如果我们再请求一些静态内容比如~/Content/StaticContent.html时会优先从路径表中匹配。

而如果我们又需要忽略某些路径的路由匹配,可以:

... 
public static void RegisterRoutes(RouteCollection routes) { 
routes.RouteExistingFiles = true; 
routes.IgnoreRoute("Content/{filename}.html"); 
...

它会在RouteCollection中添加一个route handler为StopRoutingHandler的路由对象,在匹配到content路径下的后缀为html的文件时停止继续搜索路径表,转而匹配磁盘文件。

ASP.NET MVC 4 (二)控制器

和ViewBag只用于控制器和同名视图不同,TempData类似于会话Session数据,可以在不同控制器/视图中共享,但是和Session跨请求不同的是一旦读取了TempData的数据TempData就会被删除。TempData.Peek读取数据但是不删除数据:

DateTime time = (DateTime)TempData.Peek("Date"); 

TempData.Keep可以标记数据不被删除,但是如果再被读取又会再次标记删除,类似于引用计数器:

TempData.Keep("Date");

返回HTTP状态码 我们可以返回一个HTTP状态码比如404错误:

public HttpStatusCodeResult StatusCode() { 
    return new HttpStatusCodeResult(404, "URL cannot be serviced"); 
} 

或者直接调用:

return HttpNotFound(); 

401未授权错误:

return new HttpUnauthorizedResult();

ASP.NET MVC 4 (三) 过滤器

Authorize特性类AuthorizeAttribute就称作MVC的Filter,它在横向为MVC框架扩展功能,让我们可以更方便的处理日志、授权、缓存等而不影响纵向主体功能。

MVC常用的滤器类型:

  • Authorization:实现IAuthorizationFilter接口,默认实现类AuthorizeAttribute,在调用Action方法前首先处理认证信息。
  • Action:实现IActionFilter接口,默认实现类ActionFilterAttribute,在运行Action前后调用实现一些额外的动作。
  • Result:实现IResultFilter接口,默认实现类ActionFilterAttribute,在action result运行前后调用实现额外的动作。
  • Exception:实现IExceptionFilter接口,默认实现类HandleErrorAttribute,仅在其他过滤器或者action方法或者action result抛出异常时调用。

过滤器可以应用在整个控制器类上,也可以单对某个Action方法,当然也可以是同时应用多个过滤器:

Authorization过滤器

Authorization过滤器用于控制仅授权的用户可以调用某个Action方法,它必须实现IAuthorizationFilter接口

处理安全方面的代码必须谨慎全面,一般我们不要直接实现接口IAuthorizationFilter,而从AuthorizeAttribute扩展自定义类

Exception过滤器

Exception过滤器需要实现IExceptionFilter接口

从filterContext我们可以获取很多相关信息,比如Controller、HttpContext、IsChildAction、RouteData、Result、Exception、ExceptionHandled等。异常的具体信息我们可以从Exception获取,我们设置ExceptionHandled为true报告异常已经处理,在设置为true之前最好检查异常是否已经被action方法上其他的过滤器处理,以避免重复的错误纠正动作。如果异常未被处理(ExceptionHandled!=true),MVC框架使用默认的异常处理程序显示ASP.NET的黄屏错误页面。

public class RangeExceptionAttribute : FilterAttribute, IExceptionFilter {

    public void OnException(ExceptionContext filterContext) {

        if (!filterContext.ExceptionHandled &&
            filterContext.Exception is ArgumentOutOfRangeException) {

                 int val = (int)(((ArgumentOutOfRangeException)filterContext.Exception).ActualValue);

                //filterContext.Result = new RedirectResult("~/Content/RangeErrorPage.html");
                filterContext.Result = new ViewResult { 
                    ViewName = "RangeError", 
                    ViewData = new ViewDataDictionary<int>(val)
                };
                filterContext.ExceptionHandled = true;
        }
    }
}

Action过滤器

Action过滤器必须实现IActionFilter接口

OnActionExecuting()在调用action方法前调用,OnActionExecuted()则在调用action方法后调用。

public class CustomActionAttribute : FilterAttribute, IActionFilter {

    public void OnActionExecuting(ActionExecutingContext filterContext) {
        if (filterContext.HttpContext.Request.IsLocal) {
            filterContext.Result = new HttpNotFoundResult();
        }
    }

    public void OnActionExecuted(ActionExecutedContext filterContext) {
        // not yet implemented
    }
}

>

public class ProfileActionAttribute : FilterAttribute, IActionFilter {
    private Stopwatch timer;


    public void OnActionExecuting(ActionExecutingContext filterContext) {
        timer = Stopwatch.StartNew();
    }

    public void OnActionExecuted(ActionExecutedContext filterContext) {
        timer.Stop();
        if (filterContext.Exception == null) {
            filterContext.HttpContext.Response.Write(
                string.Format("<div>profile result method elapsed time: {0}</div>",
                    timer.Elapsed.TotalSeconds));
        }
    }
}

Result过滤器

Result过滤器实现IResultFilter接口

Result过滤器操作的是action方法返回结果,有意思的是即使action方法返回void所应用的Result过滤器也会动作。

类似上面的ProfileAction过滤器我们可以对Result运行计时

public class ProfileResultAttribute : FilterAttribute, IResultFilter {
    private Stopwatch timer;

    public void OnResultExecuting(ResultExecutingContext filterContext) {
        timer = Stopwatch.StartNew();
    }

    public void OnResultExecuted(ResultExecutedContext filterContext) {
        timer.Stop();
        filterContext.HttpContext.Response.Write(
                string.Format("<div>Result elapsed time: {0}</div>",
                    timer.Elapsed.TotalSeconds));
    }
}

内建的Result过滤器类ActionFilterAttribute

MVC为我们提供了ActionFilterAttribute类,它同时实现了action和result过滤器接口

public class ProfileAllAttribute : ActionFilterAttribute {
    private Stopwatch timer;

    public override void OnActionExecuting(ActionExecutingContext filterContext) {
        timer = Stopwatch.StartNew();
    }

    public override void OnResultExecuted(ResultExecutedContext filterContext) {
        timer.Stop();
        filterContext.HttpContext.Response.Write(
                string.Format("<div>Total elapsed time: {0}</div>",
                    timer.Elapsed.TotalSeconds));
    }
}

Controller类的过滤器支持

MVC的Controller类内部实现了IAuthorizationFilter、IActionFilter、IResultFilter、IExceptionFilte四个接口,并提供OnXXX的虚函数供调用,

public class HomeController : Controller {
    private Stopwatch timer;

    protected override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        timer = Stopwatch.StartNew();
    }

    protected override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        timer.Stop();
        filterContext.HttpContext.Response.Write(
                string.Format("<div>Total elapsed time: {0}</div>",
                    timer.Elapsed.TotalSeconds));
    } 

}

全局过滤器

全局过滤器应用于应用程序内所有控制器的所有action方法,我们在App_Start/FilterConfig.cs可以注册全局过滤器:

public class FilterConfig {
    public static void RegisterGlobalFilters(GlobalFilterCollection filters) {
        filters.Add(new HandleErrorAttribute());
        filters.Add(new ProfileAllAttribute());
    }
}

HandleErrorAttribute是VS为我们默认添加的,用于未处理异常错误时显示/Views/Shared/Error.cshtml页面;ProfileAllAttribute则是我们添加的自定义过滤器,运行任何action方法时都会调用这个过滤器。

其他MVC内建过滤器

MVC框架还内建提供以下过滤器:

  • RequireHttps:指示必须以HTTPS访问action方法,仅用于HTTP get方法
  • OutputCache:缓存action方法的结果
  • ValidateInput和ValidationAntiForgeryToken:安全授权相关的过滤器
  • AsnycTimeOut和NoAsyncTimeout:用于异步控制器
  • ChildActionOnlyAttribute:用于授权Html.Action或者Html.RenderAction调用子action方法

这些过滤器的使用方法可以参见MSDN。

过滤器的运行顺序

过滤器的运行按先后顺序是:authorization过滤器、action过滤器、result过滤器,期间任何时刻发生未处理异常调用异常处理器。这是针对不同类型的过滤器,但是如果所应用的是同一类的过滤器呢?MVC默认并不保证同类型过滤器的调用程序,也就是说很可能并非按照出现在代码中的先后程序来调用过滤器,但是我们可以显式的指定它们的调用顺序

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()满足上面的要求,但是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对象,方法内部使用await等待访问远程数据完成,结果就是请求该action时工作线程不会被阻塞停止,而是返回到线程池相应其他其他请求,在获取到远程数据完成后再启动线程继续后续工作。

ASP.NET MVC 4 (五) 视图

“@:”,它阻止Razor将后续语句解释为代码

ASP.NET MVC 4 (六) 帮助函数

内联帮助函数

我们可以直接在视图中定义内联的帮助函数,使用@helper标记内联函数定义:

@model string
@{
    Layout = null;
}
@helper ListArrayItems(string[] items)
{
    foreach (string str in items)
    {
        <b>@str </b>
    }
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
</head>
<body>
    <div>
        Here are the fruits: @ListArrayItems(ViewBag.Fruits)
    </div>
    <div>
        Here are the cities: @ListArrayItems(ViewBag.Cities)
    </div>
    <div>
        Here is the message:
        <p>@Model</p>
    </div>
</body>
</html>

外部帮助函数

内联帮助函数虽然很方便,但是只能在视图中定义和使用,如果内联函数很复杂会让视图很难读得清楚,对此我们可以定义外部帮助函数,外部帮助函数实际上是对HtmlHelper类的方法扩展,上面的内联帮助函数改写成外部帮助函数是这样的

namespace HelperMethods.Infrastructure {
    public static class CustomHelpers {

        public static MvcHtmlString ListArrayItems(this HtmlHelper html, string[] list) {

            TagBuilder tag = new TagBuilder("ul");
            foreach(string str in list) {
                TagBuilder itemTag = new TagBuilder("li");
                itemTag.SetInnerText(str);
                tag.InnerHtml += itemTag.ToString();
            }

            return new MvcHtmlString(tag.ToString());
        }

    }
}

HtmlHelper暴露一些属性比如RouteCollection、ViewBag、ViewContext方便我们获取当前请求相关的数据。外部帮助函数最后返回一个MvcHtmlString对象,它的内容直接写到输出响应。我们可以这样在视图中使用自定义的外部帮助函数:

@model string
@using HelperMethods.Infrastructure
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>
</head>
<body>
    <div>
        Here are the fruits: @Html.ListArrayItems((string[])ViewBag.Fruits)
    </div>
    <div>
        Here are the cities: @Html.ListArrayItems((string[])ViewBag.Cities)
    </div>
    <div>
        Here is the message:
        <p>@Model</p>
    </div>
</body>
</html>

我们需要引入定义外部帮助函数的命名空间,也可以定义在/Views/Web.config中供所有的视图使用。使用@html来调用外部帮助函数,它返回一个HtmlHelper类的实例,在调用外部帮助函数时也要做参数类型转化。

帮助函数中的字符串编码

在使用帮助函数时我们需要注意HTML编码的问题

ASP.NET MVC 4 (七) 模板帮助函数

和普通HTML帮助函数不同,模板帮助函数不需要指定所用的HTML类型,MVC会推断选择合适的HTML元素,这让我们有更多的灵活性。

使用模板帮助函数

ASP.NET MVC 4 (八) URL链接和Ajax帮助函数

知识共享许可协议
《Apress Pro ASP.NET MVC 4》 常伟华 创作。
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议 | 3.0 中国大陆许可协议进行许可。

站内公告

A PHP Error was encountered

Severity: Core Warning

Message: PHP Startup: zip: Unable to initialize module Module compiled with module API=20060613 PHP compiled with module API=20090626 These options need to match

Filename: Unknown

Line Number: 0

Backtrace: