Pull to refresh

VSTO и CAB: Интеграция .NET приложения в Microsoft Word

Reading time 14 min
Views 13K
VSTO расшифровывается как Visual Studio Tools for Office. Эти средства позволяют довольно легко скрещивать ужа с ежом — писать .NET приложения, исполняемые CLR в среде Microsoft Office. В частности, программисты обладают возможностью создавать подключаемые модули (плагины) и «кастомизированные» шаблоны для документов почти ко всему основному семейству продуктов Microsoft Office.

В статье приведена инфраструктура Windows Forms проекта, в котором Microsoft Word воспринимается приложением в качестве шелла. В статье раскрыты несколько интересных моментов использования Composite UI Application Block, в частности подключение инфраструктуры доменной модели Word в сервисам расширения каркаса, а так же приведены некоторые факты и особенности разработки с использованием средств VSTO.

Задача


Предположим, у нас есть несколько десятков людей, которые пишут документы и постоянно работают с их редакциями. Sharepoint и другие порталы по каким-то причинам не подходят, поэтому требуется реализация собственной бизнес-логики. Усложним задачу — люди работают в области, очень далекой от компьютерной тематики и хорошо знают только пакет Microsoft Office. Еще усложним — контора бедная, Microsoft Office у большинства народа версии 2003-ей. Кое где, у больших начальников и Главного босса стоит 2007ой. Парк машин разнородный — начиная от 2000-ой винды и до Windows Vista.

Решение

Одним из решений является плагин для Microsoft Office. Его можно написать на COM-е, через голый Office Interop, или же через такой же интероп, но по хитрому обернутый в средства VSTO. Эти средства и задействуем. Пользователь будет запускать Word, среда исполнения VSTO будет подцеплять сборки .NET и каким-то образом дополнять элементы управления Microsoft Word, позволяя пользователю выполнить нужную в задаче бизнес-логику.

А к чему все это?

(Это кому не терпиться узнать, чем все закончится). На выходе я выложу Visual Studio solution, который позволит в полпинка создать свое приложение, подключаемое в Microsoft Word. По ходу дела я объясню практически все, что и как в этом солюшене используется.

Что потребуется для работы

  1. Microsoft Word 2003 SP3
  2. VSTO 2005 SE
  3. VSTO 2005 SE Runtime
  4. CAB
  5. Visual Studio 2005 или 2008

Зачем CAB?

Для того, чтобы определить явное разделение функционала, связанного с Microsoft Word от простой Windows Forms логики. Проще говоря, наш плагин будет являться модулем CAB, и при желании, мы легко сможем подключить эту логику в другие приложения CAB. Еще проще говоря, использование CAB минимизирует количество glue-кода.

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

Подробнее про VSTO

Вообще говоря, VSTO не только плагины для Word-а умеет создавать. Это мощная система, сейчас она уже есть в третьей версии. VSTO — это врата для разработчика в мир автоматизации и программирования Office-based решений. В двух словах — разработчик .NET получает доступ к доменной модели приложения Office и делает с ней что хочет.

Все эти решения можно разделить на два типа:
  1. Уровень документа (Document Level Customization)
  2. Уровень приложения (Application Level Customization)

Это независимые уровни. На первом создаются шаблоны документов для приложений Microsoft Office. Можно взять шаблон Excel, разнообразить его разными пользовательскими элементами управлениями (кнопками, гридами), и добавить всякую логику — например, чтобы по событию нажатия кнопки собранные пользователем данные отправлялись через веб-сервис на какой-то сервер и обрабатывались там. На этом уровне разработчику доступен Action Pane (в 2003-ей версии), куда можно подключать свои элементы управления, плюс еще очень много всего в 2007-ой версии.

Application Level Customization означает то, что в среду приложения Microsoft Office подключается модуль расширения (т.н. add-in). Собственно, про него я изначально и начал говорить. Этот модуль расширения является локомотивом бизнес-приложения и позволяет подключать в Microsoft Office все возможности платформы .NET. Я буду показывать именно решение второго уровня.

Если кто заинтересовался возможностями VSTO — очень много про них есть в msdn.

Псевдозадача

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

Более подробно. Пользователь запускает Microsoft Word. После запуска среди тулбаров пользователю будет доступен ТР — тулбар расширения (т.е., нашего расширения). На нем находится кнопка, по нажатию которой пользователю показывется окно с кнопкой «Получить текст» и элементом управления multiline textbox. Пользователь нажимает на кнопку «Получить текст», выделенный в документе текст копируется в textbox. Все счастливы.

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

Создание solution

После установки VSTO в студии при создании нового solution станут доступны Office проекты:



Будьте аккуратны с названием — оно одновременно будет являться названием root namespace и поменять его можно будет только ручками через выгрузку проекта.

На выходе получается солюшен с двумя проектами:


WordCAB — это, собственно, add-in. Второй — не менее важный, это deployment проект для модуля расширения. Почему он важен?

Дело в том, что установка модуля расширения на рабочие станции пользователей — весьма трудоемкая затея. Она заключается не только в том, чтобы просто скопировать библиотеки в нужную папку. Для успешного фукнционирования модуля требуется прописать кучу ключей в реестр. Если копнуть глубже, получается, что модуль расширения подключается к Microsoft Office как COM-библиотека, со всеми вытекающими. Кому интересно, все нужные ключи реестра можно поглядеть в deployment-проекте (Нажать правой кнопкой, View->Registry).

В классе ThisAddIn находится точка доступа в модуль:
public partial class ThisAddIn
{
  private void ThisAddIn_Startup(object sender, System.EventArgs e)
  {
   new AddInApplication().Run();
  }

  private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
  {
  }

  #region VSTO generated code

  /// <summary>
  /// Required method for Designer support - do not modify
  /// the contents of this method with the code editor.
  /// </summary>
  private void InternalStartup()
  {
    this.Startup += new System.EventHandler(ThisAddIn_Startup);
    this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
  }
  
  #endregion
}


* This source code was highlighted with Source Code Highlighter.


В ThisAddIn_Startup будем писать код. Если быть точнее, будем запускать CAB-приложение.

Что такое CAB-приложение?

Это класс такой. В нем есть точка запуска — метод Run(), и некоторая инфраструктура каркаса — в частности, имеются доступ к главному прецеденту «Приложение» (т.е., к основному, рутовому WorkItem) и возможности переопределить системные сервисы, добавляемые в приложения по-умолчанию. Этот класс уже реализован в базовом виде, а программисту требуется параметризировать его — указать тип рутового прецедента и тип главной формы:

internal sealed class AddInApplication : WindowsFormsApplication<AddInWorkItem, object>
{
  protected override void Start()
  {
  }
}


* This source code was highlighted with Source Code Highlighter.


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

По ходу пьесы мне пришло в голову немного прояснить модель приложения для модуля расширения в Microsoft Word.

Модель приложения Word Add-in
Итак, пользователь работает с документами. Более того, в единицу времени он может работать только с одним документом. Поэтому модуль расширения имеет четкий контекст — текущий открытый документ. (Кстати, в модели Microsoft Word VSTO этот документ обозначен как Globals.ThisAddIn.Application.ActiveDocument). Отсюда (я сделал) вывод (или упрощение) — показывать пользовательские бизнес-элементы имеет смысл в модальном режиме, поскольку в ином случае нарушается контекст. Элементы управления, открытые для одного документа, должны существовать только во время активности этого документа.

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

Modal Workspace

Пользовательские элементы управления в каркасе CAB показываются в специальных так называемых рабочих зонах (Workspace). Согласно описанному выше поведению, нам требуется показывать элементы управления в модальном режиме. Поскольку родной CAB-овский WindowWorkspace оказался дубоват и выглядит некрасиво, я написал простенький свой — приводить код не буду, потому что его пара-тройка экранов. Насладиться творчеством можно скачав solution с кодом (ссылка на архив приведена в конце статьи).

Регистрация модальной рабочей зоны происходит в методе InitializeServices() главного прецедента — AddInWorkitem:

Workspaces.Add(( new SimpleModalWorkspace() ), WorkspaceNames.MainWorkspace);

* This source code was highlighted with Source Code Highlighter.


Итого: под ключем-идентификатором WorkspaceNames.MainWorkspace зарегистрировали модальную рабочую зону. Доступиться до нее теперь можно откуда угодно, где есть ссылка на главный прецедент: запрос интерфейса IWorkspace из коллекции Workspaces по указанному выше ключу. Все просто, как апельсин! (с) х/ф «Терминатор 2»

Фабрика элементов управления Microsoft Word

Речь пойдет о тулбаре Word и кнопках на нем. В VSTO это обертки над COM-объектами — CommandBar и CommandBarButton из пространства имен Microsoft.Office.Core. Жутко глючные штуки, если честно, особенно их анимация. Понять все тонкости и детали удалось только после ударного троллинга на форумах VSTO.

В чем идея — идея состоит в том, чтобы избавить программиста и разработчиков бизнес-модулей от необходимости работать с COM-обертками. Для этого, мы (то есть, я) интегрируем модель Misrosoft Word в механизм так называемых мест расширения (UiExtensionSite) каркаса CAB.

Механизм мест расширения состоит из функциональных связок-добавлений. Проще говоря, в коде необходимо визуализировать отношения подчиненных элементов. В нашем случае, эти отношения такие:
  1. В массив тулбаров Microsoft Word вставляется тулбар
  2. В коллекцию кнопок на тулбаре вставляется кнопка
  3. В кнопку уже ничего не вставляется (не рассматриваем случае комбобоксов, хотя они присутствуют), поэтому это терминальный объект

Итого, у нас получились три подчиненные друг другу сущности:
  1. IWordCommandBarContainer — контейнер тулбаров Microsoft Word
  2. IWordCommandBar — контейнер кнопок на тулбаре
  3. IWordButton — кнопка


К слову сказать, помимо определения элементов управления Word конкретные типы интерфейсов IWordCommandBar и IWordButton являются адаптерами к упомянутым выше CommandBar и CommandBarButton соответственно.

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

public class CommandBarUIAdapterFactory : IUIElementAdapterFactory
{
  public IUIElementAdapter GetAdapter(object uiElement)
  {
    if ( uiElement is IWordCommandBarContainer ) //тулбары
      return new CommandBarUIAdapter(( IWordCommandBarContainer )uiElement);
    if ( uiElement is IWordCommandBar ) //кнопки в тулбарах
      return new CommandBarButtonUIAdapter(( IWordCommandBar )uiElement);

    throw new ArgumentException("uiElement");
  }

  public bool Supports(object uiElement)
  {
    return ( uiElement is IWordCommandBarContainer ) || ( uiElement is IWordCommandBar );
  }
}


* This source code was highlighted with Source Code Highlighter.


А вот реализация коллекции тулбаров (с возможностью добавить новый!)
internal class BarCollection : IWordCommandBarContainer
{
  #region IWordCommandBarContainer Members

  public void AddBar(IWordCommandBar bar)
  {
    CommandBar commandBar = null;
    //todo: сделать так, чтобы второй раз создать нельзя было
    try
    {
      commandBar = Globals.ThisAddIn.Application.CommandBars[bar.Id];
    }
    catch ( ArgumentException ) { }

    if ( commandBar == null )
      commandBar = Globals.ThisAddIn.Application.CommandBars.Add(bar.Id, ( object )MsoBarPosition.msoBarTop, _null, true);

    commandBar.Visible = true;
  }

  private object _null = System.Reflection.Missing.Value;

  #endregion
}


* This source code was highlighted with Source Code Highlighter.

С кнопками на тулбарах аналогично. Гляньте код на досуге.

Пришлось, к слову, создавать фабрику самих кнопкок и тулбаров (см. IWordUIElementFactory). Дело в том, что модули CAB работают с интерфейсами IWordCommandBar и IWordButton. Конкретные типы этих интерфейсов находятся в сборке VSTO Word-AddIn и завязаны на Microsoft.Office.Core. Поэтому, чтобы в модулях (где отсутствует ссылка на Office) была возможность получать инстансы указанных элементов, создается фабрика. Она регистрируется в главном WorkItem.

Регистрация фабрик и места расширения под тулбары:
Services.AddNew<WordUIElementFactory, IWordUIElementFactory>();
IUIElementAdapterFactoryCatalog factoryService = base.Services.Get<IUIElementAdapterFactoryCatalog>();
factoryService.RegisterFactory(new CommandBarUIAdapterFactory());

UIExtensionSites.RegisterSite(UIExtensionSiteNames.WordBarsSite, new BarCollection());


* This source code was highlighted with Source Code Highlighter.


Если вы запутались, извините:) Я сам понимаю, что тут без поллитры не разберешься. (Если подебажиться, то многое становится понятным.)

Модуль CAB

Это обычная сборка. В ней должен быть класс ModuleInit. В этом классе есть ссылка на главный прецедент AddInWorkitem и, как следствие, есть доступ ко всему добру, про которое я писал выше.

Задача модуля CAB такова — подгрузиться к главному преценденту, вставить тулбар, вставить кнопку на него, описать обработчик кнопки:

Voila:
UIExtensionSite site = _rootWorkItem.UIExtensionSites[UIExtensionSiteNames.WordBarsSite];
IWordCommandBar mainBar = site.Add<IWordCommandBar>(_factory.CreateBar("AddInToolbar"));

IWordButton btn = _factory.CreateButton(mainBar, CommandNames.OpenForm, ToolStripItemDisplayStyle.ImageAndText, "Открыть окно",
  "Открыть форму просмотра Custom Control", Resources.OpenForm, false);

mainBar.AddButton(btn);
btn.Click += new EventHandler<WordButtonClickArgs>(ButtonClick);


* This source code was highlighted with Source Code Highlighter.


Обработчик кнопки будет создавать пользовательское окно с кнопкой «получить текст» и текстовым полем, после чего показывать его (применив специальный модификатор показа WindowSmartPartInfo) в ранее упомянутой рабочей зоне (которую, как мы помним, я зарегистрировал в прецеденте под ключем WorkspaceNames.MainWorkspace):
private void ButtonClick(object sender, WordButtonClickArgs e)
{
  object smartPart = _rootWorkItem.SmartParts.AddNew<SampleSmartPart>();
  WindowSmartPartInfo info = new WindowSmartPartInfo();
  info.FormStartPosition = FormStartPosition.CenterScreen;
  info.MaximizeBox = false;
  info.MinimizeBox = false;
  info.Resizable = false;
  info.Title = "Custom Control";
  info.ShowInTaskbar = false;
  _rootWorkItem.Workspaces[WorkspaceNames.MainWorkspace].Show(smartPart, info);
}


* This source code was highlighted with Source Code Highlighter.


Подгрузка модуля

Здесь я сделал финт коленом. Обычно модули подгружаются в каркас через специальный декларативный формат подгрузки — так называемый ProfileCatalog. Обычно, это хороший способ подключить все, что нужно. Но, учитывая суровые советские реалии, имеется ненулевая вероятность того, что программисту потребуется недекларативная логика подключения модуля. Для этого мы будем переопределять специальный сервис перечисления подгружаемых модулей — IModuleEnumerator. Я сделал его очень простым — он глядит в папку исполняемой сборки и ищет в ней модуль под названием CustomModule.dll. Находит, и подгружает. Ну, или не подгружает, если не находит:
public class CustomModuleEnumerator : IModuleEnumerator
{
  #region Constants
  private const string ModuleName = "CustomModule.dll";
  #endregion

  #region IModuleEnumerator Members

  public IModuleInfo[] EnumerateModules()
  {
    List<IModuleInfo> result = new List<IModuleInfo>();

    string path = GetModulePath(ModuleName);
    if ( File.Exists(path) )
      result.Add(new ModuleInfo(ModuleName));
    return result.ToArray();
  }

  #endregion

  #region Private Methods
  private string GetModulePath(string assemblyFile)
  {
    if ( !Path.IsPathRooted(assemblyFile) )
      assemblyFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, assemblyFile);

    return assemblyFile;
  }
  #endregion
}


* This source code was highlighted with Source Code Highlighter.


Сейчас эта служба очень простая, но при желании, разумеется, в нее можно нагородить хоть черта лысого. Например, подгружать модули из базы данных:)

Перегрузить эту службу нужно в классе приложения CAB:
protected override void AddServices()
{
  base.AddServices();

  RootWorkItem.Services.Remove<IModuleEnumerator>();
  RootWorkItem.Services.AddOnDemand<CustomModuleEnumerator, IModuleEnumerator>();
}


* This source code was highlighted with Source Code Highlighter.


Доктор, я устал. Что получилось?

Получилось то, что задумывалось в изначальном псевдопримере:

В тулбарах Microsoft находится наш тулбар, на нем висит кнопка с иконкой, по событию нажатия кнопки вылезает пользовательский элемент управления, в текстовое поле которого с помощью кнопки можно загнать выделенный в документе текст:) Тривиально, но обратите внимание, как ничтожна связность между объектами! Хочется подметить, что у каркаса, при должном обращении, идеальная code maintainability.

Внимание! Подводные камни!


Security
Когда скомпилируете и запустите пример, ничего не запустится. Более того, вывалится сообщение, которое скажет вам — дескать, не хватает прав на запуск стороннего модуля (в нашем случае, CustomModule.dll). Проблема в том, что по умолчанию приложение VSTO (в режиме разработки!) дает Full Trust права только на исполняемую сборку и на все сборки, от которых она зависит — т.е. на WordCAB.dll. Для того, чтобы разрешить использование кода сторонних библиотек, выполните следующее действие:
C:\Windows\Microsoft.NET\Framework\v2.0.50727>caspol -u -ag All_Code -url "D:\Projects\WordCAB\bin\Debug\*" FullTrust
Microsoft (R) .NET Framework CasPol 2.0.50727.3053
Copyright (c) Microsoft Corporation. All rights reserved.

The operation you are performing will alter security policy.
Are you sure you want to perform this operation? (yes/no)
yes
Added union code group with "-url" membership condition to the User level.
Success


Вместо "D:\Projects\WordCAB\bin\Debug\*" нужно указать папку, из которой производится запуск Add-in. Не забудьте про звездочку.

Add-in перестал загружаться!
Иногда, когда в модуле возникает необработанный Exception, Word блокирует исполнение этого модуля при следующей загрузке. Зайдите Help->About->Disabled Items и посмотрите, нет ли в списке вашего расширения. Если есть, уберите его оттуда. При следующем Run-Debug он появится.

Как убрать
Удаление расширения не такое тривиальное. Вытащите кнопку COM-AddIns на тулбар Microsoft Word. Для этого надо зайти в Tools->Customize->Tools->(Drag'n'Drop)COM-AddIns. Кликните на нее и снимите галку с вашего расширения. Он выгрузится. Чтобы заново подгружать, наоборот, выставите галку обратно. Другой способ — зайти в панель управления и удалить из программ.

А что с Word 2007?
Add-in замечательно подгружается в Ribbon на последнюю вкладку.

Там еще кучка мелких подводных камней. Но я тут и так уже настолько много написал, что не уверен, что кто-нибудь до конца дочитает:)

Где взять код

github.com/head-thrash/VSTO-CAB

Выводы

В общем, можно взять VSTO, Microsoft Office 2003, .NET Framework 2.0 и сделать решение. Базовое решение, на которое можно нарастить функционал, я привел в этой статье. Кто хочет — пользуйтесь на здоровье. Буду рад ответить на вопросы и подправить неточности, если здесь такие будут. Спасибо огромное за внимание!
Tags:
Hubs:
+22
Comments 13
Comments Comments 13

Articles