核心思想

无论采用哪种方法,多模板实现的核心思想都是“根据某种条件动态地切换视图引擎查找视图的路径”,ASP.NET MVC 的视图引擎(如 RazorViewEngine)有一个 ViewLocationFormats 属性,它是一个字符串数组,定义了查找视图的路径顺序,我们的任务就是在运行时修改这个属性,或者自定义一个视图引擎来使用我们自己的路径。

asp.net mvc 多模板支持
(图片来源网络,侵删)

基于约定,动态修改 ViewEngine(推荐,最常用)

这是最灵活且最推荐的方法,它不依赖于第三方库,完全利用 MVC 框架的扩展点,基本思路是:在 ControllerAction 执行之前,根据当前请求的上下文(如 URL 参数、用户角色、域名等)动态设置视图引擎的搜索路径。

实现步骤

  1. 创建一个自定义的 Action Filter:这是最优雅的方式,因为它可以将模板选择逻辑与业务代码分离。
  2. 在 Filter 中修改 ViewEngine 的路径

代码示例

创建自定义 Action Filter

using System.Web.Mvc;
public class TemplateSelectorAttribute : ActionFilterAttribute
{
    // 通过构造函数传入模板名称
    public string TemplateName { get; }
    public TemplateSelectorAttribute(string templateName)
    {
        TemplateName = templateName;
    }
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        // 获取当前的视图引擎集合
        var viewEngines = ViewEngines.Engines;
        // 我们只关心第一个默认的 RazorViewEngine
        if (viewEngines[0] is RazorViewEngine razorViewEngine)
        {
            // 保存原始路径,以便在请求结束后恢复(可选,但推荐)
            var originalLocations = razorViewEngine.ViewLocationFormats.ToArray();
            // 构建新的视图路径
            // 新的路径优先级高于默认路径
            var newViewLocationFormats = new[]
            {
                // "~/Views/Shared/{0}.cshtml", // 默认共享视图
                $"~/Views/Templates/{TemplateName}/{{0}}.cshtml", // 指定模板的视图
                $"~/Views/Templates/{TemplateName}/Shared/{{0}}.cshtml", // 指定模板的共享视图
                // 可以继续添加其他默认路径...
            };
            // 合并新旧路径,确保自定义模板路径优先查找
            razorViewEngine.ViewLocationFormats = newViewLocationFormats.Concat(originalLocations).ToArray();
            // 同样,也需要修改 MasterLocationFormats 和 AreaViewLocationFormats
            // ... (类似逻辑)
        }
        base.OnActionExecuting(filterContext);
    }
    // 可选:在请求结束后恢复原始路径,避免影响其他请求
    // 这在 ASP.NET 的托管模型中可能不是必须的,因为每个请求都是独立的,
    // 但作为一种良好的防御性编程习惯,可以考虑。
    public override void OnResultExec(ResultExecutingContext filterContext)
    {
        base.OnResultExecuting(filterContext);
    }
}

在 Controller 或 Action 上使用 Filter

你可以轻松地在任何 Controller 或 Action 上应用这个模板。

asp.net mvc 多模板支持
(图片来源网络,侵删)
public class HomeController : Controller
{
    // 整个 HomeController 都使用 "Admin" 模板
    [TemplateSelector("Admin")]
    public ActionResult Index()
    {
        return View();
    }
    // 这个 Action 使用 "Mobile" 模板
    [TemplateSelector("Mobile")]
    public ActionResult About()
    {
        return View();
    }
    // 这个 Action 使用默认模板
    public ActionResult Contact()
    {
        return View();
    }
}

创建对应的文件夹结构

在你的 Views 文件夹下,创建 Templates 文件夹,并在其中为每个模板创建子文件夹。

/Views
  /Templates
    /Admin
      /Home
        Index.cshtml
      /Shared
        _Layout.cshtml
    /Mobile
      /Home
        About.cshtml
      /Shared
        _Layout.cshtml
  /Home
    Index.cshtml (默认模板)
  /Shared
    _Layout.cshtml (默认模板)

优点

  • 灵活:可以根据任何条件(URL、用户登录状态、数据库配置等)动态选择模板。
  • 非侵入性:通过 AOP(面向切面编程)实现,不污染业务逻辑。
  • 性能高:没有额外的文件系统或数据库查询开销。
  • 易于维护:模板结构清晰,与默认 MVC 项目结构一致。

缺点

asp.net mvc 多模板支持
(图片来源网络,侵删)
  • 需要手动管理多个 _Layout.cshtml 和其他共享视图,可能导致代码重复。

基于 Area 的多模板

如果你的模板之间有非常强的业务隔离,并且每个模板都有自己独立的 Controller、Model 和 View,那么使用 ASP.NET MVC 的 Area (区域) 功能是最佳选择,Area 本质上就是一个小型的、独立的 MVC 应用。

实现步骤

  1. 创建 Area:在项目中右键 -> 添加 -> 区域...,命名为 AdminMobile
  2. 结构:每个 Area 都会拥有自己的 ControllersModelsViews 文件夹,以及独立的 _ViewStart.cshtml_Layout.cshtml
/YourProject
  /Areas
    /Admin
      /Controllers
        HomeController.cs
      /Views
        /Home
          Index.cshtml
        /Shared
          _Layout.cshtml
      /Models
    /Mobile
      /Controllers
        HomeController.cs
      /Views
        /Home
          Index.cshtml
        /Shared
          _Layout.cshtml
      /Models
  /Controllers
  /Views
    /Shared
      _Layout.cshtml (默认布局)
  1. 路由配置:在 RouteConfig.cs 中,Area 的路由会自动注册,你可以通过 {area} 路由值来指定使用哪个 Area。
// 在 Global.asax 或 Startup.cs 中注册 Area
AreaRegistration.RegisterAllAreas();
// 在 RouteConfig 中配置路由
routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
    namespaces: new[] { "YourProject.Controllers" } // 指定默认命名空间
);
// Area 会自动生成类似这样的路由:
// {area}/{controller}/{action}/{id}
  1. 访问:通过 URL 访问,http://yourdomain.com/Admin/Home/Index 会使用 Admin Area 的模板。

优点

  • 高度隔离:每个模板的代码、路由、视图完全独立,非常适合大型、复杂的模板。
  • 结构清晰:项目结构非常规整,易于团队协作。
  • 路由强大:可以轻松为每个 Area 配置独立的前缀或域名。

缺点

  • 不灵活:模板是硬编码在路由中的,不适合在运行时根据用户选择动态切换。
  • 代码冗余:如果多个模板共享大量 Controller 或 Model 逻辑,会导致代码重复。

使用第三方库(如 SmartStore.NET)

一些开源的电子商务框架(如 SmartStore.NET)为了实现高度灵活的主题系统,开发了非常复杂的视图引擎替换方案,它们通常通过以下方式实现:

  1. 自定义 VirtualPathProvider:拦截所有对视图文件的请求,从数据库或文件系统中的自定义位置(如 ~/Themes/ThemeName/Views/)加载视图文件。
  2. 动态编译:将主题中的 .cshtml 文件作为独立的资源进行动态编译和加载。
  3. 覆盖机制:提供一种机制,允许主题“覆盖”主应用程序中的视图文件。

优点

  • 极其灵活:主题可以像插件一样安装、卸载、切换,无需重新编译网站。
  • 功能强大:通常包含主题配置、资源打包等高级功能。

缺点

  • 复杂度高:实现和维护成本非常高。
  • 性能开销:动态文件提供和编译会增加性能开销。
  • 依赖性强:引入了第三方库的依赖。

适用场景:构建一个需要用户或管理员可以随时更换“皮肤”或“主题”的 SaaS(软件即服务)平台或电商网站。


总结与选择建议

方法 灵活性 复杂度 隔离性 适用场景
动态修改 ViewEngine 极高 绝大多数场景,需要根据用户、URL、配置等动态切换界面风格的系统。
基于 Area 极高 模板间业务逻辑完全独立,有独立路由需求的应用,如后台管理、移动端API等。
第三方库 极高 极高 需要实现“热插拔”式主题系统的 SaaS 平台或电商网站。

给你的建议

  • 如果你只是想在同一个应用中为不同用户或不同入口提供不同的界面,强烈推荐使用方法一(动态修改 ViewEngine),它是最简单、最直接、最符合 MVC 设计思想的方式。
  • 如果你的后台管理系统和前台展示系统在业务和路由上完全不同,可以考虑使用 方法二(Area) 来组织代码。
  • 除非你正在构建一个复杂的、需要用户自由切换主题的商业级平台,否则不要轻易尝试方法三,因为它会大大增加你的项目复杂度和维护成本。