Asp.Net Mvc里有一个叫做Area的技术,就是可以把不同逻辑组件的controller, view等放到不同的文件夹里。比如所有管理相关的都放到Admin area里。其实之前我一直对这个功能不太感冒,并不觉得能解决什么大问题,如果项目规模较大的话,肯定是很多开发协同开发,这时候很大的一个问题是版本库冲突的问题以及一个模块build不过导致其他模块受难。我觉得area并不能解决这个问题,直到我看到了Orchard的源码。
Orchard的思路简单来说就是,在Areas文件夹下建了好几个web工程,因为web工程下面会有Controllers, Views的文件夹结构,是完全符合area的结构定义的,然后把这个Areas文件夹从web project里exclude出去,在工程里就看不到了,最后再把这些工程加到solution里。这样不同模块的开发可以创建多个solution,每个solution里只引用自己所需的最少的工程就可以,避免了版本库冲突以及build失败的问题了。
当初我看到这种做法的时候不由得眼前一亮,这种结构可以很好地解决协同开发的问题,而且实现组件化,热插拔也都方便了许多。所以当开始学习Asp.Net 5(area实际上是Mvc的概念,说Mvc 6更严谨)的时候,一开始就想到了试试新框架的area功能。结果发现和以前的版本并不太一样。这里不做比较了,直接上代码。
首先在web工程里创建出Areas文件夹以及相应的结构目录
-Areas
--Management
---Controllers
----HomeController.cs
--Views
---_ViewStart.cshtml
---Home
----Index.cshtml
HomeController的Index action直接return View();
, _ViewStart.cshtml是指定layout的,和web工程里的一样,当然你可以自己定义新的模板,自己可以添加_GlobalImport.cshtml以加入TagHelper和命名空间之类的,不多说。
真正的工作有两样,一是设置路由,二是标记Area。目前没发现MVC5里的area自动发现功能,所以需要给area的controller加上Area
标签。
[Area("Management")]
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
}
然后在web工程的Startup里添加路由。
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerfactory)
{
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
routes.MapRoute(
name: "area",
template: "{area:exists}/{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index" });
});
}
启动项目,浏览器里输入url localhost:8888/Management
就看到这个Management area的home页面了。
然后问题来了,我们写代码的时候,很多情况下都需要做一些自定义的东西,下面来看一下一些自定义的方法,主要处理三个问题:1,文件夹名不叫Areas,而是叫modules;2,不在Controller上加Area标签,自动发现;3,url里不包含area的名字。
第一个问题很好解决,就是告诉Razor引擎寻找View的时候别在Areas文件夹下找就可以了。实现一个自定义的RazorViewEngine,然后改变一下默认的搜索路径。别忘了把之前的Areas文件夹改名为modules。
public class CustomLocationRazorViewEngine : RazorViewEngine
{
public CustomLocationRazorViewEngine(IRazorPageFactory pageFactory, IRazorViewFactory viewFactory, IViewLocationExpanderProvider viewLocationExpanderProvider, IViewLocationCache viewLocationCache)
: base(pageFactory, viewFactory, viewLocationExpanderProvider, viewLocationCache)
{
var areaViewLocationFormats = AreaViewLocationFormats as string[];
areaViewLocationFormats[0] = "/modules/{2}/Views/{1}/{0}.cshtml";
areaViewLocationFormats[1] = "/modules/{2}/Views/Shared/{0}.cshtml";
}
}
第三个问题也比较好解决,只要在制定路由规则的时候,把area信息传一下就行了。
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
routes.MapRoute(
name: "area",
template: "manage/{controller}/{action}/{id?}",
defaults: new { controller = "Home", action = "Index", area = "Management" });
});
第二个问题略微麻烦一点,什么样的Controller才是area里的Controller呢?即便能判定出这样的Controller来,又把它算到哪个area下面去呢?这个其实各个项目都有不同的需求,这里就假设,主web工程里的Controller的namespace都是AspNetAreaDemo.Controllers,除了这种命名空间的,都是area里的Controller,而area名字也通过namespace来体现。就是默认area Controller的命名空间都是XXX.XXX.XXX.AreaName.Controllers,这样用.
分割一下,取倒数第二个就可以了。实际开发中,可能会以assembly的name,project的name等来作为area name,甚至干脆维护一个映射。
现在策略有了,怎么才能自动地给Controller加上Area标签呢。看看源码,AreaAttribute
实际上是IRouteConstraintProvider
接口的一个实现类,而这个接口是在ControllerModel
类里有所体现的,是一个集合。所以我们需要做的就是给符合条件的ControllerModel
的RouteConstraints
集合里添加一个AreaAttribute
。代码如下
public class AreaAutoDiscoverControllerModelConvention : IControllerModelConvention
{
public void Apply(ControllerModel controller)
{
var ns = controller.ControllerType.Namespace;
if (ns != "AspNetAreaDemo.Controllers")
{
var segments = ns.Split(new char[] { '.' });
var areaName = segments[segments.Length - 2];
controller.RouteConstraints.Add(new AreaAttribute(areaName));
}
}
}
准备工作做完了,最后只要把自定义的RazorViewEngine和ControlleModelConvention添加到程序里就可以了。还是Startup类。
public void ConfigureServices(IServiceCollection services)
{
services.Configure<AppSettings>(Configuration.GetSubKey("AppSettings"));
services.AddMvc();
services.ConfigureMvc(options =>
{
options.ViewEngines.Clear();
options.ViewEngines.Add(new ViewEngineDescriptor(typeof(CustomLocationRazorViewEngine)));
options.Conventions.Add(new AreaAutoDiscoverControllerModelConvention());
});
}
启动程序,输入url localhost:8888/manage
就看到新的页面了。