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类里有所体现的,是一个集合。所以我们需要做的就是给符合条件的ControllerModelRouteConstraints集合里添加一个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就看到新的页面了。

posted on 2015-05-13 17:18  Jason Li  阅读(620)  评论(0编辑  收藏  举报