CM + MEF: регистрация экспорта MEF из внешних сборок - PullRequest
2 голосов
/ 14 июня 2011

этот пост в идеале продолжает мой другой пост о плагинах MEF, но мой первый пост был слишком полон комментариев, и этот пример более полный.Здесь я суммирую мой обновленный сценарий, со всеми моими выводами до этого момента.Надеюсь, что это может быть полезно для других новичков CM, таких как я.

Вы можете загрузить полный пример сценария воспроизведения: это почти тупой скелет для приложения на основе плагинов CM + MEF:

VS2010 repro solution (обновлено)
Это минимальное урезанное решение, представляющее мои проблемы с CM + MEF.Существует 3 проекта:

  • основной пользовательский интерфейс (CmRepro).
  • основная DLL, общая для всех надстроек (AddinCore), с парой интерфейсов и пользовательских атрибутов, используемых для MEFметаданные.
  • образец DLL-надстройки (AlphaAddin) с представлением и моделью представления, реализующей интерфейсы.

* ядро ​​ содержит 2 интерфейса, представляющих модель представления иего вид и 2 атрибута, которые будут использоваться для украшения моделей и видов.Интерфейс viewmodel описывает класс, который должен составлять приветственное сообщение от имени какого-то человека, поэтому он предоставляет несколько свойств и метод для этого.Интерфейс представления просто предоставляет свойство, возвращающее его приведение DataContext к интерфейсу viewmodel.

Пример addin имеет реализацию для viewmodel и представления;оба являются MEF-экспортами, оформленными соответствующим атрибутом.В реальном решении несколько свойств этих атрибутов используются для фильтрации;здесь у меня просто есть фиктивное свойство Language , которое должно допускать использование других плагинов для разных языков.

Основной пользовательский интерфейс имеет загрузчик MEF, который добавляет код для получения экспорта MEFиз папки надстроек.Я изменил этот код, чтобы включить экспорт из каталогов MEF и получить лучшее представление о некоторых исключениях MEF, но все же я не могу понять, как правильно «зарегистрировать» их в CM.

Основная модель представления имеет 2 методы : one (A) использует каталог MEF для извлечения модели представления и ее вида, связывания их и отображения в окне.Другой (B) использует тот же каталог для получения модели представления, а затем менеджер окон CM для определения местоположения, создания, привязки и отображения соответствующего представления в соответствии с соглашениями об именах CM.Эти методы представляют два альтернативных способа, с которыми мне приходится иметь дело в моем реальном коде, то есть создание экземпляров некоторых критически важных объектов "самостоятельно", просто используя MEF, но затем позволяя им работать для CM, или позволяя CM (с MEF-загрузчиком) выполнять большую частьработа начинается с viewmodel.

Во всяком случае кажется, что в обоих случаях мне не хватает чего-то, что касается регистрации в CM.Проблемы:

  • (A) как подключить VM + V для CM, чтобы применялись соглашения для привязки данных и т. Д.?В настоящее время я могу собрать свои части MEF вместе, но CM игнорирует их, поскольку он не использовался для создания экземпляров ни одной из них. Я отвечаю себе здесь :

    <code>ViewModelBinder.Bind(viewmodel, (UserControl)view, null);
  • (B) как мне зарегистрировать экспорт из MEF в CM такчто диспетчер окон CM может найти представление?В настоящее время не удается найти представление из модели представления.

Добавление (21 июня)

Я пытаюсь объяснить лучше, для кого нет доступа к репро-решению.Я использую «стандартный» загрузчик MEF, изменяя переопределение Configure следующим образом:

_container = new CompositionContainer(
  new AggregateCatalog(AssemblySource.Instance.Select(
    x => new AssemblyCatalog(x)).OfType()
  .Union(GetAddinDirectoryCatalogs())));

, это создает контейнер композиции MEF, который объединяет каталог из AssemblySource с типами CM, такими как агрегатор событий или менеджер окон, с каталогомиз нескольких каталогов надстроек, которые содержат экспорты как для V, так и для виртуальных машин.

В моей основной модели основного вида я создаю новую виртуальную машину из плагина, найденного в каталоге приложения хоста среди других, и мне нужен CMоконный менеджер для поиска, создания и отображения своего представления в диалоговом окне, например:

viewmodel = GetMyViewModelFromAddin();
windowmanager.ShowDialog(viewmodel);

CM в любом случае не может найти вид. AFAIK, соглашения об именах соблюдаются: и V, и VM находятся в одной сборке надстройки, помеченной как экспорт MEF, названной как SomethingViewModel / SomethingView. В любом случае, как указал Лиф в своем разъяснении, AssemblySource.Instance - это статическая коллекция сборок IObservableCollection, и я не добавил в нее свои надстройки. Но суть в том, что я не хотел бы добавлять их все заранее, потому что это означает загрузку ВСЕХ надстроек, еще не зная, какие (если они есть) будут когда-либо использоваться. Надежная система плагинов является причиной использования MEF, в конце концов. Я новичок в CM и не уверен, возможно ли (и где) найти точку расширения для CM в этом сценарии. Диспетчер окон вообще не вызывает мою реализацию загрузчика, очевидно, потому что IoC не создает экземпляров, так как в экземпляре исходного кода не найдено ни одного совпадения. Так что, похоже, я застрял здесь, единственное решение заключается в предварительной загрузке всех сборок в Экземпляре, но это, похоже, наносит ущерб всей цели использования MEF.

Приложение на основе плагинов, которое я разрабатываю, загружает тонны «пар» V + VM CM, представляющих виджеты пользовательского интерфейса, которые, в свою очередь, часто используют диспетчер окон для вызова других пар V + VM в качестве диалогов. Я могу обойти создание экземпляров с помощью CM и использовать MEF для извлечения V + VM для каждого виджета, но все же я сталкиваюсь с той же проблемой местоположения вида для каждого виджета, для которого требуется оконный менеджер. Другой альтернативный (обходной путь), который я вижу, - это избегать использования оконного менеджера и реализовывать свой собственный механизм для показа диалогов из виджетов, но это заставляет меня немного ошибаться в CM ... :). Обычно, когда я пишу гораздо больше кода, чем ожидалось, я склонен думать, что я неправильно использую инструмент. Есть идеи?

Ответы [ 2 ]

2 голосов
/ 20 июня 2011

Caliburn.Micro проходит 3 этапа, чтобы найти представление из экземпляра модели представления.

  1. Преобразует ли текст для преобразования имени типа представления в имя типа модели представления.Существует ряд стандартных соглашений для этих преобразований, например, SomeNamespace.ViewModels.CustomerViewModel может отображаться на SomeNamespace.Views.CustomerView.

  2. С именами типов представлений, CaliburnЗатем .Micro использует AssemblySource.Instance (статическая IObservableCollection<Assembly> коллекция сборок), чтобы найти первое совпадение Type.

  3. Caliburn.Micro пытается создать экземпляр этого Type с помощью одного из IoC.GetInstance() методов (которые делегируются вашему загрузчику и, следовательно, MEF).

Я предполагаю (ваш общий файловый сайт заблокирован здесь), что проблема с разрешениемПредставления из моделей представления связаны со вторым шагом и коллекцией AssemblySource.Instance, не содержащей динамическую сборку.

Одним из решений может быть добавление каждой динамически загружаемой сборки надстройки в AssemblySource.Instance при их загрузке или если вызнать все сборки при запуске, тогда вы можете переопределить метод Bootstrapper.SelectAssemblies, чтобы получить список сборок, которые, как вы ожидаете, будут содержать представления и модели представления.


Обновление, чтобы показать, как можноизвлекать сборки из загруженных MEF деталей

Если вы используете DirectoryCatalog для загрузки деталей из других сборок, вы можете найти сборки, используемые следующим образом:

var directoryCatalog = new DirectoryCatalog(@"./");            
AssemblySource.Instance.AddRange(
    directoryCatalog.Parts
        .Select(part => ReflectionModelServices.GetPartType(part).Value.Assembly)
        .Where(assembly => !AssemblySource.Instance.Contains(assembly)));

Если папка надстроек изменитсяво время выполнения приложения вам нужно будет DirectoryCatalog.Refresh() каталог и запустить код, чтобы добавить любые новые сборки в AssemblySource.Instance

0 голосов
/ 22 июня 2011

Я нашел обходной путь.Это не так красиво, но позволяет CM и его оконному менеджеру работать.Я суммирую здесь свои выводы, надеюсь, что это поможет другим или позволит кому-то указать мне на лучшее решение.

Учитывая, что (а) я не хочу загружать ВСЕ сборки, включая все их зависимости, в папку моих плагинов,чтобы избежать загрязнения моего домена приложения неиспользуемым материалом;и что (b) единственной доступной точкой расширения CM для этого кажется SelectAssemblies, моя цель - добавить туда мои сборки, но только сборки плагинов, требующие регистрации в CM.

Поэтому я начал искать способзагрузка всех библиотек DLL во временный домен приложения, сканирование их на наличие каких-либо аспектов, помечающих их как подключаемые модули, а затем загрузка в текущий домен приложения только подключаемых модулей, передавая их в SelectAssemblies.Это далеко не оптимальное решение, так как я не могу использовать способ сканирования общего назначения, такой как MEF, и чувствую, что дублирую усилия, но по крайней мере это рабочее решение.

Во-первых, чтобы предоставитьПо крайней мере, для загрузки только плагинов, я украшаю свои сборки плагинов, требующие регистрации CM, с помощью собственного атрибута, помечающего их как плагины, и далее перечисляю их типы в поисках тех, которые должны быть позже использованы MEF.

Атрибут прост: [AttributeUsage(AttributeTargets.Assembly)] public class AssemblyRegisteredWithCMAttribute : Attribute {}

Затем я нашел этот очень хороший кусок кода для сканирования сборок в другой временный домен приложения:

http://sachabarber.net/?p=560

Мне пришлосьизмените его немного, потому что он не работал при загрузке сборок с зависимостями, и он сканировал типы сборок, в то время как в моем случае мне просто нужно проверить атрибут.

Вот мой код: <pre>using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Policy; using System.Reflection; using System.Diagnostics.CodeAnalysis;</p> <p>namespace CmRepro { /// /// Separate AppDomain assembly loader. /// /// Modified from <a href="http://sachabarber.net/?p=560" rel="nofollow">http://sachabarber.net/?p=560</a> . public class SeparateAppDomainAssemblyLoader { /// /// Loads an assembly into a new AppDomain returning the names of the files /// containing assemblies marked with the assembly attribute name matching /// the specified name. The new AppDomain is then Unloaded. /// /// list of files to load /// assemblies directory /// matching attribute name /// list of found namespaces /// null files, assembly directory or matching attribute public List LoadAssemblies(string[] aFiles, string sAssemblyDirectory, string sMatchingAttribute) { if (aFiles == null) throw new ArgumentNullException("aFiles"); if (sAssemblyDirectory == null) throw new ArgumentNullException("sAssemblyDirectory"); if (sMatchingAttribute == null) throw new ArgumentNullException("sMatchingAttribute");</p> <pre><code> List<String> namespaces = new List<String>(); AppDomain childDomain = BuildChildDomain(AppDomain.CurrentDomain); try { Type loaderType = typeof(AssemblyLoader); if (loaderType.Assembly != null) { AssemblyLoader loader = (AssemblyLoader)childDomain. CreateInstanceFrom( loaderType.Assembly.Location, loaderType.FullName).Unwrap(); namespaces = loader.LoadAssemblies(aFiles, sAssemblyDirectory, sMatchingAttribute); } //eif return namespaces; } finally { AppDomain.Unload(childDomain); } } /// <summary> /// Creates a new AppDomain based on the parent AppDomains /// Evidence and AppDomainSetup. /// </summary> /// <param name="parentDomain">The parent AppDomain</param> /// <returns>A newly created AppDomain</returns> private AppDomain BuildChildDomain(AppDomain parentDomain) { Evidence evidence = new Evidence(parentDomain.Evidence); AppDomainSetup setup = parentDomain.SetupInformation; return AppDomain.CreateDomain("DiscoveryRegion", evidence, setup); } /// <summary> /// Remotable AssemblyLoader, this class inherits from <c>MarshalByRefObject</c> /// to allow the CLR to marshall this object by reference across AppDomain boundaries. /// </summary> private class AssemblyLoader : MarshalByRefObject { private string _sRootAsmDir; /// <summary> /// ReflectionOnlyLoad of single Assembly based on the assemblyPath parameter. /// </summary> /// <param name="aFiles">files names</param> /// <param name="sAssemblyDirectory">assemblies directory</param> /// <param name="sMatchingAttribute">matching attribute name</param> [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] internal List<string> LoadAssemblies(string[] aFiles, string sAssemblyDirectory, string sMatchingAttribute) { AppDomain.CurrentDomain.ReflectionOnlyAssemblyResolve += OnReflectionOnlyAssemblyResolve; _sRootAsmDir = sAssemblyDirectory; List<string> aAssemblies = new List<String>(); try { sMatchingAttribute = "." + sMatchingAttribute; foreach (string sFile in aFiles) Assembly.ReflectionOnlyLoadFrom(sFile); aAssemblies.AddRange(from asm in AppDomain.CurrentDomain.ReflectionOnlyGetAssemblies() let attrs = CustomAttributeData.GetCustomAttributes(asm) where attrs.Any(a => a.ToString().Contains(sMatchingAttribute)) select asm.FullName); return aAssemblies; } catch (FileNotFoundException) { /* Continue loading assemblies even if an assembly * can not be loaded in the new AppDomain. */ return aAssemblies; } } private Assembly OnReflectionOnlyAssemblyResolve(object sender, ResolveEventArgs e) { // http://blogs.msdn.com/b/junfeng/archive/2004/08/24/219691.aspx System.Diagnostics.Debug.WriteLine(e.Name); AssemblyName name = new AssemblyName(e.Name); string sAsmToCheck = Path.GetDirectoryName(_sRootAsmDir) + "\\" + name.Name + ".dll"; return File.Exists(sAsmToCheck) ? Assembly.ReflectionOnlyLoadFrom(sAsmToCheck) : Assembly.ReflectionOnlyLoad(e.Name); } } }

} ​​

Теперь в моем загрузчике я переопределил метод SelectAssemblies следующим образом:

<pre>... protected override IEnumerable SelectAssemblies() { string sAddinPath = GetAbsolutePath(ADDIN_PATH); FileCheckList list = new FileCheckList(sAddinPath);</p> <pre><code>// check only DLL files which were added or changed since last check SeparateAppDomainAssemblyLoader loader = new SeparateAppDomainAssemblyLoader(); List<string> aAssembliesToRegister = loader.LoadAssemblies(list.GetFiles(null), sAddinPath, "AssemblyRegisteredWithCM"); string[] aFilesToRegister = (from s in aAssembliesToRegister select Path.Combine(sAddinPath, s.Substring(0, s.IndexOf(',')) + ".dll")).ToArray(); // update checklist foreach (string sFile in aFilesToRegister) list.SetCheck(sFile, true); list.UncheckAllNull(); list.Save(); // register required files return (new[] { Assembly.GetExecutingAssembly(), }).Union((from s in list.GetFiles(true) select Assembly.LoadFrom(s))).ToArray();

} ...

Как видите, я вызываю загрузчик не для всех библиотек DLL в моем пути надстроек, а только для тех, которыеконтрольный список кэшированных файлов показывает, что они были добавлены или изменены в этой папке с момента последнего полного сканирования.Это должно немного ускорить процесс и не требует существования файла контрольного списка: если он не найден, он будет воссоздан при запуске путем сканирования всех файлов, если найдены только добавленные или измененные файлы, они будут отсканированы снова (я использую CRC для обнаруженияизменения).Поэтому я получаю папку надстроек, создаю контрольный список файлов для этой папки, получаю из нее список новых или измененных файлов и передаю ее загрузчику сборок.Это возвращает только имена файлов DLL, которые должны быть зарегистрированы (то есть те, которые содержат сборки, помеченные моим атрибутом);Затем я обновляю контрольный список для следующего запуска и регистрирую только необходимые файлы.Таким образом, я могу позволить моей виртуальной машине надстройки использовать оконные менеджеры и правильно находить представление для каждой требуемой модели представления.Несколько некрасиво, но работает.Еще раз спасибо Leaf, который объяснил мне работу CM.

...