I'm attempting to create a framework for allowing controllers and views to be dynamically imported into an MVC application. Here's how it works so far:
- I'm using .NET 4, ASP.NET MVC 3 RC and the Razor ViewEngine
- Controllers are exported and imported using MEF per project - I call a set of controllers and views from a given project a "Module"
- Assemblies discovered using MEF are dynamically referenced by the BuildManager using a pre-application start method and
BuildManager.AddReferencedAssembly
. - Binaries (from exporting project) and Views are copied into the target project's folder structure using a build event
- Controllers are selected using a custom controller factory which inherits from DefaultControllerFactory and overrides GetControllerType()
- Views are selected using a custom view engine which inherits from RazorViewEngine and overrides GetView() and GetPartialView() to allow it to look for views in Module-specific view directories
Everything works so far except for views using a strongly typed model. Views that use the dynamic model work fine, but when I specify a model type using @model
, I get a YSOD that says "The view 'Index' or its master was not found".
When debugging my ViewEngine implementation, I can see that:
this.VirtualPathProvider.FileExists(String.Format(this.ViewLocationFormats[2], viewName, controllerContext.RouteData.GetRequiredString("controller")))
returns true, while
this.FileExists(controllerContext, String.Format(this.ViewLocationFormats[2], viewName, controllerContext.RouteData.GetRequiredString("controller")))
returns false.
Looking in Reflector, the RazorViewEngine implementation of FileExists()
ultimately winds up doing this:
return (BuildManager.GetObjectFactory(virtualPath, false) != null);
However, I can't view BuildManager.GetObjectFactory()
from Reflector because it's hidden somehow.
I'm suspecting that it has something to do with the fact that the model type is a type that is loaded from MEF, but since I'm already referencing the assemblies discovered by MEF from BuildManager, I'm out of leads. Can anyone provide a little more insight into what might be going on?
Update: Turns out I was using an outdated version of Reflector from before .NET 4. I can see GetObjectFactory() now, but I can't really seem to find anything helpful. I've tried adding this into my FindView() overload:
try { var path = String.Format(this.ViewLocationFormats[2], viewName, controllerContext.RouteData.GetRequiredString("controller")); var objFactory = System.Web.Compilation.BuildManager.GetObjectFactory(virtualPath: path, throwIfNotFound: true); } catch { }
Unfortunately, objFactory
ends up null, and no exception gets thrown. All the bits that deal with compilation errors are part of private methods or types so I can't debug any of that, but it even seems like they'd end up throwing an exception, which doesn't seem to be happening. Looks like I'm at a dead end again. Help!
Update 2
I've discovered that at the point where FindView() is being called, if I call AppDomain.CurrentDomain.GetAssemblies()
, the assembly that the model type is in is included. However, I cannot load the type using Type.GetType()
.
Update 3
Here's what I'm seeing:
Update 4
Here's the ViewEngine implementation:
using System;
using System.Linq;
using System.Web.Mvc;
using System.Web.Hosting;
using System.Web.Compilation;
namespace Site.Admin.Portal
{
public class ModuleViewEngine : RazorViewEngine
{
private static readonly String[] viewLocationFormats = new String[]
{
"~/Views/{0}/{{1}}/{{0}}.aspx",
"~/Views/{0}/{{1}}/{{0}}.ascx",
"~/Views/{0}/{{1}}/{{0}}.cshtml",
"~/Views/{0}/Shared/{{0}}.aspx",
"~/Views/{0}/Shared/{{0}}.ascx",
"~/Views/{0}/Shared/{{0}}.cshtml"
};
public ModuleViewEngine(IModule module)
{
this.Module = module;
var formats = viewLocationFormats.Select(f => String.Format(f, module.Name)).ToArray();
this.ViewLocationFormats = formats;
this.PartialViewLocationFormats = formats;
this.AreaViewLocationFormats = formats;
this.AreaPartialViewLocationFormats = formats;
this.AreaMasterLocationFormats = formats;
}
public IModule Module { get; private set; }
public override ViewEngineResult FindPartialView(ControllerContext controllerContext, String partialViewName, Boolean useCache)
{
var moduleName = controllerContext.RouteData.GetRequiredString("module");
if (moduleName.Equals(this.Module.Name, StringComparison.InvariantCultureIgnoreCase))
{
return base.FindPartialView(controllerContext, partialViewName, useCache);
}
else return new ViewEngineResult(new String[0]);
}
public override ViewEngineResult FindView(ControllerContext controllerContext, String viewName, String masterName, Boolean useCache)
{
var moduleName = controllerContext.RouteData.GetRequiredString("module");
if (moduleName.Equals(this.Module.Name, StringComparison.InvariantCultureIgnoreCase))
{
var baseResult = base.FindView(controllerContext, viewName, masterName, useCache);
return baseResult;
}
else return new ViewEngineResult(new String[0]);
}
}
}
Based on Update 2, I'm guessing what you've got is an explicitly loaded copy of your assembly (that is, it was loaded through some other method than Load, like LoadFrom). Explicitly loaded assemblies are set off aside into a special place, because they are not allowed to satisfy implicit type requirements. The rules for Fusion (the assembly loader) can be pretty arcane and hard to understand.
I agree with Matthew's assessment that, to get this to work, your DLL is going to have to be in /bin or else it will never be able to satisfy the implicit type requirement.
The imported libaries aren't in the
/bin
directory so aren't probed when trying to resolve references. I discovered a work around which I published in my MVC + MEF article (Part 2). Essentially you need to add your directories where your extensions sit to the probing path of the AppDomain.Essentially where I am building my container:
I register all the directories of catalogs in the current domain:
I believe the same should work for MVC3.
UPDATE: Correct me if I am wrong, but I don't believe that ViewEngines are instantiated once per request, you create a single instance that you register with MVC. Because of this only one
IModule
instance is ever used with your ViewEngine, so if a path doesn't match that firstIModule.Name
it won't be found? Does that make sense?