Ok so I have this interesting ASP.NET MVC 4 solution/project structure, which creates pluggable application modules. I created it following this technique:
As a result, I have a main application with an empty Areas folder in the project. I also have a Plugin project that resides in the Areas folder of the main application on disk, and it also sets its build output folder to the main application \bin
folder.
In my pluggable module application, I decided to create an Areas section within it, and created an Area called Test. By default, the ASP.NET MVC 4 view engine doesn't support it as a pluggable module because it tries to look for the View in the incorrect location.
So conceptually, we have:
Main <- Main application folder
Areas <- Main application folder
Plugin <- Plugin module application folder
Areas <- Plugin module application folder
Test <- Plugin module application folder
To fix this, I created a way to interpret the AreaName
property in a customized RazorViewEngine
class to rewrite the URL the view engine is looking for to find the view files in these pluggable module areas.
First, I use the following convention to define my Area registration class for the Test Area belonging to my pluggable modules:
Namespace Areas.Plugin
Public Class PluginAreaRegistration
Inherits AreaRegistration
Public Overrides ReadOnly Property AreaName() As String
Get
Return "Plugin.Test"
End Get
End Property
Public Overrides Sub RegisterArea(ByVal context As System.Web.Mvc.AreaRegistrationContext)
context.MapRoute( _
"Plugin_default", _
"Plugin/Test/{controller}/{action}/{id}", _
New With {.controller = "Home", .action = "Index", .id = UrlParameter.Optional},
{"Plugin.Test.Controllers"}
)
End Sub
End Class
End Namespace
I then inherited the the RazorViewEngine
and overrode some methods to parse and generate the views path in the pluggable module's Areas folder:
Public Class MyExtendedRazorViewEngine
Inherits RazorViewEngine
' set the location format strings
Public Sub New()
MyBase.PartialViewLocationFormats = _
{
"~/Views/{1}/{0}.cshtml",
"~/Views/{1}/{0}.vbhtml",
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/{1}/{0}.vbhtml",
"~/Views/Shared/{0}.cshtml",
"~/Views/Shared/{0}.vbhtml",
"~/Areas/{3}/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{3}/Areas/{2}/Views/{1}/{0}.vbhtml",
"~/Areas/{1}/Views/Shared/{0}.cshtml",
"~/Areas/{1}/Views/Shared/{0}.vbhtml",
"~/Areas/{2}/Areas/{1}/Views/{0}.cshtml",
"~/Areas/{2}/Areas/{1}/Views/{0}.vbhtml"
}
MyBase.AreaViewLocationFormats = {
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/{1}/{0}.vbhtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.vbhtml",
"~/Areas/{2}/Areas/{1}/Views/{0}.cshtml",
"~/Areas/{2}/Areas/{1}/Views/{0}.vbhtml"
}
MyBase.AreaMasterLocationFormats = {
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/{1}/{0}.vbhtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.vbhtml",
"~/Areas/{2}/Areas/{1}/Views/{0}.cshtml",
"~/Areas/{2}/Areas/{1}/Views/{0}.vbhtml"
}
MyBase.AreaPartialViewLocationFormats = {
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/{1}/{0}.vbhtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.vbhtml",
"~/Areas/{2}/Areas/{1}/Views/{0}.cshtml",
"~/Areas/{2}/Areas/{1}/Views/{0}.vbhtml"
}
MyBase.ViewLocationFormats = {
"~/Views/{1}/{0}.cshtml",
"~/Views/{1}/{0}.vbhtml",
"~/Views/Shared/{0}.cshtml",
"~/Views/Shared/{0}.vbhtml"
}
MyBase.MasterLocationFormats = {
"~/Views/{1}/{0}.cshtml",
"~/Views/{1}/{0}.vbhtml",
"~/Views/Shared/{0}.cshtml",
"~/Views/Shared/{0}.vbhtml"
}
MyBase.PartialViewLocationFormats = {
"~/Views/{1}/{0}.cshtml",
"~/Views/{1}/{0}.vbhtml",
"~/Views/Shared/{0}.cshtml",
"~/Views/Shared/{0}.vbhtml"
}
End Sub
Protected Overrides Function CreatePartialView(controllerContext As ControllerContext, partialPath As String) As IView
Dim area As String = controllerContext.RouteData.DataTokens.Item("Area")
Dim areaname As String()
Dim pp As String = partialPath
If Not area Is Nothing Then
areaname = area.Split(".")
If areaname.Length > 1 Then
pp = pp.Replace(area, areaname(0) & "/Areas/" & areaname(1))
End If
End If
Return MyBase.CreatePartialView(controllerContext, pp)
End Function
Protected Overrides Function CreateView(controllerContext As ControllerContext, viewPath As String, masterPath As String) As IView
Dim area As String = controllerContext.RouteData.DataTokens.Item("Area")
Dim areaname As String()
Dim vp As String = viewPath
Dim mp As String = masterPath
If Not area Is Nothing Then
areaname = area.Split(".")
If areaname.Length > 1 Then
vp = vp.Replace(area, areaname(0) & "/Areas/" & areaname(1))
mp = mp.Replace(area, areaname(0) & "/Areas/" & areaname(1))
End If
End If
Return MyBase.CreateView(controllerContext, vp, mp)
End Function
Protected Overrides Function FileExists(controllerContext As ControllerContext, virtualPath As String) As Boolean
Dim area As String = controllerContext.RouteData.DataTokens.Item("Area")
Dim areaname As String()
Dim vp As String = virtualPath
If Not area Is Nothing Then
areaname = area.Split(".")
If areaname.Length > 1 Then
vp = vp.Replace(area, areaname(0) & "/Areas/" & areaname(1))
End If
End If
Return MyBase.FileExists(controllerContext, vp)
End Function
End Class
I've modified the main application Global.asax
file to pick up the new view engine:
Imports System.Web.Http
Imports System.Web.Optimization
Public Class MvcApplication
Inherits System.Web.HttpApplication
Sub Application_Start()
AreaRegistration.RegisterAllAreas()
WebApiConfig.Register(GlobalConfiguration.Configuration)
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters)
RouteConfig.RegisterRoutes(RouteTable.Routes)
BundleConfig.RegisterBundles(BundleTable.Bundles)
ViewEngines.Engines.Clear()
ViewEngines.Engines.Add(New MyExtendedRazorViewEngine())
End Sub
End Class
After launching the browser and invoking the Home controller for my main application, I see the correct pages and layout render. When I go to the Home controller action for the Index for my Plugin module, again the Index view renders properly with the _Layout.vbhtml
being picked up from the main application.
However, when I invoke the Home controller action for the Index view of Plugin's Test Area, I can only see the Index page view render, but the master _Layout.vbhtml
isn't being included from the main application.
What am I missing to get the Areas views below the Plugin pluggable module to render the main application's master layout template?