Взаимодействие с хостом

99

»» СКАЧАТЬ ИСХОДНИКИ ПРОГРАММЫ (VS 2012)

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

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

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

Дополнение, сообщающее о продвижении

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

Ниже приведен интерфейс, описывающий, как дополнение должно сообщать о продвижении, посредством вызова метода по имени ReportProgress() в приложении-хосте:

public interface IHostObjectContract : IContract
{
        void ReportProgress(int progressPercent);
}

Как и интерфейс дополнения, интерфейс хоста должен наследоваться от IContract. В отличие от интерфейса дополнения интерфейс хоста не использует атрибут AddInContract, поскольку не реализуется дополнением.

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

public abstract class HostObject
{
        public abstract void ReportProgress(int progressPercent);
}

Обратите внимание, что определение класса не использует атрибут AddInBase ни в одном из проектов.

Действительная реализация метода ReportProgress() находится в приложении-хосте. Ему нужен класс, унаследованный от класса HostObject (в сборке представления хоста). Ниже приведен пример, использующий процент для обновления элемента управления ProgressBar:

private class AutomationHost : HostView.HostObject
{
            private ProgressBar progressBar;

            public AutomationHost(ProgressBar progressBar)
            {
                this.progressBar = progressBar;
            }
            public override void ReportProgress(int progressPercent)
            {
                // Обновить контрол в потоке окна
                progressBar.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
                    (ThreadStart)delegate()
                    {
                        progressBar.Value = progressPercent;
                    }
                );
            }
}

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

Решение заключается в том, чтобы приложение-хост передало ссылку HostObject дополнению. Обычно этот шаг будет выполнен при первоначальной активизации дополнения. По соглашению метод, используемый приложением-хостом для передачи этой ссылки, часто называется Initialize().

Вот как выглядит обновленный контракт для дополнений приложения обработки изображений:

[AddInContract]
public interface IImageProcessorContract : IContract
{
        byte[] ProcessImageBytes(byte[] pixels);
        void Initialize(IHostObjectContract hostObj);
}

При вызове Initialize() дополнение просто сохраняет ссылку для последующего использования. Затем оно может вызывать метод ReportProgress(), когда это понадобится, как показано ниже:

...
public class NegativeImageProcessor : ImageProcessorAddInView
{
        private HostObject host;
        public override void Initialize(HostObject hostObj)
        {
            host = hostObj;
        }
         
        public override byte[] ProcessImageBytes(byte[] pixels)
        {
            int iteration = pixels.Length / 100;
            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]);

                if (i % iteration == 0)
                    host.ReportProgress(i / iteration);
            } 
            return pixels;
        }
}

До сих пор код не представлял никаких серьезных трудностей. Однако последний фрагмент — адаптеры — несколько сложнее всего, что было ранее. Теперь, когда к контракту дополнения добавлен метод Initialize(), также нужно добавить его в представления хоста и дополнения. Однако сигнатура метода не может соответствовать интерфейсу контракта. Дело в том, что метод Initialize() в интерфейсе ожидает в качестве аргумента IHostObjectContract. Представления, которые никак не связаны с контрактом, не имеют никакого понятия о IHostObjectContract. Вместо этого они используют описанный ранее абстрактный класс HostObject:

public abstract class ImageProcessorHostView
{
        public abstract byte[] ProcessImageBytes(byte[] pixels);
        public abstract void Initialize(HostObject host);
}

Адаптеры представляют собой сложную часть системы. Они призваны заполнить пробел между абстрактным представлением HostObject и интерфейсом IHostObjectContract.

Например, рассмотрим ImageProcessorHostAdapter на стороне хоста. Он унаследован от абстрактного класса ImageProcessorHostView, в результате чего реализует версию Initialize(), принятую от экземпляра HostObject. Этот метод Initialize() должен преобразовать представление в контракт, после чего вызвать метод IHostObjectContract.Initialize().

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

Ниже показан измененный адаптер с новым классом HostObjectViewToContractHostAdapter, который выполняет работу, и метод Initialize(), использующий его для перехода от класса представления к интерфейсу контракта:

[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);
        }

        public override void Initialize(HostView.HostObject host)
        {
            HostObjectViewToContractHostAdapter hostAdapter = new HostObjectViewToContractHostAdapter(host);
            contract.Initialize(hostAdapter);
        }
}

public class HostObjectViewToContractHostAdapter : ContractBase, Contract.IHostObjectContract
{
        private HostView.HostObject view;

        public HostObjectViewToContractHostAdapter(HostView.HostObject view)
        {
            this.view = view;
        }

        public void ReportProgress(int progressPercent)
        {
            view.ReportProgress(progressPercent);
        }
}

Аналогичная трансформация имеет место в адаптере дополнения, но в обратном направлении. Здесь ImageProcessoraddInAdapter реализует интерфейс IImageProcessorContract. Ему нужно взять объект IHostObjectContract, который он получает в своей версии метода Initialize(), и затем преобразовать контракт в представление. Затем он может передавать вызов наряду с вызовом метода Initialize() в представлении. Ниже показан код:

[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);
        }

        public void Initialize(Contract.IHostObjectContract hostObj)
        {
            view.Initialize(new HostObjectContractToViewAddInAdapter(hostObj));
        }
}

public class HostObjectContractToViewAddInAdapter : AddInView.HostObject
{
        private Contract.IHostObjectContract contract;
        private ContractHandle handle;

        public HostObjectContractToViewAddInAdapter(Contract.IHostObjectContract contract)
        {
            this.contract = contract;
            this.handle = new ContractHandle(contract);
        }

        public override void ReportProgress(int progressPercent)
        {
            contract.ReportProgress(progressPercent);
        }
}

Теперь, когда хост вызывает Initialize() на дополнении, он может пройти через адаптер хоста (ImageProcessorHostAdapter) и адаптер дополнения (ImageProcessoraddInAdapter), прежде чем быть вызванным на самом дополнении. Когда дополнение вызывает метод ReportProgress(), он проходит те же этапы, но в обратном порядке. Сначала он проходит через адаптер дополнения (HostObjectContractToViewAddInAdapter), а затем переходит к адаптеру хоста (HostObjectViewToContractHostAdapter).

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

Намного лучший подход предусматривает выполнение затратного по времени вызова ProcessImageBytes() в фоновом потоке — либо за счет создания объекта Thread вручную, либо с применением BackgroundWorker. Затем, когда пользовательский интерфейс должен быть обновлен (при вызове ReportProgress() и возврате окончательного изображения), необходимо воспользоваться методом Dispatcher.BeginInvoke() для обратной маршализации вызова в поток пользовательского интерфейса. Ниже показан видоизмененный код приложения хоста (не забудьте добавить элемент ProgressBar в окно):

...
using System.AddIn.Hosting;
using System.Windows.Threading;
using System.Threading;

namespace HostApplication
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        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;

            automationHost = new AutomationHost(progressBar);
        }

        private AutomationHost automationHost;

        // Эти переменные устанавливаются в потоке пользовательского интерфейса и читаются в фоновом потоке
        private BitmapSource originalSource;
        private int stride;
        private byte[] originalPixels;
        private HostView.ImageProcessorHostView addin;

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

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

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

            // Launch the image processing work on a separate thread.
            Thread thread = new Thread(RunBackgroundAddIn);
            thread.Start();
        }

        private void RunBackgroundAddIn()
        {
            byte[] changedPixels = addin.ProcessImageBytes(originalPixels);

            // Обновить контрол в потоке окна
            this.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
                    (ThreadStart)delegate()
                    {
                        BitmapSource newSource = BitmapSource.Create(originalSource.PixelWidth,
                            originalSource.PixelHeight, originalSource.DpiX, originalSource.DpiY,
                            originalSource.Format, originalSource.Palette, changedPixels, stride);

                        img.Source = newSource;
                        progressBar.Value = 0;
                        addin = null;
                    }
                );
        }

        private void lstAddIns_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            cmdProcessImage.IsEnabled = (lstAddIns.SelectedIndex != -1);
        }

        private class AutomationHost : HostView.HostObject
        {
            private ProgressBar progressBar;

            public AutomationHost(ProgressBar progressBar)
            {
                this.progressBar = progressBar;
            }
            public override void ReportProgress(int progressPercent)
            {
                // Обновить контрол в потоке окна
                progressBar.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
                    (ThreadStart)delegate()
                    {
                        progressBar.Value = progressPercent;
                    }
                );
            }
        }
    }
}
Пройди тесты
Лучший чат для C# программистов