在一个较复杂的Mvc项目中,我们可以利用Area对模块进行划分。在MVC2中,添加Area后,经常出现找不到视图的情况,此时,我们必须自行定义一个视图引擎,以告知相关的View的路径,形成正确的ViewEngineResult返回。具体步骤如下:
一、定义一个视图引擎,继承自WebFormViewEngine
1.1 在其构造函数中指定ViewLocationFormats与MasterLocationFormats,将Area中对应的area参数加到寻找View的路径中
1.2 override基类型WebFormViewEngine中的两个方法:FindView与FindPartialView,对出现area的路由,指明正确寻找View的方法,以便返回正确的ViewEngineResult结果。
1.3 示例代码如下:
public class MyViewEngine:WebFormViewEngine
{
public MyViewEngine()
: base()
{
// 视图位置匹配规则设置
ViewLocationFormats = new string[]
{
"~/{0}.aspx",
"~/{0}.ascx",
"~/Views/{1}/{0}.aspx",
"~/Views/{1}/{0}.ascx",
"~/Views/Shared/{0}.aspx",
"~/Views/Shared/{0}.ascx",
};
// 母版页匹配规则设置
MasterLocationFormats = new string[]
{
"~/{0}.Master",
"~/Shared/{0}.Master",
"~/Views/{1}/{0}.Master",
"~/Views/Shared/{0}.Master",
};
}
///<summary>
///重写视图推送的方法
///</summary>
///<param name="controllerContext"></param>
///<param name="viewName"></param>
///<param name="masterName"></param>
///<param name="useCache"></param>
///<returns></returns>
public overrideViewEngineResult FindView(ControllerContext controllerContext,string viewName, string masterName,bool useCache)
{
ViewEngineResult areaResult = null;
if (controllerContext.RouteData.Values.ContainsKey("area")) {
//如果存在area
//将原来寻找view的路径“~/Views/controller名/viewName”改为"Areas/area名/Views/controller名/viewName"
string areaViewName = FormatViewName(controllerContext, viewName);
//根据view生成一个ViewEngineResult对象
areaResult = base.FindView(controllerContext, areaViewName, masterName, useCache);
if (areaResult != null && areaResult.View != null) {
return areaResult;
}
//没有找到对应的view,则到share下去找是否有共用的view
//将原来的从shared下找view的路径"~/Views/Shared/viewName"改为"Areas/area名/Views/Shared/viewName"
string sharedAreaViewName = FormatSharedViewName(controllerContext, viewName);
//根据shared下的view重新生成ViewEngineResult,返回
areaResult = base.FindView(controllerContext, sharedAreaViewName, masterName, useCache);
if (areaResult != null && areaResult.View != null) {
return areaResult;
}
}
//没有找到相关结果,返回默认的
return base.FindView(controllerContext, viewName, masterName, useCache);
}
///<summary>
///重写PartialView推送的方法
///</summary>
///<param name="controllerContext"></param>
///<param name="partialViewName"></param>
///<param name="useCache"></param>
///<returns></returns>
public overrideViewEngineResult FindPartialView(ControllerContext controllerContext,string partialViewName, bool useCache)
{
ViewEngineResult areaResult = null;
if (controllerContext.RouteData.Values.ContainsKey("area")) {
string areaPartialName = FormatViewName(controllerContext, partialViewName);
areaResult = base.FindPartialView(controllerContext, areaPartialName, useCache);
if (areaResult != null && areaResult.View != null) {
return areaResult;
}
string sharedAreaPartialName = FormatSharedViewName(controllerContext, partialViewName);
areaResult = base.FindPartialView(controllerContext, sharedAreaPartialName, useCache);
if (areaResult != null && areaResult.View != null) {
return areaResult;
}
}
return base.FindPartialView(controllerContext, partialViewName, useCache);
}
private staticstring FormatViewName(ControllerContext controllerContext,string viewName)
{
string controllerName = controllerContext.RouteData.GetRequiredString("controller");
string area = controllerContext.RouteData.Values["area"].ToString();
return string.Format("Areas/{0}/Views/{1}/{2}", area, controllerName, viewName);
}
private staticstring FormatSharedViewName(ControllerContext controllerContext,string viewName)
{
string area = controllerContext.RouteData.Values["area"].ToString();
return string.Format("Areas/{0}/Views/Shared/{1}", area, viewName);
}
}
二、在Global.asax中指定自定义视图引擎,并注册Area路由
2.1 清空默认的视图引擎,并指定新的视图引擎为自定义的视图引擎
2.2 注册相关的Area路由
2.3 示例代码:
public class MvcApplication : System.Web.HttpApplication
{
public staticvoid RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", //默认的路由
"Yang/{controller}/{action}/{id}",
new { area="Yang",controller ="Root", action = "Index", id =UrlParameter.Optional },
new string[] {"MvcWebForTest2.Areas.Yang.Controllers" }
);
routes.MapRoute(
"Root", // 默认首页指定到一个Area下的HomeController的Index
"", // 空Url,即网站名的url
new { area="Xiao",controller ="Home", action = "Index", id =UrlParameter.Optional },
new string[] { "MvcWebForTest2.Areas.Xiao.Controllers" }
);
}
protected void Application_Start()
{
//清除原来所有的视图引擎
ViewEngines.Engines.Clear();
//加上自定义的视图引擎
ViewEngines.Engines.Add(newMyViewEngine());
//注册所有的Area
AreaRegistration.RegisterAllAreas();
//注册路由表
RegisterRoutes(RouteTable.Routes);
}
}
2.4 补充说明
在MVC2中,当在一个项目中添加一个Area后,VS会自动为这个Area添加一个继承自AreaRegistration的类型,这个类型的作用是,当2.3中的AreaRegistration.RegisterAllAreas()方法执行时,项目下所有继承自AreaRegistration的子类的对应的命名空间都会注册到路由表中去。AreaRegistration子类型的示例如下(VS自动生成):
using System.Web.Mvc;
namespace MvcWebForTest2.Areas.Yang
{
public classYangAreaRegistration:AreaRegistration
{
public overridestring AreaName
{
get
{
return "Yang";
}
}
public overridevoid RegisterArea(AreaRegistrationContext context)
{
context.MapRoute(
"Yang_default",
"Yang/{controller}/{action}/{id}",
new { action = "Index", id = "" }
);
}
}
}
通过对System.Web.Mvc的源码进行分析可以看出RegisterAllAreas()方法的执行是非常有趣的。
a、首先读取缓存中是否保存了相关的AreaRegistration子类型对应的Area的命名空间记录(注:缓存只在.net FrameWork4.0中才会启用)
b、如果缓存中没有找到,它通过反射,获取所有的Type相符(属于AreaRegistration子类)的子类型的命名空间(在.net FrameWork4.0中它会把找到的类型的命名空间序列化后写入缓存的"MVC-AreaRegistrationTypeCache.xml"的中),以下节选自System.Web.Mvc项目源码中的表述
// Go through all assemblies referenced by the application and search for types matching a predicate
上文中是否相符的type的判断条件
type != null && type.IsPublic && type.IsClass && !type.IsAbstract
且typeof(AreaRegistration).IsAssignableFrom(type) &&type.GetConstructor(Type.EmptyTypes) != null;
c、利用AreaRegistrationContext对象,对所有符合条件的命名空间进行注册
internal void CreateContextAndRegister(RouteCollection routes,object state) {
AreaRegistrationContext context =new AreaRegistrationContext(AreaName, routes, state);
string thisNamespace = GetType().Namespace;
if (thisNamespace != null) {
context.Namespaces.Add(thisNamespace + ".*");
}
RegisterArea(context);
}
(注:如果在global.ascx中没有执行RegisterAllAreas()或者在对应的area下没有添加继承自AreaRegistration的子类,在对应的Html方法输出Html.ActionLink时,即使指定了routeDictionary中的area参数,也会出现链接出错的情况,此时就需要自行在application_start中对这个area进行注册了,此部分详见第四大点)
三、在View中使用Html.ActionLink在不同Area之间进行跳转
<%=Html.ActionLink("链接到xiao","Me", "Home",new { area="Xiao" },null)%>
四、在global.ascx的Application_Start方法中自行注册Area的方式:
在项目中,若不喜欢在每个area的根文件夹下都生成一个继续自AreaRegistration子类的cs文件,或者在Application_Start()方法中没有调用AreaRegistration.RegisterAllAreas()方法,或者希望整个项目下的路由路径(URL)可以统一集中到Global中管理,我们也可以自行利用MapRoute或自定义扩展方法来注册area的命名空间。但不推荐此种做法。下面给出一个RouteCollection类型扩展CreateArea的方法的示例,以便在RegisterRoutes中一次性注册某个Area下的所有Controller,扩展方法CreateArea的示例代码如下:
namespace System.Web.Routing
{
//请注册扩展方法的命名空间
public staticclass RoutingExtension
{
public staticvoid CreateArea(thisRouteCollection routeCollection, string area, string controllerNameSpace, params Route[] routeEntities)
{
if (routeEntities == null || routeEntities.Length <= 0) {
return;
}
foreach (Route routein routeEntities) {
if (route.Constraints == null) {
route.Constraints = new RouteValueDictionary();
}
if (route.Defaults == null) {
route.Defaults = new RouteValueDictionary();
}
if (route.DataTokens == null) {
route.DataTokens = new RouteValueDictionary();
}
route.Constraints.Add("area", area);
//将一个area下的controllers命名空间加入
route.DataTokens.Add("namespaces",new[] { controllerNameSpace });
route.Defaults.Add("area", area);
if (!routeCollection.Contains(route)) {
routeCollection.Add(route);
}
}
}
}
}
然后在Global的RegisterRoutes方法中如下图所示调用CreateArea扩展方法注册某个area下的所有路由
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
// Area Yang下的控制器组注册
routes.CreateArea("Yang",
"MvcWebForTest2.Areas.Yang.Controllers",
new Route[]{
routes.MapRoute("yangRt1","Yang/{controller}/{action}", new { controller = "Home", action = "Index" }),
routes.MapRoute("yangRt2","Yang/myurl/{action}",new {controller="Home",action="Index"})
});
//默认路由及首页设置,定位到指定Area
routes.CreateArea("Xiao",
"MvcWebForTest2.Areas.Xiao.Controllers",
new Route[]{
routes.MapRoute("Default","Xiao/{controller}/{action}", new { controller = "Home", action = "Index" }),
routes.MapRoute("Root","", new { controller ="Home", action = "Index" })
});
}
这样的话,即使在每个area下没有继承自AreaRegistration的子类,或者没有调用RegisterAllAreas()方法,都可以正常地路由所有area下的页面。