Расширяет инфраструктуру для добавления поддержки подключаемых модулей в ваши .NET-приложения, чтобы вы также могли осуществлять динамический поиск подключаемых модулей в собственном каталоге приложения. Прежде всего, эта статья — дополнение к моей предыдущей статье о подключаемых модулях. Я рекомендую вам, прежде чем погрузиться в эту статью, ознакомиться с первой. Основная цель этой статьи — избавить пользователя от файлов конфигурации. Основная мысль — обеспечить, чтобы при загрузке ваше приложение могло просматривать .DLL-файлы своего каталога, находить те, которые содержат типы, поддерживающие интерфейс IPlugin, и создавать экземпляры этих подключаемых модулей. Никакого вмешательства пользователя, за исключением копирования .DLL-файла в каталог приложения, не должно быть.
Использование System.Reflection — путь к спасениюОдно из наиболее мощных пространств имен в Microsoft® .NET Framework — System.Reflection. Как следует из имени, оно позволяет коду «отбрасывать свою тень», раскрывая любые свойства, члены (как открытые, так и закрытые), методы, интерфейсы, цепочки интерфейсов — практически все, что вы хотели знать о Типе Х, но никогда не осмеливались спросить. Используя это могущественное пространство имен, вы будете проходить по каждому файлу, обнаруживая все находящиеся в нем типы, и для каждого типа будете выяснять, поддерживает ли он интерфейс IPlugin. Класс, который вам надо использовать, чтобы извлечь все типы, входящие в .NET-сборку, называется System.Reflection.Assembly. Вот простой метод, используемый этим классом именно для того, что мы только что обсуждали: private void TryLoadingPlugin(string path) { Assembly asm = AppDomain.CurrentDomain.Load(path); foreach(Type t in asm.GetTypes()) { foreach(Type iface in t.GetInterfaces()) { if(iface.Equals(typeof(IPlugin))) { AddToGoodTypesCollection(t); break; } } } } Как видите, с помощью пространства имен System.Reflection очень просто извлечь большое количество информации о любом заданном файле сборки. В приведенном выше методе вы вызываете метод для GetInterfaces() для каждого Type, существующего в заданном файле. Затем вы проверяете, является ли какой-нибудь интерфейс этого типа интерфейсом IPlugin. Если да, это означает, что вы можете загружать его в ваше приложение; поместите его в список массивов (Array List) для хранения. Позже вы можете вернуться к этому списку массивов и использовать Activator.CreateInstance(Type) этих типов и, таким образом, создать экземпляр любого из обнаруженных вами подключаемых модулей. Небольшая проблема Этот код, безусловно, работоспособен и мог бы быть приемлемым, если бы не существовало одной маленькой проблемы. Чтобы объяснить ее суть, вам понадобится узнать о AppDomain. Я избавлю вас от собственных объяснений, что такое AppDomain, и приведу цитаты из документации по этому поводу: Домены приложений, которые представлены объектами AppDomain, обеспечивают изолированные, выгружаемые и безопасные границы для выполнения управляемого кода.В одном процессе могут выполняться несколько доменов приложений; однако не существует взаимно-однозначного соответствия между доменами приложений и потоками. Одному домену приложения могут принадлежать несколько потоков, и, пока данный поток не ограничен отдельным доменом приложения, в любой момент времени поток выполняется в одном домене приложения.Домены приложений создаются методом CreateDomain. Экземпляры AppDomain используются для загрузки и выполнения сборок. Если AppDomain больше не используется, он может быть выгружен. Я бы добавил следующее: любая сборка, загруженная в приложении, по умолчанию загружается в AppDomain приложения. Само по себе это неплохо, если бы не тот факт, что вы не можете напрямую выгрузить сборку, если загрузили ее в AppDomain. Единственный способ выгрузить ее — выгрузить сам AppDomain. Отсюда несколько следствий: 1. Любой .DLL-файл, проверяемый на наличие IPlugin, будет с момента проверки загружен в ваше приложение на все оставшееся время существования AppDomain. 2. Проверка множества .DLL-файлов может привести к серьезным перерасходам памяти для приложения. Итак, теперь вы столкнулись с проблемой, как пройти по всем файлам каталога, загрузить сборки, но при этом иметь возможность выгрузить их. Решение намного проще, чем вы могли бы ожидать: 1. Вы создадите новый AppDomain и загрузите все проверяемые в данный момент сборки в этот AppDomain. 2. Завершив проверку и обнаружив только те типы, экземпляры которых могут быть созданы, вы выгрузите отдельный AppDomain. 3. Затем вы загрузите «хорошие» типы в ваш AppDomain, таким образом, вы избавите себя от мусора в памяти вашего приложения. Создать новый AppDomain просто: AppDomain domain = AppDomain.CreateDomain("PluginLoader"); PluginFinder finder = (PluginFinder)domain.CreateInstanceFromAndUnwrap( Application.ExecutablePath,"Royo.PluggableApp.PluginFinder"); ArrayList FoundPluginTypes = finder.SearchPath(Environment.CurrentDirectory); AppDomain.Unload(domain); 4. Вы создаете новый экземпляр объекта AppDomain, используя статический метод AppDomain. Вы передаете в него удобное для пользователя имя этого нового AppDomain. 5. Вы создаете экземпляр класса PluginFinder (в котором есть метод SearchPath()) в AppDomain. Для этого вы передаете в него (очень похоже на использование Activator) имя сборки, в которой находится класс, и полное имя класса, экземпляр которого надо создать. 6. В результате последней операции вы получаете Proxy, который выглядит и ведет себя так же, как ваш класс PluginLoader, но на самом деле является посредником между AppDomain вашего приложения и только что созданным вами новым AppDomain. Из вышесказанного вы знаете, что с этого момента любые загружаемые PluginLoader сборки будут на самом деле загружаться в ваш новый AppDomain, а не в AppDomain вашего приложения. Это означает, что после того, как этот класс выполнит свою работу, вы сможете выгрузить новый AppDomain, избавляясь, таким образом, от засорения памяти. 7. Вы вызываете метод SearchPath() в Proxy вашего реального класса PluginLoader находящегося в другом AppDomain. Назад вы получаете список массивов, содержащий только типы, использующие интерфейс IPlugin. 8. Вы выгружаете другой AppDomain, поскольку он вам больше не нужен. 9. Теперь вы можете двигаться дальше и создавать экземпляры подключаемых модулей, как описано в моей предыдущей статье («Создание подключаемого модуля»), используя класс Activator. Важно! Т.к. вы используете Proxy при сообщении между AppDomain, любой объект, экземпляр которого будет создан в этом прокси (в данном случае, PluginLoader), должен быть сериализуемым. Вы должны или унаследовать PluginLoader от MarshalByRefObject, или применить атрибут [Serializable] к этому классу. В противном случае, вы получите исключение: "Additional information: The type Royo.PluggableApp.PluginFinder in Assembly PluggableApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null is not marked as serializable."(«Дополнительная информация: тип Royo.PluggableApp.PluginFinder в сборке PluggableApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null не отмечен как сериализуемый.») Полезный инструмент отладки При работе с AppDomain и отладке исключений, которые могут возникнуть при их загрузке и выгрузке, может возникнуть огромное количество ошибок. Полезный инструмент, который практически не задокументирован — fuslogvw.exe или Fusion Log Viewer. Fusion — это имя подсистемы загрузки. Вы можете использовать этот инструмент для регистрации сбоев. Если вы получаете ошибки во время загрузки сборок, обновите представление этого инструмента и получите протокол исключительных ситуаций. |