We have an ASP.NET MVC 4 application with around 3000 views in it. We've decided to split this set of views into separated DLLs and compile it with RazorGenerator. We keep only main _Layout.cshtml and related files in the main MVC project.
We cannot load partial views from DLL together with master view in main MVC project. Detailed description is below.
What is already done:
The views compile successfully into DLLs (I've confirmed that they are in the binary)
The PrecompiledMvcEngine object is created and registered for each DLL containing views using the code below in Application_Start in Global.asax.cs:
.
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
// ...
// some code determining whether we've got an assembly with views
// ...
var engine = new PrecompiledMvcEngine(assembly);
engine.UsePhysicalViewsIfNewer = true;
ViewEngines.Engines.Insert(0, engine);
// StartPage lookups are done by WebPages.
VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
}
What does not work:
I cannot load a view defined in the main MVC project (say _Layout.cshtml) with partial view defined in one of the libraries (say Partial.cshtml). I use the following code in controller's action to tell the MVC framework which view I requested:
var view = "~/Views/" + partialName + ".cshtml";
return View(view, "~/Views/Shared/_Layout.cshtml", model);
The error messages says:
The view '~/Views/Partial.cshtml' or its master was not found or no view engine supports the searched locations. The following locations were searched:
~/Views/Partial.cshtml
~/Views/Shared/_Layout.cshtml
When I attempt to load the views separately by specifying either:
return View("~/Views/Shared/_Layout.cshtml", model);
or
return View(view, model);
, the right view is found. However I need them to be loaded together. The code works when I have all required .cshtml files in the main MVC project.
Note that the views in compiled DLLs have PageVirtualPathAttribute with the same path as specified in the controller action, e.g.:
namespace SomeBaseNamespace.Views
{
[GeneratedCode("RazorGenerator", "1.5.0.0"), PageVirtualPath("~/Views/Partial.cshtml")]
public class Partial : WebViewPage<PartialModel>
{
[CompilerGenerated]
private static class <Execute>o__SiteContainer3
{
// logic
}
public override void Execute()
{
// logic
}
}
}
To sum up, the question is how to call the master view stored in main MVC project with a partial compiled view defined in another project?
At app start, when your app calls this line...
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
The assemblies containing your external views have likely not yet been loaded, and are therefore not included as view engines. I'd actually recommend against using AppDomain.CurrentDomain.GetAssemblies()
anyway, as that will include all assemblies loaded at startup.
The solution is to add the RazorGenerator.Mvc NuGet package to each project which contains compiled views. This will add the following app start code in a similar manner to yours...
[assembly: WebActivatorEx.PostApplicationStartMethod(typeof(SomeBaseNamespace.Views.RazorGeneratorMvcStart), "Start")]
namespace SomeBaseNamespace.Views
{
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);
}
}
}
Note how this creates a view engine using the current assembly (your views assembly) and adds it to the static ViewEngines
collection (contained within the main MVC project).
Once in production, I'd also recommend turning off the UsePhysicalViewsIfNewer
setting, which adds a significant performance overhead.
Terminology
BaseMvc - with Razor Generated Views, Controllers etc
ConsumerMvc - Has layout for this project and references BaseMvc
Summary
Create the delivery of the view in the base controller. The view uses a layout which is present in the ConsumerMvc via the _ViewStart.cshtml in BaseMvc. For my situation I had projects with differing layouts, hence the "pointer" layout view. I thought it a useful example.
BaseMvc Example
I created an AREA
so I could set a default layout.
/Areas/Components/Controllers/ShoppingController.cs
public ActionResult Basket()
{
return View();
}
/Areas/Components/Views/Shopping/Basket.cshtml
Welcome to the Basket!
/Areas/Components/Views/_ViewStart.cshtml
@{
//-- NOTE: "Layout_Component.cshtml" do not exist in the BaseMVC project. I did not
// experiment with having it in both projects. A tip if you do is to ensure both
// the base and consumer _Layout_Component.cshtml files are both razor
// generated to allow the razor generator to handle the overrride. See
// my other SO answer linked below.
Layout = "~/Views/Shared/_Layout_Component.cshtml";
}
Link referenced in the code comment: Override view in ASP.NET MVC site not working
ConsumerMvc Example
/Views/Shared/_Layout_Component.cshtml
@{
Layout = "~/Views/Shared/_Layout_ConsumerMvc.cshtml";
}
@RenderBody()
My Url
http://www.consumermvc.example.com/Components/Shopping/Basket
Not all assemblies are loaded when Application_Start
is called. Add an extra handler:
AppDomain.CurrentDomain.AssemblyLoad += (sender, args) =>
{
// ...
// some code determining whether we've got an assembly with views
// ...
var engine = new PrecompiledMvcEngine(args.LoadedAssembly);
engine.UsePhysicalViewsIfNewer = true;
ViewEngines.Engines.Insert(0, engine);
// StartPage lookups are done by WebPages.
VirtualPathFactoryManager.RegisterVirtualPathFactory(engine);
}