70124

Плагины

Лабораторная работа

Информатика, кибернетика и программирование

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

Русский

2014-10-15

162 KB

2 чел.

Плагины стали неотъемлемой частью больших коммерческих приложений. С их помощью можно наращивать функциональность приложений без повторной компиляции или быстро изменять бизнес-правила, на основе которых работает приложение. Кроме того, для разработки плагинов не нужно иметь доступа к исходному коду приложения, поэтому они могут разрабатываться сторонними организациями. В .NET написание плагинов является простой задачей, которая решается с помощью рефлексии (reflection). Рефлексия позволяет динамически загружать сборки, получать информацию о методах, свойствах, событиях и полях классов из сборок,  создавать новые типы и вызывать методы во время выполнения. Классы и интерфейсы для рефлексии находятся в пространстве имен System.Reflection. В этой статье мы рассмотрим создание плагинов и их подключение к приложению с помощью т.н. позднего связывания. Со сборками, в которых будут находится плагины, мы будем работать с помощью класса Assembly.  Сборка может быть загружена с помощью статических методов класса Assembly Load, LoadFrom и LoadWithPartialName. Load загружает сборку по ее имени, заданным строкой, или на основе информации хранящейся в объекте AssemblyName (версия, криптографический ключ, ифнормация о культуре). В имя сборки не входит расширения файла, в котором она находится. Например, имя сборки (MyAsm.dll будет MyAsm). LoadFrom напрямую загружает сборку из файла, путь к которому передается методу. Метод LoadWithPartialName загружает сборку при неполных сведениях о ней, но пользоваться им не рекомендуется из-за непредсказуемости его работы, т.к. он был разработан для бетта-тестеров .NET Framework. Можно загружать сборки и вызовом метода Load для объектов домена AppDomain. Например, чтобы загрузить сборку в текущий домен можно воспользоваться таким кодом

AppDomain.CurrentDomain.Load(assemblyName);

    Основной класс для динамического получения информации о классах, интерфейсах, их полях, методах и перечислениях - Type. Для получения объекта Type можно воспользоваться несколькими разными методами:

  •  статический метод Type.GetType, который по имени типа возвращает объект Type
  •  методы GetInterface, GetInterfaces, FindInterfaces, GetElementType и GetTypeArray  класса Type
  •  методы GetType, GetTypes и GetExportedTypes класса Assembly
  •  методы GetType, GetTypes и FindTypes класса Module 
  •  оператор typeof

    Когда мы получили объект Type для какого-то типа, то у нас есть множество способов получить информацию о нем. Например, с помощью метода GetFields можно получить массив объектов FieldInfo с информацией о методах, а свойством IsSealed можно определить, объявлен ли тип как sealed.

Создание экземпляров типов

    По объекту Type можно не только определять параметры типа, но и создавать его экземпляры и вызывать их методы. Для этого также существует несколько методов:

  •  методы CreateInstance, CreateInstanceAndUnrap, CrateInstanceFrom и CrateInstanceFromAndUnrap класса AppDomain. После вызова методов, названия которых не оканчиваются на AndUnrap, для доступа к реальным данным нужно вызывать дополнительную функцию Unrap, т.к. эти методы возвращают wrapper (объект класса ObjectHandle) для нового экземляра типа
  •  методы CreateInstance и CreateInstanceFrom класса Activator. Это специальный класс для создания экземпляров типов и получения ссылок на удаленные объекты. Методу CreateInstance передаются объект Type или название инстанцируемого типа, массив объектов, соответствующих параметрам конструктора типа и объекты CultureInfo. Методу CreateInstanceFrom дополнительно передается имя сборки, содержащий тип. Методы, не принимающие в качестве параметра объект Type, также возвращают wrapper's ObjectHandle
  •  метод CreateInstance класса Assembly, создающий тип по его имени
  •  метод Invoke класса ContructorInfo
  •  метод InvokeMember класса Type

Использование интерфейсов

    При создании плагинов обычно используются интерфейсы, определяющие методы и свойства, которые должны реализовываться плагином. Для получения интерфейсов, которые есть у типа, используются методы GetInterface,GetInterfaces и FindInterfaces класса Type. Метод GetInterface по имени интерфейса позвращает объект Type для этого интерфейса или null если такого интерфейса у типа нет. Метод  GetInterfaces возвращает массив объектов Type с информацией об интерфейсах. Метод FindInterfaces возвращает массив интерфейсов, выбранных с помощью фильтра - делегата, вызываемого для каждого интерфейса. 
    Если класс реализует несколько интерфейсов, у которых есть методы с одинаковыми названиями, то нужно использовать метод GetInterfaceMap класса Type. Он возвращает объект InterfaceMapping для определения соотношения методов интерфейсов и методов класса, которые их реализуют.

Вызов методов

    Обычно методы вызываются с помощью метода InvokeMember класса Type. Процесс вызова метода состоит из двух этапов - привязки, при котором находится нужный метод, и непосредственно вызова. Для вызова нужно указать

  •  имя метода (в качестве метода может быть обычный метод, конструктор, свойство или поле)
  •  битовую маску из значений BindingFlags для поиска метода. В маске можно указать тип доступа метода, тип метода (поле, свойство, ...), тип данных и пр.
  •  объект Binder для связывания членов и аргументо
  •  объект, у которого вызывается метод
  •  массив аргументов метода
  •  массив объектов ParameterModifier
  •  объект CultureInfo

Разработка плагинов

    Для демонстрации применения рефлексии при создании плагинов было разработано небольшое тестовое приложение, состоящее из 4 проектов.

  •  MainApp - основное приложение, к которому будут подключаться плагины. Приложение загружает из графических файлов изображения и выводит их на форме
  •  Interface - определяет интерфейсы IPlugin для плагинов и IMainApp для приложений, к которым будут подключаться плагины
  •  RandomPlugin и ReversePlugin - плагины для добавления шума к изображениям и отражения изображения по вертикали

    Проект Interface содержит только определения двух интерфейсов. Приложение, которое подключает плагины, должно реализовывать интерфейс IMainApp. Этот интерфейс объявляет единственное свойство Image, с помощью которого плагины получают изображение и возвращают его после преобразования. 

public interface IMainApp
{
    Bitmap Image { get; set; }
}

    Интерфейс для плагинов называется IPlugin и содержит объявления трех свойств и одного метода Transform для преобразования изображения. Свойства используются для получения информации о плагинах - названия, номера версии и автора. Методу передается интерфейс IMainApp. Если бы наши плагины содержали бы несколько методов для преобразования изображения, то можно было поступить другим образом - создать в плагине метод для передачи в плагин интерфейса IMainApp, чтобы не передавать его каждому методу. Плагин тогда содержал бы в себе ссылку на главное приложение. 

public interface IPlugin
{
    string Name { get; }
    string Version { get; }
    string Author { get; }

    void Transform(IMainApp app);
}

    Если бы наше приложение использовало какие то типы (классы, интерфейсы, перечисления, ...), которые бы использовались или передавались плагинам, то их тоже нужно было бы поместить в сборку Interface.


 

Основное приложение

    Приложение MainApp, к которому мы будем подключать плагины, это простое windows-forms приложение для отображения графический файлов. Оно реализует интерфейс IMainApp - класс формы определен как public class Form1 : System.Windows.Forms.Form, Interface.IMainApp. На форме находится PictureBox для вывода изображения. Для реализации интерфейса IMainApp определяем свойство Image для доступа к изображению.

public Bitmap Image
{
    get { return (Bitmap)pictureBox.Image; }
    set { pictureBox.Image = value; }
}

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

void FindPlugins()
{
    // папка с плагинами
    string folder = System.AppDomain.CurrentDomain.BaseDirectory;

    // dll-файлы в этой папке
    string[] files = Directory.GetFiles(folder, "*.dll");

    foreach (string file in files)
        try
        {
            Assembly assembly = Assembly.LoadFile(file);

            foreach (Type type in assembly.GetTypes())
            {
                Type iface = type.GetInterface("Interface.IPlugin");

                if (iface != null)
                {
                    Interface.IPlugin plugin = (Interface.IPlugin)Activator.CreateInstance(type);
                    plugins.Add(plugin.Name, plugin); 
                }
           }
      }
      catch (Exception ex)
      {
          MessageBox.Show("Ошибка загрузки плагина\n" + ex.Message);
      }
}

    Вначале определяется папка для поиска плагинов. Т.к. у нас все плагины лежат в одной папке вместе с основным приложением, то мы используем свойство BaseDirectory для домена нашего приложения. Затем получаем все dll файлы из папки - их массив возвращает статическая функция GetFiles. Сборку для проверки на наличие плагина загружаем методом LoadFile и в цикле проходим по всем типам, определенным в сборке. Если тип содержит интерфейс IPlugin (при этом метод GetInterface возвращает не null), то создаем экземпляр этого типа (инстанцируем) методом Activator.CreateInstance. Для последующего использования мы сохраняем инстанцированный тип в хеш-таблице plugins. Ключем в хеш-таблице является название плагина.
    Потенциальной проблемой для нашего кода может стать то, что из домена приложения нельзя выгрузить сборку. Если в папке с приложением окажется много сборок, которые будут загружаться в процессе поиска плагинов, то это приведет к ненужному расходу памяти. В таком случае можно создать новый домен, вызвав статическую функцию AppDomain.CreateDomain, загрузить все сборки в созданный домен и получить названия только тех сборок, которые содержат плагины, выгрузить домен функцией AppDomain(Unload) и загрузить сборки с плагинами в домен.
    После того, как все плагины найдены, создаем для них в функции CreatePluginsMenu пункты меню. Названия пунктов меню берутся из ключей в хеш-таблице. Для обработки событий от меню для вызова плагинов создается обработчик OnPluginClick. В обработчике определяется названия пункта меню, который выбрал пользователь, и по нему, как по ключу в хеш-таблице, получаем интерфейс IPlugin соответствующего плагина. У плагина вызывается метод Transform, в качестве параметра this (т.к. класс формы наследуется от интерфейса IMainApp).

void CreatePluginsMenu()
{
    // создаем обработчик для команд меню для плагинов
    EventHandler handler = new EventHandler(OnPluginClick);

    foreach (string name in plugins.Keys)
    {
        MenuItem item = new MenuItem(name, handler);
        menuItemPlugins.MenuItems.Add(item);
    }
}


private void OnPluginClick(object sender, EventArgs args)
{
    Interface.IPlugin plugin = (Interface.IPlugin)plugins[((MenuItem)sender).Text];
    plugin.Transform(this);
}

    Окончательно приложение с загруженной фотографией выглядит следующим образом. В пункте меню Фильтры добавлены два плагина. 


 

Создание плагина

    Для создания плагинов создаем новый проект ClassLibrary, добавляем ссылку на сборку Interface и реализуем свойства и методы интерфейса IPlugin. Для примера были разработаны 2 плагина: ReverseTransform для отражения изображения по вертикали и RandomTransform для внесения случайного шума в изображение. Приведем их код и результаты применения к изображению.

public class ReverseTransform : Interface.IPlugin
{
    public string Name
    {
        get { return "Переворот изображения"; }
    }

    public string Version
    {
        get { return "1.0"; }
    }

    public string Author
    {
        get { return "Кондратьев Денис"; }
    }

    public void Transform(Interface.IMainApp app)
    {
        Bitmap bitmap = app.Image;

        for (int i = 0; i < bitmap.Width; ++i)
            for (int j = 0; j < bitmap.Height / 2; ++j)
            {
                Color color = bitmap.GetPixel(i, j);
                bitmap.SetPixel(i, j, bitmap.GetPixel(i, bitmap.Height - j - 1));
                bitmap.SetPixel(i, bitmap.Height - j - 1, color);
            }

        app.Image = bitmap;
    }
}

public class RandomTransform : Interface.IPlugin
{
    public string Name
    {
        get { return "Случайная трансформация"; } 
    }

    public string Version
    {
        get { return "1.0"; }
    }

    public string Author
    {
        get { return "Кондратьев Денис"; }
    }

    public void Transform(Interface.IMainApp app)
    {
        Bitmap bitmap = app.Image;
        Random rand = new Random(DateTime.Now.Millisecond);
        int pixels = (int)(0.1 * bitmap.Width * bitmap.Height);

        for (int i = 0; i < pixels; ++i)
             bitmap.SetPixel(rand.Next(bitmap.Width - 1), rand.Next(bitmap.Height), Color.FromArgb(rand.Next(255), rand.Next(255), rand.Next(255)));

        app.Image = bitmap;
    }
}


Поворот изображения плагином ReverseTransform


Внесение случайного шума плагином RandomTransfor

    Свойства для получения номера версии плагина и авторства в нашей программе не используются, но с их помощью можно выводить информацию об установленных плагинах, как в Adobe Photoshop. После компиляции нужно поместить плагины в папку с приложением MainApp.


 

А также другие работы, которые могут Вас заинтересовать

30633. Евангельские мотивы в романе М. А. Булгакова «Мастер и Маргарита» 15.42 KB
  Евангельские истории художественно преображены Булгаковым в главах представляющих собою роман в романе – произведение мастера о Понтии Пилате и Иешуа ГаНоцри. Многие из них – измененные имена Нового и Ветхого Завета: Иешуа Понтий Пилат Иуда Марк Аврелий Кот Бегемот имеет параллель своего имени в Библии Азазелло – падший ангел в Ветхом завете.Свобода и несвобода в философском аспекте поставлена в спорах Иешуа и ГаНоцри с могущественным римским прокуратором Понтием Пилатом в душе которого происходит борьба человеческого и...
30634. Какова роль евангельского сюжета о воскрешении Лазаря в понимании идеи романа Ф.М. Достоевского «Преступление и наказание» 12.72 KB
  В центре Преступления и наказания помещен эпизод чтения XI главы Евангелия от Иоанна о воскрешении Лазаря.Образ воскресения Раскольникова действительно связан с евангельским повествованием о воскрешении Лазаря Христом которое читает Раскольникову Соня. Сама же Соня при чтении мысленно сравнивает его с иудеями присутствовавшими при совершении неслыханного чуда воскрешения уже смердящего Лазаря и уверовавшими во Христа.
30635. Женские образы и судьбы в русской классической литературе 16.26 KB
  Только в русской литературе обращается так много внимания на изображение внутреннего мира и сложных переживаний женской души. Ее любовь к русскому народу к патриархальной старине к русской природе проходит через все произведение. Все эти черты воспитала в ней связь с русским народом и русской природой создавшими поистине русскую женщину человека большой душевной красоты.
30636. Земное и вечное в стихотворении А.А. Ахматовой «Приморский сонет» 15.8 KB
  Ахматовой Приморский сонет.Облик героини поэзии Ахматовой предстает в житейской простоте но в нем заключается пафос сильной личности. Ее лирическая героиня не отражает персональной судьбы Ахматовой а отражает проявление женской доли женского голоса. Предметный мир воспринимается уже в ином виде: три ступеньки кажутся вечностью любимый прием Ахматовой – оксюморон темный дом свечи горевшие равнодушножелтым огнем.
30637. Изображение народного характера в рассказе А.И. Солженицына «Матренин двор» 12.62 KB
  Солженицына является то что он исследует русский характер. Но главным в образе героини является доброта доброе расположение духа добрая улыбка побеждающая в душе все тяготы и заботы.Неслучайно Солженицын хотел назвать свой рассказ Не стоит село без праведника поскольку именно Матрена является истинным праведником: она тот самый праведник без которого не стоит село.Реалистическая достоверность изображенных событий накладывает особый отпечаток на образ Матрены – ее жизнь является не только символом праведности но и ощутимым...
30638. Интерьер как средство характеристики героя 18.61 KB
  Интерьер его домика состоящего из шести крошечных комнат говорит о том что перед нами маленький человек очевидно небогатый который не претендует на значимость любит уют. И действительно толстоногий стол заваленный почерневшими от старинной пыли бумагами говорит о том что Василий Иванович занимается работой но делает это время от времени. О том что Василий Иванович интересуется естественными науками и физическими опытами говорит сломанная электрическая машина но это увлечение осталось в прошлом так как она до сих...
30639. «Испытание любовью» как средство характеристики героя в произведениях отечественной классики 19 века 14.36 KB
  Но можем сказать что испытание любовью открыло насколько изменился Онегин.Ещё одним героем прошедшим через испытание любовью является Печорин Лермонтова Герой нашего времени. В таких случаях жизнь мстит за себя: в крепости Печорин пытается заглушить тоску сердца любовью к дикарке Бэле но очень скоро убеждается в бесплодности своих усилий.
30640. Стихотворение М.Ю. Лермонтова «Родина». (Восприятие, истолкование, оценка.) 15.68 KB
  Лермонтова Родина. Ни в одном произведении Лермонтов не достигал такой поэтической ясности как в стихотворении Родина написанном в 1841 году. Родина отразила целый комплекс народных понятий и представлений сложившихся на протяжении столетий и выявившихся как разум народа в отличие от предрассудков предубеждений мгновенного настроения толпы или тех чувств которые несли на себе печать векового рабства и порабощения. И как богат этот опыт как он многосторонен как утонченно и благородно народное чувство и как велик его разум...
30641. Любовная лирика А.С. Пушкина. Чтение наизусть и анализ одного из стихотворений по выбору учащегося 13.5 KB
  Пушкина. Любовь в поэзии Пушкина – это глубокое нравственно чистое и самоотверженное чувство облагораживающее и очищающее человека.Я помню чудное мгновенье одно из самых проникновенных трепетных гармонических стихотворений Пушкина относящихся к любовной тематике. Вновь возрождение чувств в душе поэта вновь прилив жизненных сил вновь – приход творческого вдохновения: Душе настало пробужденье: И вот опять явилась тыВ этом стихотворении Пушкина любовная тема сочетается с философскими раздумьями поэта о своей жизни о радости...