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.


 

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

1523. Теория программирования на языке Oracle 164 KB
  Архитектура Oracle. База данных. Физические и логические сегменты. Создание базы данных Oracle. Управляющие файлы. Создание, удаление и перемещение (переименование) управляющих файлов. Файлы данных. Создание, перемещение (переименование) файлов данных. Изменение состояния файлов данных. Использование CPU для нужд Oracle.
1524. Инновационный проект по разработке модели термопластавтомата 196.23 KB
  Характеристика инновационного проекта по разработке модели термопластавтомата на предприятии ООО Имид. Назначение и техническое описание инновационного проекта. Оценка эффективности инновационного проекта. Расчет затрат на электроэнергию по проекту. Анализ показателей эффективности инновационного проекта. Анализ чувствительности проекта и оценка рисков.
1525. Расчет экономических показателей деятельности предприятия 130.41 KB
  Расчет показателей динамики и состояния основных производственных фондов. Анализ влияния факторов на прирост объема произведенной продукции. Оценка влияния отдельных факторов на изменение прибыли от реализации продукции. Оценка влияния отдельных факторов на рентабельность производства.
1526. Аппарат для непрерывного преобразования значения гидростатического давления 76.5 KB
  Использование ЖКИ в диапазонах температуры окружающего воздуха. Шифр преобразователя, код модели, максимальный верхний предел измерений, ряд верхних пределов измерений, пределы допускаемых основных приведенных погрешностей преобразователей. Поверку АИР-20/М2 проводят органы Государственной метрологической службы.
1527. Финансовый и налоговый контроль 187 KB
  Понятие финансов, финансовой системы и финансовой деятельности государства и муниципальных образований. Компетенция государственных и муниципальных органов в области финансового контроля: представительных органов, исполнительных органов власти. Компетенция Счетная Палата РФ в сфере финансового контроля. Участники отношений, регулируемых законодательством о налогах и сборах.
1528. Расчет материальных затрат предприятия 67.99 KB
  Расчет расходов по освоению изделия и на специальную технологическую оснастку. Расчет заводской себестоимости и полной себестоимости агрегата. Структура основных материалов в черном весе турбоагрегата. Расчет стоимости реализуемых отходов.
1529. Логіка і методологія 166.5 KB
  РОБИТЬСЯ СПРОБА РОЗГЛЯНУТИ ПИТАННЯ, ЧИ ЛОГІКА Є МИСТЕЦТВОМ, ШЛЯХОМ ВИЗНАЧЕННЯ Й ПОДІЛУ МИСТЕЦТВА ВЗАГАЛІ. СТВЕРДЖУЄТЬСЯ, ЩО ЛОГІКА Є МИСТЕЦТВО, І ВІДКИДАЄТЬСЯ ТЕ, ЩО ЗАПЕРЕЧУЄ ЦЮ ДУМКУ. ПИТАННЮ, ЧИ ГІДНА ЛОГІКА НАЗИВАТИСЬ НАУКОЮ, ПЕРЕДУЄ РОЗВІДКА ПРО ВИЗНАЧЕННЯ І ПОДІЛ НАУКИ ВЗАГАЛІ. ВСТАНОВЛЮЄТЬСЯ, ЩО ЛОГІКА Є НАУКОЮ У ПРЯМОМУ РОЗУМІННІ СЛОВА, Й ВІДКИДАЮТЬСЯ АРГУМЕНТИ ПРОТИВНИКІВ У ЦЬОМУ ПИТАННІ.
1530. Приспособление для сверления отверстий и фрезерования поверхностей детали 94.69 KB
  Деталь Щит представляет собой литой корпус. Метод получения заготовки – литье в кокиль. Деталь изготавливается из алюминиевого сплава АЛ7ч(АЛ9). АЛ9 – конструкционный герметичный сплав, отличается высокими литейными свойствами и герметичностью изготовленных из них отливок, обладает удовлетворительной коррозионной стойкостью.
1531. Расчет продуктов производства пива 54.85 KB
  Производим расчет выхода товарного пива на 100кг перерабатываемого сырья. Характеристика готовой продукции и процент выпуска пива. Расчёт количества промежуточных продуктов и готового пива.