MVC3 - Compiled Razor view can't find _ViewSta

2019-03-29 09:14发布

问题:

I'm using compiled Razor views in separate class libraries as a sort of plugin system for MVC3.

I've followed the guide by Chris Van De Steed here and only deviated primarily on the part about adding references, as I'm loading my assemblys at runtime.

Because I'm loading assemblies at runtime, I'm not using the VirtualPathProviderViewEngine in the BoC library, and have instead implemented my own ViewEngine based on RazorViewEngine. It works by rewriting the viewPath in CreateView to insert the appropriate namespace so that the view can be resolved.

So far so good... I can have different modules loaded, and their controllers won't collide if they share the same name.

The only problem I have now is that for compiled views, the _ViewStart is not called. The _ViewStart works for views in the host MVC3 project, but for any views loaded from the plugin assemblies it's not found.

I have a Route setup like this:-

RouteTable.Routes.MapRoute(
    string.Format("Plugin{0}Route", pluginName),
    string.Format(@"Plugin/{0}/{{controller}}/{{action}}", pluginName),
    new { },
    new string[] { string.Format("{0}.Controllers", pluginName) });

the ViewEngine looks like this:-

public class PluginRazorViewEngine : RazorViewEngine
{
    public PluginRazorViewEngine() : base()
    {
        ViewLocationFormats = new[]
        {
            "~/Plugin/%1/Views/{1}/{0}.cshtml",
            "~/Plugin/%1/Views/{1}/{0}.vbhtml",
            "~/Plugin/%1/Views/Shared/{0}.cshtml",
            "~/Plugin/%1/Views/Shared/{0}.vbhtml",
            "~/Views/{1}/{0}.cshtml",
            "~/Views/{1}/{0}.vbhtml",
            "~/Views/Shared/{0}.cshtml",
            "~/Views/Shared/{0}.vbhtml"
        };

(the %1 is replaced with the name of the assembly)

and the assembly is registered with the BoC library like this:-

BoC.Web.Mvc.PrecompiledViews.ApplicationPartRegistry.Register(assembly, string.Format("/Plugin/{0}/", pluginName));

When the view is loaded from a plugin assembly (in this example "accounts"), the view is found and displayed OK. But then it looks in these locations for the _ViewStart:-

~/plugin/accounts/views/invoice/_viewstart.cshtml
~/plugin/accounts/views/invoice/_viewstart.vbhtml
~/plugin/accounts/views/_viewstart.cshtml
~/plugin/accounts/views/_viewstart.vbhtml
~/plugin/accounts/_viewstart.cshtml
~/plugin/accounts/_viewstart.vbhtml
~/plugin/_viewstart.cshtml
~/plugin/_viewstart.vbhtml
~/_viewstart.cshtml
~/_viewstart.vbhtml

But it doesn't look in ~/Views/Shared/_ViewStart.cshtml where the file lives.

I've tried changing all the location formats in my ViewEngine (AreaMasterLocationFormats, AreaPartialViewLocationFormats, AreaViewLocationFormats, MasterLocationFormats, PartialViewLocationFormats and ViewLocationFormats) but none of them seem to make a difference.

I've looked around and it seems that System.Web.WebPages.StartPage.GetStartPage is responsible for finding and returning the start page in a view, but I can't find any information on how to control where it looks.

I've tried moving the _ViewStart.cshtml to ~/_ViewStart.cshtml (one of the places it looks) however I immediately get:-

Unable to cast object of type 'ASP._Page__ViewStart_cshtml' to type 'System.Web.WebPages.StartPage'.

Which according to what I've read, is because the _ViewStart needs to live under /Views

Can I modify where MVC looks for a _ViewStart?

The BoC library implements it's own IView, and calls the following:-

startPage = this.StartPageLookup(page, VirtualPathFactoryManagerViewEngine.ViewStartFileName, this.ViewStartFileExtensions);

But in this case ViewStartFileName is just "_ViewStart" and ViewStartFileExtensions are just cshtml and vbhtml... nothing that would control where MVC should search for the file.

回答1:

An idea... (as in, haven't tried it. Will it work? No idea)

Maybe have a look at inheriting from RazorView (or replacing it entirely, considering - as we'll see - you'll be rewriting the one method that is the bulk of the class).

RazorView is where StartPage.GetStartPage is introduced by way of assigning it to a StartPageLookup property:

// In RazorView constructor:
StartPageLookup = StartPage.GetStartPage;

Unfortunately, that delegate property is internal, so you can't just overwrite it in your derived class' constructor. You may, however, be able to override RazorView.RenderView, which is where it's used (MVC3 source code, lots of lines removed, line-breaks added by me):

protected override void RenderView(ViewContext viewContext, TextWriter writer, 
   object instance) 
{
  // [SNIP]

  WebPageRenderingBase startPage = null;
  if (RunViewStartPages) {
     // HERE IT IS:
     startPage = StartPageLookup(
        webViewPage, 
        RazorViewEngine.ViewStartFileName, 
        ViewStartFileExtensions
     );
  }
  webViewPage.ExecutePageHierarchy(
     new WebPageContext(
        context: viewContext.HttpContext, 
        page: null, 
        model: null),
     writer, startPage);
}

Replace that StartPageLookup call with your own lookup, then replace the result of CreateView and CreatePartialView in your PluginRazorViewEngine with your new PluginRazorView class.



回答2:

So, to answer my own question... it seems like no, you can't modify where MVC looks for the _ViewStart...

Looking at the source of System.Web.WebPages.StartPage.GetStartPage (which I got from here) I can see that it only traverses the path from the calling page all the way up to the root, and there doesn't seem to be any way to control this behavior (i.e. it's hard-coded in System.Web.WebPages.StartPage)

Under normal circumstances (i.e. a standard MVC3 layout) this would be OK... all views sit under /Views and so does the main _ViewStart file, which would get evaluated when GetStartPage reached it.

So what I've basically done is break this functionality by moving my views out of the /Views folder hierarchy.

I guess this means I can either move my _ViewStart file into a spot where it will be found (which kind of breaks my intended goal of having a common _ViewStart for all my plugins) or work out some way of re-writing/re-directing on of the requests for _ViewStart in that hierarchy to the correct file in /Views/Shared/_ViewStart (which is not immediately apparent to me).

What's interesting is that the MVC3 code will look for _ViewStart in /, which based on what I've read won't work, resulting in the "Unable to cast object of type 'ASP.Page_ViewStart_cshtml' to type 'System.Web.WebPages.StartPage'" error (though I suspect this is just because the default /web.config doesn't have the necessary stuff in it to properly parse the file, whereas /Views/web.config does)