VSTO and CAB: .NET Application Integration in Microsoft Word

    VSTO stands for Visual Studio Tools for Office. These tools make it quite easy to cross already with a hedgehog - write .NET applications executed by the CLR in the Microsoft Office environment. In particular, programmers have the ability to create plug-ins (plug-ins) and “customized” document templates for almost the entire main Microsoft Office product family.

    The article provides the infrastructure of the Windows Forms project, in which Microsoft Word is perceived by the application as a shell. The article reveals several interesting points of using the Composite UI Application Block, in particular, the connection of the Word domain model infrastructure in the framework extension services, as well as some facts and development features using VSTO tools.

    Task


    Suppose we have several dozen people who write documents and constantly work with their editors. Sharepoint and other portals for some reason are not suitable, therefore, the implementation of your own business logic is required. Let's complicate the task - people work in an area very far from computer topics and know only the Microsoft Office suite well. Even more complicated - the office is poor, Microsoft Office has the majority of the people of the 2003 version. Here and there, the big bosses and the Main Boss are 2007th. The fleet of machines is diverse - from the 2000th Windows to Windows Vista.

    Decision

    One solution is a plugin for Microsoft Office. It can be written on COM, through the bare Office Interop, or through the same interop, but cunningly wrapped in VSTO tools. We use these funds. The user will start Word, the VSTO runtime will pick up .NET assemblies and somehow supplement Microsoft Word controls, allowing the user to execute the business logic necessary for the task.

    And why all this?

    (This is someone who can’t wait to find out how it will end). At the output, I will lay out a Visual Studio solution, which will allow half-kick to create my own application that connects to Microsoft Word. In the process, I will explain almost everything that is used in this solution.

    What is required for work

    1. Microsoft Word 2003 SP3
    2. VSTO 2005 SE
    3. VSTO 2005 SE Runtime
    4. Cab
    5. Visual Studio 2005 or 2008

    Why CAB?

    In order to determine the explicit separation of the functionality associated with Microsoft Word from simple Windows Forms logic. Simply put, our plugin will be a CAB module, and if desired, we can easily connect this logic to other CAB applications. Simply put, using CAB minimizes the amount of glue code.

    There are two more reasons why I included CAB in the solution. The first is that I already gave a description of this framework here , and now I want to give a full-fledged working example. The second reason is more commonplace - I like CAB, it makes the world and things in it easier :)

    Read more about VSTO

    Generally speaking, VSTO is not only able to create plugins for Word. This is a powerful system, now it is already in the third version. VSTO is the gateway to the developer in the world of automation and programming Office-based solutions. In a nutshell - the .NET developer gains access to the domain model of the Office application and does whatever he wants with it.

    All these solutions can be divided into two types:
    1. Document Level Customization
    2. Application Level Customization

    These are independent levels. The first creates document templates for Microsoft Office applications. You can take the Excel template, diversify it with different user controls (buttons, grids), and add any logic - for example, so that when the button is clicked, the data collected by the user is sent through a web service to some server and processed there. At this level, the developer can use the Action Pane (in the 2003 version), where you can connect your controls, plus a lot more in the 2007 version.

    Application Level Customization means that an extension module (the so-called add-in) is connected to the Microsoft Office application environment. Actually, I initially started talking about him. This extension module is the locomotive of the business application and allows you to connect all the features of the .NET platform in Microsoft Office. I will show exactly the second level solution.

    If anyone is interested in the capabilities of VSTO - a lot of them are in msdn .

    Pseudo task

    Let's create a small example illustrating all of the above. Let there be a requirement according to which a user can select text from a Microsoft Word document and get this selected text in the window of a user element (I know a stupid example, but I don’t want to do more complicated, but it’s hard to come up with a less complex one).

    In details. The user launches Microsoft Word. After launch, among the toolbars, the user will have access to the TP - extension toolbar (i.e., our extension). There is a button on it, by clicking on which the user is shown a window with the "Get Text" button and a multiline textbox control. The user clicks on the “Get Text” button, the text selected in the document is copied to the textbox. Everyone is happy.

    It is clear that an example is of little use for life. But the infrastructure being created will make it quite easy to build “muscles” on it, if, of course, someone needs it.

    Creating solution

    After installing VSTO in the studio, when creating a new solution, Office projects will become available:



    Be careful with the name - it will be the root namespace name at the same time and it will only be possible to change it by handles through unloading the project.

    The output is a solution with two projects:


    WordCAB is, in fact, add-in. The second - no less important, is the deployment project for the extension module. Why is it important?

    The fact is that installing the expansion module on user workstations is a very time-consuming undertaking. It is not just about just copying the libraries to the right folder. For successful functioning of the module, you need to register a bunch of keys in the registry. If you dig deeper, it turns out that the expansion module is connected to Microsoft Office as a COM library, with all that it implies. Who cares, all the necessary registry keys can be viewed in the deployment project (Right-click, View-> Registry).

    In the ThisAddIn class, there is an access point to the module:
    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

      ///
      /// Required method for Designer support - do not modify
      /// the contents of this method with the code editor.
      ///

      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.


    In ThisAddIn_Startupwe will write the code. To be more precise, we will launch the CAB application.

    What is a cab application?

    This is such a class. It has a launch point - a method Run(), and some framework infrastructure - in particular, there is access to the main use-case “Application” (that is, to the main, root WorkItem) and the ability to override the system services that are added to applications by default. This class is already implemented in its basic form, and the programmer needs to parameterize it - indicate the type of root use case and the type of the main form:

    internal sealed class AddInApplication : WindowsFormsApplication
    {
      protected override void Start()
      {
      }
    }

    * This source code was highlighted with Source Code Highlighter.


    As the type of the main form (shell form), object is specified. Why? Because the application runs in Microsoft Word, so there is no need to create a main form. The Microsoft Word form is the main form, though without its traditional features.

    As the play progressed, it occurred to me to clarify a little the application model for the extension module in Microsoft Word.

    Word Add-in Application Model
    So, the user works with documents. Moreover, in a unit of time, he can work with only one document. Therefore, the extension module has a clear context - the current open document. (By the way, in the Microsoft Word VSTO model this document is designated as Globals.ThisAddIn.Application.ActiveDocument). From here (I made) the conclusion (or simplification) - it makes sense to show custom business elements in modal mode, because otherwise the context is broken. Controls that are open to a single document should only exist while this document is active.

    Example - a user opened a document and opened its card (not in modal mode). I switched to another document, but the card remained for the previous one - a violation of the context and integrity of perception. Of course, such behavior can be controlled (and even necessary, in some cases), but the modality of the controls and their tight binding to the current active document makes life easier.

    Modal workspace

    User controls in the CAB are displayed in special so-called workspaces. According to the behavior described above, we need to show the controls in modal mode. Since the native CAB WindowWorkspace turned out to be a little oaky and looks ugly, I wrote my own simple one - I won’t give the code, because there are a couple of screens. You can enjoy creativity by downloading the solution with the code (a link to the archive is provided at the end of the article).

    The registration of the modal work area takes place in the InitializeServices()main use case method - AddInWorkitem:

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

    * This source code was highlighted with Source Code Highlighter.


    Total: a WorkspaceNames.MainWorkspacemodal work zone was registered under an identifier key . You can now access it from anywhere, where there is a link to the main use case: requesting an interface IWorkspacefrom the collection Workspacesusing the key specified above. Everything is just like an orange! (c) x / f “Terminator 2”

    Microsoft Word Control Factory

    It will be about the Word toolbar and buttons on it. In VSTO, these are wrappers over COM objects - CommandBarand CommandBarButtonfrom the namespace Microsoft.Office.Core. Terribly buggy things, to be honest, especially their animation. It was possible to understand all the subtleties and details only after shock trolling on the VSTO forums.

    What is the idea - the idea is to save the programmer and business module developers from the need to work with COM wrappers. To do this, we (that is, me) integrate the Misrosoft Word model into the mechanism of the so-called extension points (UiExtensionSite) of the CAB framework.

    The mechanism of expansion sites consists of functional connectives-additions. Simply put, the code needs to visualize the relationships of subordinate elements. In our case, these relationships are:
    1. The toolbar is inserted into the array of toolbars of Microsoft Word
    2. A button is inserted into the collection of buttons on the toolbar
    3. Nothing is inserted into the button (we do not consider the case of combo boxes, although they are present), therefore this is a terminal object

    Total, we got three entities subordinate to each other:
    1. IWordCommandBarContainer - Microsoft Word toolbar container
    2. IWordCommandBar - a container of buttons on the toolbar
    3. IWordButton - button


    By the way, in addition to defining the control elements Word specific types of interfaces IWordCommandBarand IWordButtonare adapters to those mentioned above CommandBarand CommandBarButton, respectively.

    In order for the framework to understand what, where and, most importantly, how to insert it, it needs to register the adapter factory for user controls. In our case, you can insert toolbars (in the collection of toolbars) and buttons (in the collection of buttons on the toolbar). Therefore, it is necessary to register the adapter factory for IWordCommandBarContainerand forIWordCommandBar. Then, when the user receives the extension location, the framework will look for instances of the classes for adding subordinate elements for this extension location, and then use them for their intended purpose - add elements. Well, in order not to bother, these instances are generated by the factory:

    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.


    And here is the implementation of the toolbar collection (with the ability to add a new one!)
    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.

    With buttons on toolbars similarly. Take a look at the code at your leisure.

    It was necessary, by the way, to create a factory of buttons themselves and toolbars (see IWordUIElementFactory). The fact is that CAB modules work with IWordCommandBarand IWordButton. The specific types of these interfaces are in the VSTO Word-AddIn assembly and are tied to Microsoft.Office.Core. Therefore, in order for modules (where there is no link to Office) to receive instances of the specified elements, a factory is created. She registers in the main WorkItem.

    Registration of factories and expansion sites for toolbars:
    Services.AddNew();
    IUIElementAdapterFactoryCatalog factoryService = base.Services.Get();
    factoryService.RegisterFactory(new CommandBarUIAdapterFactory());

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

    * This source code was highlighted with Source Code Highlighter.


    If you are confused, sorry :) I myself understand that you can’t figure it out without a half liter. (If you take a seat, then much becomes clear.)

    CAB module

    This is a regular build. It should have a class ModuleInit. In this class, there is a reference to the main precedent AddInWorkitemand, as a result, there is access to all the good that I wrote about above.

    The task of the CAB module is to load onto the main precedent, insert a toolbar, insert a button on it, and describe a button handler:

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

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

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

    * This source code was highlighted with Source Code Highlighter.


    The button handler will create a custom window with the “get text” button and a text field, and then show it (using a special display modifier WindowSmartPartInfo) in the previously mentioned work area (which, as we recall, I registered in the use-case under the key WorkspaceNames.MainWorkspace):
    private void ButtonClick(object sender, WordButtonClickArgs e)
    {
      object smartPart = _rootWorkItem.SmartParts.AddNew();
      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.


    Module loading

    Here I made a feint knee. Typically, modules are loaded into the framework through a special declarative loading format - the so-called ProfileCatalog. Usually, this is a good way to connect everything you need. But, given the harsh Soviet realities, there is a non-zero probability that the programmer will need the non-declarative logic of connecting the module. To do this, we will redefine the special service for listing the loadable modules - IModuleEnumerator. I made it very simple - it looks in the executable assembly folder and looks for a module called CustomModule.dll in it. Finds and loads. Well, or does not load if it does not find:
    public class CustomModuleEnumerator : IModuleEnumerator
    {
      #region Constants
      private const string ModuleName = "CustomModule.dll";
      #endregion

      #region IModuleEnumerator Members

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

        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.


    Now this service is very simple, but if you wish, of course, you can even equip it with a bald one. For example, to load modules from the database :)

    You need to reload this service in the CAB application class:
    protected override void AddServices()
    {
      base.AddServices();

      RootWorkItem.Services.Remove();
      RootWorkItem.Services.AddOnDemand();
    }

    * This source code was highlighted with Source Code Highlighter.


    Doctor, I'm tired. What happened?

    It turned out what was intended in the original pseudo-

    example: Microsoft toolbar contains our toolbar, a button with an icon hangs on it, a user control pops up on the button press event, in the text field of which you can use the button to select the text selected in the document :) Trivial, but notice how insignificant the connection between the objects! I want to notice that the frame, with proper handling, has the perfect code maintainability.

    Attention! Underwater rocks!


    Security
    When you compile and run the example, nothing will start. Moreover, a message will fall out that tells you to say that there are not enough rights to launch a third-party module (in our case, CustomModule.dll). The problem is that by default, the VSTO application (in development mode!) Gives Full Trust rights only to the executable assembly and to all assemblies on which it depends - i.e. to WordCAB.dll. In order to allow the use of third-party library code, perform the following action: Instead, you need to specify the folder from which the Add-in is launched. Do not forget about the asterisk.
    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 has stopped loading!
    Sometimes, when an unhandled Exception occurs in a module, Word blocks the execution of that module the next time it loads. Go to Help-> About-> Disabled Items and see if your extension is on the list. If there is, remove it from there. At the next Run-Debug it will appear.

    How to remove
    Removing an extension is not so trivial. Pull out the COM-AddIns button on the Microsoft Word toolbar. To do this, go to Tools-> Customize-> Tools -> (Drag'n'Drop) COM-AddIns. Click on it and uncheck your extension. He will unload. To reload, on the contrary, put the daw back. Another way is to go to the control panel and remove from programs.

    What about Word 2007?
    The add-in is loaded wonderfully in Ribbon on the last tab.

    There are still a bunch of small pitfalls. But here I have already written so much that I’m not sure that someone will read to the end :)

    Where to get the code

    github.com/head-thrash/VSTO-CAB

    conclusions

    In general, you can take VSTO, Microsoft Office 2003, .NET Framework 2.0 and make a decision. The basic solution, on which you can increase the functionality, I gave in this article. Who wants to - use your health. I will be glad to answer questions and correct inaccuracies, if there are any. Thank you so much for your attention!

    Also popular now: