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.


 

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

53653. Бюджетирование как инструмент финансового планирования. Финансовые бюджеты 27 KB
  Планирование текущей деятельности предприятия заключается в построении генерального бюджета, представляющего собой систему взаимосвязанных операционных и финансовых бюджетов
53654. Прямоугольник и квадрат 53 KB
  Цель: Формировать первоначальное представление о геометрических фигурах: прямоугольник и квадрат. Задачи: 1 уточнить понятия прямоугольника и квадрата выявить существенные признаки прямоугольника и квадрата 2 формировать способность к распознанию фигур на основе существенных свойств изображению и вычислению их периметра 3 развивать устные вычислительные навыки логическое мышление обогащать...
53655. Деление чисел с разными знаками 2.66 MB
  Организационный момент Учитель: Здравствуйте садитесь. Проверка домашнего задания учитель включает проектор со слайдом домашней работы на котором также отражены критерии оценки работы Учитель: Поменяйтесь тетрадями. ученики сверяют ответы Учитель: Критерий оценки: все решено верно ставьте ПЯТЬ один минус ЧЕТЫРЕ дватри минуса ТРИ во всех остальных случаях ДВА. Устная работа Таблица с правилом знаков на магнитной доске Учитель: повторим правило знаков для умножения внимание на магнитную доску.
53656. Сложение 36 KB
  Что обозначают точки Сравните эти ряды что вы заметили в 1ом ряду числа расположены в порядке возрастания в 2ом ряду в порядке убывания 2. слайд № 5 счет до 10 в прямом и обратном порядке Назову я вам число Всем известное оно.
53657. Смысл сложения. Выражение. Равенство 31.5 KB
  Оборудование: таблички со словами: выражение сумма слагаемые значение суммы равенства; кодоскоп с заданиями на пленке таблица Грибы счетный материал белки и грибы 30 шт. наборное полотно калькуляторы корзинки кондитерские грибы на ватмане рисунок Старичка моховичка схема объединения множеств. Приглашаю вас друзья По грибы сегодня я.Коля с мамой в лес ходил Там грибы он находил А когда домой пришел Все грибы сложил на стол.
53658. Прием вычитания с переходом через десяток 58.5 KB
  Записать на доске пример 124 Как можно вычесть 4 Можно вычесть 4 по частям. Убрать два круга из нижнего ряда Сколько мы вычли из 12 Сколько осталось кругов Записать 122=10 А нам надо вычесть 4. Дополнить первую запись 12 4 2 Затем убрать с наборного полотна еще два круга и записать: 12 2 2 Сколько кругов осталось Как мы из...
53659. Письменное сложение двузначных чисел с переходом через разряд 41 KB
  Работа одного ученика у доски остальные в тетрадях. Что нам нужно сделать Работают возле доски. Работа одного ученика возле доски остальные в тетрадях. Теперь что нам нужно сделать Какое действие будем выполнять Работа одного ученика возле доски остальные в тетрадях.
53660. Страна музыкальных инструментов 73 KB
  Цель урока: показать многообразие музыкальных инструментов; познакомить детей с различными видами музыкальных инструментов; освоить игру на детских музыкальных инструментах. Русский наигрыш полянка Слушание Исполнение песни детьми Слушание Слушание Слушание Игра детей на детских музыкальных инструментах закрепление материала...
53661. Что такое экология? 79.5 KB
  Формировать УУД: Личностные: способность к самооценке на основе критерия успешности учебной деятельности. Регулятивные УУД: умение определять и формулировать цель на уроке с помощью учителя; понимать учебную задачу урока; высказывать своё предположение; контролировать свои действия в процессе выполнения задания и исправлять ошибки делать выводы отвечать на итоговые вопросы урока и оценивать свои достижения; планировать своё действие в соответствии с поставленной задачей; Воспитывать культуру поведения при фронтальной работе при...