Пример приложения, использующего дополнение MAF

64

Подготовка решения, использующего модель дополнений

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

В настоящее время Visual Studio не имеет шаблона для создания приложений, использующих дополнения. Поэтому на вас возлагается обязанность создать эти папки и настроить проект Visual Studio для их использования. Ниже описан простейший подход:

  1. Создайте каталог верхнего уровня, который будет содержать все создаваемые в дальнейшем проекты. Его можно назвать, к примеру, c:\AddInTest.

  2. Создайте в этом каталоге новый проект WPF для принимающего приложения-хоста. Неважно, как этот проект будет назван, однако он должен быть помещен в каталог верхнего уровня, который был создан на первом шаге (например, c:\AddInTest\HostApplication).

  3. Добавьте новый проект библиотеки классов для каждого компонента конвейера и поместите их все в одно и то же решение. Как минимум, понадобится создать проект для одного дополнения (например, c:\AddInTest\FadeImageAddIn), одного представления дополнения (c:\AddInTest\AddInView), одного адаптера стороны дополнения (c:\AddInTest\AddInSideAdapter), одного представления хоста (c:\AddInTest\HostView) и одного адаптера стороны хоста (c:\AddInTest\HostSideAdapter). На рисунке ниже показан пример решения, которое включает приложение (под названием HostApplication) и два дополнения (с именами FadeImageAddIn и NegativeImageAddIn):

    Решение, использующее конвейер дополнения

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

  4. Теперь необходимо создать каталог сборки внутри каталога верхнего уровня. Именно здесь будут размещены все компоненты приложения и конвейера после компиляции. Этот каталог принято называть Output (например, c:\AddInTest\Output).

  5. По мере проектирования различных компонентов конвейера путь сборки каждого из них будет модифицироваться, чтобы компонент помещался в правильном подкаталоге. Например, адаптер дополнения должен быть скомпилирован в каталоге вроде c:\AddInTest\Output\AddInSideAdapters.

    Чтобы модифицировать путь сборки, дважды щелкните на узле Properties (Свойства) в окне Solution Explorer. Затем перейдите на вкладку Build (Сборка). В разделе Output (Выход) найдите текстовое поле по имени Output Path (Выходной путь). Необходимо использовать относительный выходной путь, находящийся на один уровень выше дерева каталогов и затем использующий каталог Output. Например, выходным путем для адаптера дополнения должен быть ..\Output\AddInSideAdapters. По мере построения каждого компонента в следующих разделах будет указано, какой путь сборки нужно использовать.

Есть еще одно обстоятельство, которое должно быть учтено при разработке модели дополнений в Visual Studio. Имеются в виду ссылки. Некоторые компоненты конвейера нуждаются в ссылках на другие компоненты конвейера. Однако вы не хотите копировать ссылаемые сборки туда, где находятся сборки, содержащие ссылки на них. Вместо этого вы полагаетесь на систему каталогов модели дополнений.

Чтобы предотвратить копирование ссылаемых сборок, необходимо выделить сборку в окне Solution Explorer (под узлом References (Ссылки)). Затем в окне Properties потребуется установить настройку Copy Local (Копировать локально) в False. При построении каждого компонента в следующих разделах будет указано, какие ссылки должны быть добавлены.

Предотвращение копирования локальных сборок

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

Приложение, использующее дополнения для манипулирования изображением

Контракт

Начальная точка определения конвейера дополнения для приложения - создание сборки контракта. Сборка контракта определяет две вещи:

В примере, показанном на рисунке выше, используется чрезвычайно простой контракт. Дополнение предоставляет метод по имени ProcessImageBytes(), который принимает байтовый массив с данными изображения, модифицирует его и возвращает модифицированный байтовый массив. Вот контракт, определяющий этот метод:

// Файл Contract.cs
using System;
using System.AddIn.Contract;
using System.AddIn.Pipeline;

namespace Contract
{
    [AddInContract]
    public interface IImageProcessorContract : IContract
    {
        byte[] ProcessImageBytes(byte[] pixels);
    }
}

Создаваемый класс контракта должен быть унаследован от интерфейса IContract и также оснащен атрибутом AddInContract. Как интерфейс, так и атрибут находятся в пространстве имен System.AddIn.Contract. Чтобы иметь доступ к ним в сборке контракта, потребуется добавить ссылку на сборку System.AddIn.Contract.dll.

Поскольку в примере с обработкой изображения не используется никаких специальных типов для передачи данных (а только обычные байтовые массивы), в сборке контракта типы не определяются.

Байтовые массивы могут быть переданы между приложением-хостом и дополнением, потому что массивы и байты являются сериализуемыми. Единственный дополнительный шаг, который нужно предпринять — это конфигурирование каталога сборки. Сборка контракта должна быть помещена в подкаталог Contracts корня дополнений, а это означает, что в качестве выходного пути в текущем примере можно использовать ..\Output\Contracts.

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

Например, фильтр, затемняющий изображение, может иметь настройку интенсивности, фильтр, выполняющий наклон изображения — настройку угла наклона и т.д. Принимающее приложение может затем применять эти параметры, вызывая метод ProcessImageBytes().

Представление дополнения

Представление дополнения (add-in view) предоставляет абстрактный класс, отражающий сборку контракта, и используется на стороне дополнения. Создать этот класс просто:

// Файл AddInView.cs
using System;
using System.AddIn.Pipeline;

namespace AddInView
{
    [AddInBase]
    public abstract class ImageProcessorAddInView
    {
        public abstract byte[] ProcessImageBytes(byte[] pixels);
    }
}

Обратите внимание, что класс представления дополнения должен быть оснащен атрибутом AddInBase. Этот атрибут находится в пространстве имен System.AddIn.Pipeline. Сборка представления дополнения требует ссылки на сборку System.AddIn.dll, чтобы иметь к ней доступ.

Сборка представления дополнения должна быть помещена в подкаталог AddInViews корня дополнения, а это значит, что в данном примере можно использовать выходной путь ..\Output\AddInViews.

Дополнение

Представление дополнения — это абстрактный класс, который не реализует никакой функциональности. Чтобы создать полезное дополнение, необходим конкретный класс — наследник абстрактного класса представления. В этот класс можно затем добавлять код, который выполняет реальную работу (в рассматриваемом случае — обработка изображения).

Следующее дополнение инвертирует значения цвета, чтобы создать эффект, подобный негативу. Ниже показан его полный код:

using System;
using System.AddIn;
using AddInView;

namespace NegativeImageAddIn
{
    [AddIn("Инверсия цвета картинки", Version = "1.0.0.0", 
        Publisher = "Imaginomics", 
        Description = "Используется для иверсии цветов на картинке, чтобы получить эффект негатива пленки.")]
    public class NegativeImageProcessor : ImageProcessorAddInView
    {
        public override byte[] ProcessImageBytes(byte[] pixels)
        {
            for (int i = 0; i < pixels.Length - 2; i++) 
            { 
                // Предполагается 24-битный цвет — каждый пиксель описан 
                // тремя байтами данных
                pixels [i] = (byte)(255 - pixels[i]);
                pixels[i + 1] = (byte)(255 - pixels[i + 1]); 
                pixels[i + 2] = (byte)(255 - pixels [i + 2]); 
            } 
            return pixels;
        }
    }
}

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

Инфраструктура дополнений в действительности делает копию исходного байтового массива и передает эту копию в домен приложения дополнения. Как только байтовый массив модифицирован и возвращен из метода, инфраструктура дополнений копирует его обратно в домен приложения хоста. Если бы ProcessImageBytes() не возвращал модифицированного подобным образом массива байтов, то хост никогда бы не увидел измененных данных изображения.

Чтобы создать дополнение, нужно просто унаследовать класс от абстрактного класса представления и оснастить его атрибутом AddIn. Вдобавок можно использовать свойства атрибута AddIn для применения имени, версии, издателя дополнения и его описания, как сделано здесь. Эта информация становится доступной хосту при обнаружении дополнений.

Сборка дополнения требует двух ссылок: одну на сборку System.AddIn.dll и еще одну — на проект представления дополнения. Однако свойство Copy Local ссылки на представление дополнения должно быть установлено в False. Причина в том, что представление дополнения не развертывается с самим дополнением. Вместо этого оно помещается в выделенный подкаталог AddInViews.

Дополнение должно быть помещено в собственный подкаталог внутри подкаталога AddIns корня дополнения. В рассматриваемом примере может применяться выходной путь вроде ..\Output\AddIns\NegativeImageAddIn.

Адаптер дополнения

Текущий пример обладает всей необходимой функциональностью дополнения, но все-таки еще остается пробел между дополнением и контрактом. Хотя представление дополнения моделируется по контракту, оно не реализует интерфейс контракта, используемого для взаимодействия между приложением и дополнением.

Недостающий ингредиент — адаптер дополнения. Он реализует интерфейс контракта. Когда вызывается метод на интерфейсе контракта, он вызывает соответствующий метод в представлении дополнения. Ниже приведен код наиболее простого адаптера дополнения, который можно создать:

using System;
using System.AddIn.Pipeline;

namespace AddInSideAdapter
{
    [AddInAdapter]
    public class ImageProcessoraddInAdapter :
        ContractBase, Contract.IImageProcessorContract
    {
        private AddInView.ImageProcessorAddInView view;

        public ImageProcessoraddInAdapter(AddInView.ImageProcessorAddInView view)
        {
            this.view = view;
        }

        public byte[] ProcessImageBytes(byte[] pixels)
        {
            return view.ProcessImageBytes(pixels);
        }
    }
}

Все адаптеры дополнений должны наследоваться от класса ContractBase (из пространства имен System.AddIn.Pipeline). Класс ContractBase унаследован от MarshalByRefObject, который позволяет адаптеру вызываться через границы домена приложения.

Все адаптеры дополнения также должны быть оснащены атрибутом AddInAdapter (из пространства имен System.AddIn.Pipeline). Более того, адаптер дополнения должен включать конструктор, принимающий в качестве аргумента экземпляр соответствующего представления. Когда инфраструктура дополнения создает адаптер дополнения, она автоматически использует этот конструктор и передает ему само дополнение. (Вспомните, что дополнение наследуется от абстрактного класса представления дополнения, ожидаемого конструктором.) Код просто должен сохранить это представление для последующего использования.

Адаптер дополнения требует трех ссылок: одну на System.AddIn.dll, одну на проект представления и одну на проект контракта. Свойство Copy Local ссылки контракта и представления должно быть установлено в False.

Сборка адаптера дополнения должна быть помещена в подкаталог AddInSideAdapters корня дополнения, а это означает, что в рассматриваемом примере можно использовать выходной путь ..\Output\AddInSideAdapters.

Представление хоста

Следующий шаг — построение стороны хоста конвейера дополнения. Хост взаимодействует с представлением хоста. Подобно представлению дополнения, представление хоста — это абстрактный класс, который точно отражает интерфейс контракта. Единственное отличие в том, что он не требует никаких атрибутов:

using System;

namespace HostView
{
    public abstract class ImageProcessorHostView
    {
        public abstract byte[] ProcessImageBytes(byte[] pixels);
    }
}

Сборка хоста представления должна быть развернута вместе с приложением-хостом. Подправить выходной путь можно вручную (например, чтобы в текущем примере сборка представления хоста была размещена в папке ..\Оutput). Или же, при добавлении ссылки на представление хоста в приложение-хост можно оставить свойство Copy Local равным True. Таким образом, представление хоста будет скопировано автоматически в тот же выходной каталог, что и приложение-хост.

Адаптер хоста

Адаптер стороны хоста наследуется от представления хоста. Он принимает объект, реализующий контракт, который может затем использовать при вызове своих методов. Этот тот же процесс пересылки, который использует и адаптер дополнения, но в обратном направлении.

В данном примере, когда приложение-хост вызывает метод ProcessImageBytes() представления хоста, оно в действительности вызывает ProcessImageBytes() в адаптере хоста. Адаптер хоста вызывает ProcessImageBytes() на интерфейсе контракта (и этот вызов переправляется через границы приложения, трансформируясь в вызов метода на адаптере дополнения).

Ниже приведен полный код адаптера хоста:

using System;
using System.AddIn.Pipeline;

namespace HostSideAdapter
{
    [HostAdapter]
    public class ImageProcessorHostAdapter : HostView.ImageProcessorHostView
    {
        private Contract.IImageProcessorContract contract;
        private ContractHandle contractHandle;

        public ImageProcessorHostAdapter(
            Contract.IImageProcessorContract contract)
        {
            this.contract = contract;
            contractHandle = new ContractHandle(contract);
        }

        public override byte[] ProcessImageBytes(byte[] pixels)
        {
            return contract.ProcessImageBytes(pixels);
        }
    }
}

Обратите внимание, что адаптер хоста на самом деле использует два поля-члена. Он сохраняет ссылку на текущий объект контракта и также сохраняет ссылку на объект System.AddIns.Pipeline.ContractHandle. Объект ContractHandle управляет жизненным циклом дополнения.

Если адаптер хоста не создает объект ContractHandle (и сохраняет ссылку на него), то дополнение будет освобождено немедленно по завершении кода конструктора. Когда приложение-хост попытается использовать дополнение, то получит исключение AppDomainUnloadedException.

Проект адаптера хоста нуждается в ссылках на System.Add.dll и System.AddIn.Contract.dll. Ему также необходимы ссылки на сборку контракта и сборку представления хоста (у обеих настройка Copy Local должна быть установлена в False). Выходной путь — подкаталог HostSideAdapters в корне дополнения (в рассматриваемом примере это ..\Output\HostSideAdapters).

Хост

Теперь, когда вся инфраструктура готова, последний шаг заключается в создании приложения, которое использует модель дополнений. Хотя хостом может служить исполняемое приложение .NET любого типа, в данном примере применяется приложение WPF.

Хост нуждается только в одной ссылке, которая указывает на проект представления хоста. Представление хоста — это точка входа в конвейер дополнения. Фактически теперь, когда завершена нелегкая работа по реализации конвейера, хост может не беспокоиться о том, как он управляется. Ему нужно только найти доступные дополнения, активизировать те из них, которые он желает использовать, и затем вызывать методы представления хоста.

Исходный код разметки окна приведен ниже:

<Grid Margin="3">
        <Grid.RowDefinitions>
            <RowDefinition></RowDefinition>
            <RowDefinition Height="2*"></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition></ColumnDefinition>
            <ColumnDefinition Width="Auto"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <ListBox Name="lstAddIns" Margin="3" SelectionChanged="lstAddIns_SelectionChanged">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Margin="3,3,0,8" HorizontalAlignment="Stretch">
                        <TextBlock Text="{Binding Path=Name}" FontWeight="Bold" ></TextBlock>
                        <TextBlock Text="{Binding Path=Publisher}" ></TextBlock>
                        <TextBlock Text="{Binding Path=Description}" FontSize="10" FontStyle="Italic"></TextBlock>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <Button Grid.Column="1" Name="cmdProcessImage" Click="cmdProcessImage_Click" Margin="0,3,3,3" 
                Padding="3" VerticalAlignment="Top" IsEnabled="False">Старт</Button>
        <Image Grid.Row="1" Grid.ColumnSpan="2" Name="img" Source="Forest.jpg" Margin="3" />
</Grid>

Первый шаг — нахождение доступных дополнений — называется обнаружением (discovery). Оно осуществляется через статические методы класса System.AddIn.Hosting.AddInStore. Чтобы загрузить дополнения, просто указывается путь к корню дополнений и вызывается метод AddInStore.Update(), как показано ниже:

private void Window_Loaded(object sender, RoutedEventArgs e)
{
            // В этом примере путь, из которого запускается 
            // приложение, также является корнем дополнений
            string path = Environment.CurrentDirectory;
            AddInStore.Update(path);
}

После вызова Update() система дополнений создаст два файла с кэшированной информацией. Файл по имени PipelineSegments.store будет помещен в корневой каталог дополнений. Этот файл включает информацию о различных представлениях и адаптерах. Файл по имени AddIns.store будет помещен в подкаталог AddIns, и он содержит информацию о доступных дополнениях.

При добавлении новых представлений, адаптеров или дополнений эти файлы можно обновлять повторным вызовом AddInStore.Update(). (Этот метод быстро возвратит управление, если никаких новых дополнений или компонентов конвейера не обнаружит.) Если есть причины ожидать проблем с существующими файлами дополнений, можно вместо этого вызывать метод AddInStore.Rebuild(), который воссоздаст файлы дополнений заново.

Как только файлы кэша созданы, можно выполнять поиск определенных дополнений. С помощью метода FindAddIn() осуществляется поиск одного определенного дополнения, а посредством метода FindAddIns() — всех дополнений, которые соответствуют указанному представлению хоста. Метод FindAddIns() возвращает коллекцию маркеров (tokens), каждый из которых представляет собой экземпляр класса System.AddIn.Hosting.AddInToken:

private void Window_Loaded(object sender, RoutedEventArgs e)
{
            // В этом примере путь, из которого запускается 
            // приложение, также является корнем дополнений
            string path = Environment.CurrentDirectory;
            AddInStore.Update(path);

            IList<AddInToken> tokens = AddInStore.FindAddIns(
                typeof(HostView.ImageProcessorHostView), path);

            lstAddIns.ItemsSource = tokens;
}

Для извлечения информации о дополнении предназначено несколько ключевых свойств (Name, Description, Publisher и Version). В приложении, обрабатывающем изображения, список маркеров привязан к элементу управления ListBox, и для каждого дополнения отображается некоторая базовая информация согласно следующему шаблону данных:

...
<ListBox Name="lstAddIns" Margin="3" SelectionChanged="lstAddIns_SelectionChanged">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Margin="3,3,0,8" HorizontalAlignment="Stretch">
                        <TextBlock Text="{Binding Path=Name}" FontWeight="Bold" ></TextBlock>
                        <TextBlock Text="{Binding Path=Publisher}" ></TextBlock>
                        <TextBlock Text="{Binding Path=Description}" FontSize="10" FontStyle="Italic"></TextBlock>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
</ListBox>
...

Экземпляр дополнения создается вызовом метода AddInToken.Activate<T>. В текущем приложении пользователь щелкает на кнопке Старт для активизации дополнения. Затем извлекается информация текущего изображения (показанного в окне), которая затем передается методу ProcessImageBytes() представления хоста. Вот как это работает:

private void cmdProcessImage_Click(object sender, RoutedEventArgs e)
{
            // Скопировать информацию изображения в байтовый массив
            BitmapSource originalSource = (BitmapSource)img.Source;
            int stride = originalSource.PixelWidth * originalSource.Format.BitsPerPixel / 8;
            stride = stride + (stride % 4) * 4;
            byte[] originalPixels = new byte[stride * originalSource.PixelHeight * originalSource.Format.BitsPerPixel / 8];

            originalSource.CopyPixels(originalPixels, stride, 0);

            // Получить выбранный маркер дополнения
            AddInToken token = (AddInToken)lstAddIns.SelectedItem;

            // Получить представление хоста
            HostView.ImageProcessorHostView addin = 
                token.Activate<HostView.ImageProcessorHostView>(AddInSecurityLevel.Internet);

            // Использовать дополнение
            byte[] changedPixels = addin.ProcessImageBytes(originalPixels);

            // Создать новый BitmapSource с данными измененного изображения и отобразить его
            BitmapSource newSource = BitmapSource.Create(originalSource.PixelWidth, 
                originalSource.PixelHeight, originalSource.DpiX, originalSource.DpiY, 
                originalSource.Format, originalSource.Palette, changedPixels, stride);
            img.Source = newSource;
}

При вызове метода AddInToken.Activate<T> "за кулисами" происходит несколько действий:

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

    Можно также выбрать уровень привилегий, предоставляемых новому домену приложения. (В данном примере они ограничены набором привилегий Интернет — существенно ограниченным набором прав, которые выдаются коду, запускаемому из Интернета.)

  2. Сборка дополнения загружается в новый домен приложения. Затем создается экземпляр дополнения посредством рефлексии, используя конструктор без аргументов. Как уже можно было видеть, дополнение наследуется от абстрактного класса в сборке представления дополнения. В результате загрузка дополнения также загружает в новый домен приложения сборку представления дополнения.

  3. Создается экземпляр адаптера дополнения в новом домене приложения. Дополнение передается адаптеру дополнения в качестве аргумента конструктора. (Дополнение указывается как представление дополнения.)

  4. Адаптер дополнения делается доступным домену приложения-хоста (через удаленный прокси). Однако он указывается как реализованный им контракт.

  5. В домене приложения-хоста создается экземпляр адаптера хоста. Адаптер дополнения передается адаптеру хоста через его конструктор.

  6. Адаптер хоста возвращается приложению-хосту (указанному через представление хоста). Приложение теперь может вызывать методы представления хоста, чтобы взаимодействовать с дополнением через конвейер.

Существуют и другие перегрузки метода Activate<T>, которые позволяют применять специальный набор привилегий (для тонкой настройки безопасности), определенный домен приложения (что удобно, если нужно запускать несколько дополнений в пределах одного домена приложения) и внешний процесс (что позволяет размещать дополнение в совершенно отдельной ЕХЕ-сборке приложения для обеспечения еще более высокой степени изоляции). Все эти примеры проиллюстрированы в справочной системе Visual Studio.

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

Добавление новых дополнений

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

using System;
using System.AddIn;

namespace FadeImageAddIn
{
    [AddIn("Затемнение картинки", Version = "1.0.0.0", Publisher = "SupraImage",
            Description = "Фильтр для динамического затемнения текущей картинки")]
    public class FadeImageProcessor : AddInView.ImageProcessorAddInView
    {
        public override byte[] ProcessImageBytes(byte[] pixels)
        {
            Random rand = new Random();
            int offset = rand.Next(0, 10);
            for (int i = 0; i < pixels.Length - 1 - offset; i++)
            {
                if ((i + offset) % 5 == 0)
                {
                    pixels[i] = 0;
                }
            }
            return pixels;
        }
    }
}

В данном примере это дополнение компилируется в выходной путь ..\Output\AddIns\FadeImageAddIn. Здесь нет необходимости создавать дополнительные представления или адаптеры. После развертывания этого дополнения (и последующего вызова метода Rebuild() или Update() класса AddInStore) приложение-хост найдет оба дополнения:

Другое дополнение
Пройди тесты
Лучший чат для C# программистов