ASP.NET MVC: представления с использованием типа модели, загружаемого MEF, не могут быть найдены механизмом просмотра
Я пытаюсь создать структуру, позволяющую динамически импортировать контроллеры и представления в приложение MVC. Вот как это работает:
- Я использую .NET 4, ASP.NET MVC 3 RC и Razor ViewEngine
- Контроллеры экспортируются и импортируются с использованием MEF для каждого проекта - я вызываю набор контроллеров и представлений из данного проекта "Модуль"
- Ассембли, обнаруженные с помощью MEF, динамически ссылаются BuildManager с использованием метода запуска перед приложением и
BuildManager.AddReferencedAssembly
.
- Бинарники (из проекта экспорта) и представления копируются в структуру папок целевого проекта, используя событие сборки
- Контроллеры выбираются с использованием настраиваемого контроллера factory, который наследует от DefaultControllerFactory и переопределяет GetControllerType()
- Представления выбираются с помощью настраиваемого механизма просмотра, который наследует RazorViewEngine и переопределяет GetView() и GetPartialView(), чтобы позволить ему искать представления в каталогах представлений, специфичных для модуля.
Все работает до сих пор, кроме представлений с использованием сильно типизированной модели. Представления, которые используют динамическую модель, работают нормально, но когда я указываю тип модели с помощью @model
, я получаю YSOD, который говорит: "Индекс представления" или его мастер не был найден ".
При отладке моей реализации ViewEngine я вижу, что:
this.VirtualPathProvider.FileExists(String.Format(this.ViewLocationFormats[2], viewName, controllerContext.RouteData.GetRequiredString("controller")))
возвращает true, а
this.FileExists(controllerContext, String.Format(this.ViewLocationFormats[2], viewName, controllerContext.RouteData.GetRequiredString("controller")))
возвращает false.
В рефлекторе реализация RazorViewEngine FileExists()
в конечном итоге завершает это:
return (BuildManager.GetObjectFactory(virtualPath, false) != null);
Однако я не могу просмотреть BuildManager.GetObjectFactory()
из Reflector, потому что он каким-то образом скрыт.
Я подозреваю, что это связано с тем, что тип модели - это тип, загружаемый из MEF, но поскольку я уже ссылаюсь на сборки, обнаруженные MEF из BuildManager, у меня нет причин, Может ли кто-нибудь дать немного больше информации о том, что может происходить?
Update:
Оказывается, я использовал устаревшую версию Reflector до .NET 4. Теперь я вижу GetObjectFactory(), но я действительно не могу найти ничего полезного. Я попытался добавить это в мою перегрузку FindView():
попробовать
{ var path = String.Format(this.ViewLocationFormats [2], viewName, controllerContext.RouteData.GetRequiredString( "controller" )); var objFactory = System.Web.Compilation.BuildManager.GetObjectFactory(virtualPath: path, throwIfNotFound: true);
}
поймать
{
}
К сожалению, objFactory
заканчивается нулевым, и исключение не генерируется. Все биты, относящиеся к ошибкам компиляции, являются частью частных методов или типов, поэтому я не могу отлаживать их, но похоже, что они в конечном итоге бросают исключение, которое, похоже, не происходит. Похоже, я снова в тупике. Помогите!
Обновление 2
Я обнаружил, что в момент вызова FindView(), если я вызываю AppDomain.CurrentDomain.GetAssemblies()
, включается сборка, в которую входит тип модели. Однако я не могу загрузить тип с помощью Type.GetType()
.
Обновление 3
Вот что я вижу:
![not found]()
Обновление 4
Здесь реализация ViewEngine:
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]);
}
}
}
Ответы
Ответ 1
Основываясь на обновлении 2, я предполагаю, что у вас есть явно загруженная копия вашей сборки (то есть она загружалась каким-то другим способом, кроме Load, например LoadFrom). Явно загруженные сборки откладываются в отдельное место, потому что им не разрешено выполнять требования неявного типа. Правила Fusion (загрузчик сборок) могут быть довольно загадочными и трудными для понимания.
Я согласен с оценкой Мэтью, что, чтобы заставить это работать, ваша DLL должна быть в /bin, иначе она никогда не сможет удовлетворить требование неявного типа.
Ответ 2
Импортированные библиотеки не находятся в каталоге /bin
, поэтому при попытке разрешить ссылки не проверяются. Я обнаружил работу, которую я опубликовал в своей статье MVC + MEF (часть 2). По существу вам нужно добавить свои каталоги, где ваши расширения будут соответствовать пути обнаружения AppDomain.
По существу, где я создаю свой контейнер:
/// <summary>
/// Creates the composition container.
/// </summary>
/// <returns></returns>
protected virtual CompositionContainer CreateCompositionContainer()
{
var catalog = new AggregateCatalog();
catalog.Catalogs.Add(new DirectoryCatalog(MapPath("~/bin")));
var config = CompositionConfigurationSection.GetInstance();
if (config != null && config.Catalogs != null) {
config.Catalogs
.Cast<CatalogConfigurationElement>()
.ForEach(c =>
{
if (!string.IsNullOrEmpty(c.Path)) {
string path = c.Path;
if (path.StartsWith("~"))
path = MapPath(path);
foreach (var directoryCatalog in GetDirectoryCatalogs(path)) {
// Register our path for probing.
RegisterPath(directoryCatalog.FullPath);
// Add the catalog.
catalog.Catalogs.Add(directoryCatalog);
}
}
});
}
var provider = new DynamicInstantiationExportProvider();
var container = new CompositionContainer(catalog, provider);
provider.SourceProvider = container;
return container;
}
Я регистрирую все каталоги каталогов в текущем домене:
/// <summary>
/// Registers the specified path for probing.
/// </summary>
/// <param name="path">The probable path.</param>
private void RegisterPath(string path)
{
AppDomain.CurrentDomain.AppendPrivatePath(path);
}
Я считаю, что то же самое должно работать для MVC3.
ОБНОВЛЕНИЕ. Исправьте меня, если я ошибаюсь, но я не верю, что средства ViewEngines создаются один раз для каждого запроса, вы создаете один экземпляр, который вы регистрируете в MVC. Из-за этого только один экземпляр IModule
когда-либо используется с вашим представлением ViewEngine, поэтому, если путь не совпадает с первым IModule.Name
, он не будет найден? Это имеет смысл?