使用自定义 RazorViewEngine 和 RazorGenerator 预编译视图

Using a custom RazorViewEngine AND RazorGenerator precompiled views

我正在尝试使用自定义(派生)RazorViewEngine 和使用 RazorGenerator 的预编译视图。

一些上下文:

我们有一个用于多个客户端实现的基础产品。这样我们就有了一组核心的基本观点。大多数视图在大多数时间都有效。现在我们最终为每个新解决方案复制现有视图并根据需要进行修改。这最终导致 95% 的观点在客户之间是相同的,而 5% 的观点发生了变化。

我想做的是采用一组基本视图,将它们编译成 DLL 并在客户端之间重复使用。到目前为止,我使用 RazorGenerator 的效果很好。

现在下一步是允许自定义(覆盖)视图。但有一个警告。我们的应用程序有两个 "modes" 用户处于其中。他们所处的模式可能需要不同的视图。

我已经从 RazorGeneratorView 创建了派生的 class。此视图基本上检查 Autofac 解析的 UserProfile 对象中的 "OrderingMode"。基于模式 - 路径定位器被替换为视图分辨率。

个人客户端应用程序的想法将首先尝试在传统的 Views 文件夹中解析视图。只有我在 Views/{OrderingMode}/{Controller}/{View}.cshtml 的子目录中添加。

如果未找到视图 - 那么它将在已编译的库(核心视图)中查找。

这让我可以根据客户的需要覆盖个别视图/局部视图。

    public PosViewEngine() : base()
    {
        //{0} = View Name
        //{1} = ControllerName
        //{2} = Area Name
        AreaViewLocationFormats = new[]
        {
            //First look in the hosting application area folder / Views / ordering type
            //Areas/{AreaName}/{OrderType}/{ControllerName}/{ViewName}.cshtml
            "Areas/{2}/Views/%1/{1}/{0}.cshtml",

            //Next look in the hosting application area folder / Views / ordering type / Shared
            //Areas/{AreaName}/{OrderType}/{ControllerName}/{ViewName}.cshtml
            "Areas/{2}/Views/%1/Shared/(0}.cshtml",

            //Finally look in the IMS.POS.Web.Views.Core assembly
            "Areas/{2}/Views/{1}/{0}.cshtml"
        };

        //Same format logic
        AreaMasterLocationFormats = AreaViewLocationFormats;

        AreaPartialViewLocationFormats = new[]
        {
             //First look in the hosting application area folder / Views / ordering type
            //Areas/{AreaName}/{OrderType}/{ControllerName}/Partials/{PartialViewName}.cshtml
            "Areas/{2}/Views/%1/{1}/Paritals/{0}.cshtml",

            //Next look in the hosting application area folder / Views / ordering type / Shared
            //Areas/{AreaName}/{OrderType}/{ControllerName}/{ViewName}.cshtml
            "Areas/{2}/Views/%1/Shared/(0}.cshtml",

            //Finally look in the IMS.POS.Web.Views.Core
            "Areas/{2}/Views/{1}/{0}.cshtml"
        };

        ViewLocationFormats = new[]
        {
            "Views/%1/{1}/{0}.cshtml",
            "Views/%1/Shared/{0}.cshtml",
            "Views/{1}/{0}.cshtml",
            "Views/Shared/{0}.cshtml"
        };

        MasterLocationFormats = ViewLocationFormats;

        PartialViewLocationFormats = new[]
        {
            "Views/%1/{1}/Partials/{0}.cshtml",
            "Views/%1/Shared/{0}.cshtml",
            "Views/{1}/Partials/{0}.cshtml",
            "Views/Shared/{0}.cshtml"
        };




    }

    protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
    {
        return base.CreatePartialView(controllerContext, partialPath.ReplaceOrderType(CurrentOrderingMode()));
    }

    protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
    {
        OrderType orderType = CurrentOrderingMode();
        return base.CreateView(controllerContext, viewPath.ReplaceOrderType(orderType), masterPath.ReplaceOrderType(orderType));
    }

    protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
    {
        return base.FileExists(controllerContext, virtualPath.Replace("%1/",string.Empty));
    }


    private OrderType CurrentOrderingMode()
    {
        OrderType result;
        _profileService = DependencyResolver.Current.GetService<IUserProfileService>();

        if (_profileService == null || _profileService.OrderingType == 0)
        {
            IApplicationSettingService settingService =
                DependencyResolver.Current.GetService<IApplicationSettingService>();

            result =
                settingService.GetApplicationSetting(ApplicationSettings.DefaultOrderingMode)
                    .ToEnumTypeOf<OrderType>();
        }
        else
        {
            result = _profileService.OrderingType;
        }

        return result;
    } 



}

这是 StartUp class RazorGenerator 用来注册 ViewEngine。

public static class RazorGeneratorMvcStart
{
    public static void Start()
    {
        var engine = new PrecompiledMvcEngine(typeof(RazorGeneratorMvcStart).Assembly)
        {
            UsePhysicalViewsIfNewer = HttpContext.Current.Request.IsLocal
        };

        ViewEngines.Engines.Insert(0, engine);

        // StartPage lookups are done by WebPages.
        VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
    }
}

问题是:

  1. 此代码最后执行(在我注册 PosViewEngine 之后)并将引擎插入第一个位置(这意味着这是在提供响应时首先解析的引擎)。这最终找到了一个视图——它是核心视图。
  2. 如果我将 StartUp 中的代码更改为先注册我的自定义视图引擎,然后再注册 RazorGenerator 引擎

     public static void Start()
    {
        var engine = new PrecompiledMvcEngine(typeof(RazorGeneratorMvcStart).Assembly)
        {
            UsePhysicalViewsIfNewer = HttpContext.Current.Request.IsLocal
        };
    
        ViewEngines.Engines.Clear();
        ViewEngines.Engines.Insert(0, new PosViewEngine());
        ViewEngines.Engines.Insert(1, engine);
    
        // StartPage lookups are done by WebPages.
        VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
    }
    

我最终遇到了 FileExists(ControllerContext controllerContext, string virtualPath) 方法的异常 - "The relative virtual path 'Views/Account/LogOn.cshtml' is not allowed here."

这显然与物理路径和虚拟路径混合在一起有关。

看起来其他人也在尝试做同样的事情 here 但我没有看到这方面的答案。

对于任何其他想要尝试这种方法的人,我会 post 给出答案。基本上,您需要实现一个自定义视图引擎,该引擎派生自 RazorGenerator 程序集中的 PrecompiledMvc​​Engine。

public class PosPrecompileEngine : PrecompiledMvcEngine
{
    private IUserProfileService _profileService;



    public PosPrecompileEngine(Assembly assembly) : base(assembly)
    {
        LocatorConfig();
    }

    public PosPrecompileEngine(Assembly assembly, string baseVirtualPath) : base(assembly, baseVirtualPath)
    {
        LocatorConfig();
    }

    public PosPrecompileEngine(Assembly assembly, string baseVirtualPath, IViewPageActivator viewPageActivator) : base(assembly, baseVirtualPath, viewPageActivator)
    {
        LocatorConfig();
    }

    protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath)
    {
        return base.CreatePartialView(controllerContext, partialPath.ReplaceOrderType(CurrentOrderingMode()));
    }

    protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath)
    {
        OrderType orderType = CurrentOrderingMode();
        return base.CreateView(controllerContext, viewPath.ReplaceOrderType(orderType), masterPath.ReplaceOrderType(orderType));
    }

    protected override bool FileExists(ControllerContext controllerContext, string virtualPath)
    {
        return base.FileExists(controllerContext, virtualPath.ReplaceOrderType(CurrentOrderingMode()));
    }
}

在此 class - 我覆盖了定位器路径。因为我在 Web 应用程序的另一个程序集中有 "base" 编译视图 - 我们实现了一个约定,视图引擎将首先在 Web 中的 PosViews/{ordering mode}/{controller}/{view} 路径中查找应用。如果视图未定位 - 那么它将在传统的 /Views/controller/view 中查找。这里的技巧是后者是位于另一个 class 库中的虚拟路径。

这使我们能够 "override" 应用程序的现有视图。

    private void LocatorConfig()
    {
        //{0} = View Name
        //{1} = ControllerName
        //{2} = Area Name
        AreaViewLocationFormats = new[]
        {
            //First look in the hosting application area folder / Views / ordering type
            //Areas/{AreaName}/{OrderType}/{ControllerName}/{ViewName}.cshtml
            "PosAreas/{2}/Views/%1/{1}/{0}.cshtml",

            //Next look in the hosting application area folder / Views / ordering type / Shared
            //Areas/{AreaName}/{OrderType}/{ControllerName}/{ViewName}.cshtml
            "PosAreas/{2}/Views/%1/Shared/(0}.cshtml",

            //Next look in the POS Areas Shared
            "PosAreas/{2}/Views/Shared/(0}.cshtml",

            //Finally look in the IMS.POS.Web.Views.Core assembly
            "Areas/{2}/Views/{1}/{0}.cshtml"
        };

        //Same format logic
        AreaMasterLocationFormats = AreaViewLocationFormats;

        AreaPartialViewLocationFormats = new[]
        {
             //First look in the hosting application area folder / Views / ordering type
            //Areas/{AreaName}/{OrderType}/{ControllerName}/Partials/{PartialViewName}.cshtml
            "PosAreas/{2}/Views/%1/{1}/Partials/{0}.cshtml",

            //Next look in the hosting application area folder / Views / ordering type / Shared
            //Areas/{AreaName}/{OrderType}/{ControllerName}/{ViewName}.cshtml
            "PosAreas/{2}/Views/%1/Shared/(0}.cshtml",

            //Next look in the hosting application shared folder
            "PosAreas/{2}/Views/Shared/(0}.cshtml",

            //Finally look in the IMS.POS.Web.Views.Core
            "Areas/{2}/Views/{1}/{0}.cshtml"
        };

        ViewLocationFormats = new[]
        {
            "~/PosViews/%1/{1}/{0}.cshtml",
            "~/PosViews/%1/Shared/{0}.cshtml",
            "~/PosViews/Shared/{0}.cshtml",
            "~/Views/{1}/{0}.cshtml",
            "~/Views/Shared/{0}.cshtml"
        };

        MasterLocationFormats = ViewLocationFormats;

        PartialViewLocationFormats = new[]
        {
            "~/PosViews/%1/{1}/{0}.cshtml",
            "~/PosViews/%1/Shared/{0}.cshtml",
             "~/PosViews/Shared/{0}.cshtml",
            "~/Views/{1}/{0}.cshtml",
            "~/Views/Shared/{0}.cshtml"
        };
    }

在您的应用程序启动事件中注册此引擎。

   public static void Configure()
    {
        var engine = new PosPrecompileEngine(typeof(ViewEngineConfig).Assembly)
        {
            UsePhysicalViewsIfNewer = true,
            PreemptPhysicalFiles = true
        };
        ViewEngines.Engines.Add(engine);

        // StartPage lookups are done by WebPages.
        VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
    }

这是最后的关键。安装 RazorGenerator 后查看 NuGet - 你最终会得到这个启动 class,它将 运行 在启动时

[assembly: WebActivatorEx.PostApplicationStartMethod(typeof(Views.Core.RazorGeneratorMvcStart), "Start")]


public static class RazorGeneratorMvcStart
{
    public static void Start()
    {
        var engine = new PrecompiledMvcEngine(typeof(RazorGeneratorMvcStart).Assembly)
        {
            UsePhysicalViewsIfNewer = true,
            PreemptPhysicalFiles = true
        };
        ViewEngines.Engines.Add(engine);

        // StartPage lookups are done by WebPages.
        VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
    }
} 

默认情况下 - RazorGenerator 将 ViewEngine 添加到集合中的第一个

ViewEngines.Engines.Insert(0,engine);

您需要将其更改为添加

ViewEngines.Engines.Add(engine); 

因此它是最后添加到引擎中的 - 这样您的自定义 ViewEngine 将首先用于定位视图。

这种方法允许您在多个应用程序中重用视图,同时允许覆盖该视图的方法。

对于大多数应用程序来说,这可能有点矫枉过正 - 正如我在问题中提到的那样 - 这是我们用来开发多个客户端应用程序的基础产品。尝试实现重用,同时在每个客户端的基础上保持一定程度的灵活性是我们试图实现的目标。