美文网首页
Office VSTO AddIn模板研究

Office VSTO AddIn模板研究

作者: 赵海洋 | 来源:发表于2022-05-06 15:50 被阅读0次

    因工作需要研究了下Visual Studio里创建Office外接程序的方式,对其自动生成的C#模板工程有点兴趣。给开发者提供了最简单直接的方式实现功能,并在背后隐藏了很多细节,比Com版本的Office 加载项方便了很多。

    当前环境

    • Visual Studio 2022: .net 桌面开发、Visual Studio Tools for Office (VSTO)
    • Windows 11
    • Office 2016

    创建项目

    1. 使用模板新建项目
    image.png

    这里以Excel模块为例,项目名称取名为:MyExcelAddIn(这个名称后续在工程里比较重要,取名请慎重)。此时新建好的工程里只有以下文件:

    • MyExcelAddIn.csproj
    • Properties
    • ThisAddIn.Designer.cs
    • ThisAddIn.Designer.xml
    • ThisAddIn.cs

    后续分别解析这些文件的功能。在这里就可以直接F5生成工程,并启动Excel。Excel的加载项列表里显示已经有了MyExcelAddIn了。

    image.png

    并且这里F5后新生成了一个文件:MyExcelAddIn_TemporaryKey.pfx,用于对生成的AddIn进行签名(走的是[ClickOnce](ClickOnce 参考 - Visual Studio (Windows) | Microsoft Docs
    )逻辑),这里不再详述。

    1. 新增Office顶部的Ribbon控件选项卡

    一个Offcie插件可以有很多功能,我们这里以在顶部按钮区(Ribbon)添加一个选项卡为例。在解决方案资源管理器中右键MyExcelAddIn项目,然后选择新建项,再如下图中选中功“能区(可视化设计器)。”

    image.png

    在此步骤中直接用了默认文件名Ribbon1,这个名称后续在工程里比较常见,但不是很重要。在此步骤会新增以下几个文件:
    Ribbon1.Designer.cs、Ribbon1.cs、Ribbon1.resx

    image.png

    此时新增的代码依然非常简单,一个界面和一个cs(这里和常见的C#窗口类似)。此时一个最基本的VSTO加载项工程建立好了,这时F5直接运行会打开Excel,但新的选项卡并未出现,但在设置里有它(但奇怪的是,无论是界面设计器还是在代码中使用的名称都是默认的TabAddIns,但Excel中显示中文名称加载项,怀疑是Excel的中文语文包自动将单词翻译成了中文)。

    使用工具箱在界面上新增一个按钮,并且改为大按钮,并更改相关属性。


    image.png

    ,这时F5启动就有了选项卡。

    然后按同样的方法,再加了一个按钮,并更改其它属性,然后双击两个按钮,随便弹出个MessageBox,再F5运行,功能正常。

    image.png

    Visual Studio 背后隐藏的代码

    你以为我会继续讲开发什么功能?no,这种教程挺多的,自行去搜索。我这里只是由这个基本工程略微深入一下看看VS偷偷在背后搞了哪些事情,来,一步一步查看。

    F5为什么能直接运行Office并且加载扩展?

    MyExcelAddIn.csproj工程文件中(直接使用文本编辑器查看它吧),中间里面包含了节点OfficeApplication,这个节点定义了插件的类型(Word|Excel|...)等,这里也是很重要的一个节点,在后面被引用。

      <PropertyGroup>
        <!--
          OfficeApplication
            Add-in host application
        -->
        <OfficeApplication>Excel</OfficeApplication>
      </PropertyGroup>
    

    并在最底部,导入了Office相关的生成规则文件Microsoft.VisualStudio.Tools.Office.targets(规则文件的相关概念也是三天三夜都说不完的东西,应该没多少人自定义吧。。应该吧。。除非是某个大傻子。。),以及ProjectExtensions节点,
    当然你以为我懂节点里面的规则?并不。。反正记住这里的Host以及HostItem结点,在下面的代码中有所体现。

    <!-- Include additional build rules for an Office application add-in. -->
      <Import Project="$(VSToolsPath)\OfficeTools\Microsoft.VisualStudio.Tools.Office.targets" Condition="'$(VSToolsPath)' != ''" />
      <!-- This section defines VSTO properties that describe the host-changeable project properties. -->
      <ProjectExtensions>
        <VisualStudio>
          <FlavorProperties GUID="{BAA0C2D2-18E2-41B9-852F-F413020CAA33}">
            <ProjectProperties HostName="Excel" HostPackage="{29A7B9D7-A7F1-4328-8EF0-6B2D1A56B2C1}" OfficeVersion="15.0" VstxVersion="4.0" ApplicationType="Excel" Language="cs" TemplatesPath="" DebugInfoExeName="#Software\Microsoft\Office\16.0\Excel\InstallRoot\Path#excel.exe" DebugInfoCommandLine="/x" AddItemTemplatesGuid="{51063C3A-E220-4D12-8922-BDA915ACD783}" />
            <Host Name="Excel" GeneratedCodeNamespace="MyExcelAddIn" IconIndex="0">
              <HostItem Name="ThisAddIn" Code="ThisAddIn.cs" CanonicalName="AddIn" CanActivate="false" IconIndex="1" Blueprint="ThisAddIn.Designer.xml" GeneratedCode="ThisAddIn.Designer.cs" />
            </Host>
          </FlavorProperties>
        </VisualStudio>
      </ProjectExtensions>
    

    ThisAddIn

    整个模块工程我们能直观看到的代码只有这ThisAddIn和Ribbon1 两个cs文件,而里面的代码都非常简单,并且除了添加按钮处理事件,其它的完全都不用去修改。

    但我们要看一看生成的文件里面包含什么内容,程序的主要入口是ThisAddIn,它继承于Microsoft.Office.Tools.AddInBase 主要实现代码在自动生成的ThisAddIn.Designer.cs中(此文件不建议修改,但后面也没有被其它操作所更新, 所以理论上你也能改),构造函数如下:

     public ThisAddIn(global::Microsoft.Office.Tools.Excel.ApplicationFactory factory, global::System.IServiceProvider serviceProvider) : 
                    base(factory, serviceProvider, "AddIn", "ThisAddIn") {
                Globals.Factory = factory;
            }
    

    这里有两个参数,一个factory,一个serviceProvider。然后转调用基类构造函数。

    protected AddInBase(Factory factory, IServiceProvider serviceProvider, string primaryCookie, string identifier)
            {
                _inner = factory.CreateAddIn(null, null, primaryCookie, identifier, this, this);
                _extensionSite = _inner.DefaultExtension;
            }
    

    在这里primaryCookie就是传入的“AddIn”,而identifier就是传入的“ThisAddIn”,这里又使用入口处的 Excel.ApplicationFactory::CreateAddIn 来创建一个私有的addin类型对象_inner,有一些事件或系统对象的获取最终都指向它。

    在ThisAddIn的Initialize函数中设置了Application属性(this.Application = this.GetHostItem<Microsoft.Office.Interop.Excel.Application>(typeof(Microsoft.Office.Interop.Excel.Application),),以及设置了Globals.ThisAddIn为自己。

    Globals

    在ThisAddIn的构造函数中,还引入了Globals类,它提供了3个属性:

    • ThisAddIn
    • Factory:Microsoft.Office.Tools.Excel.ApplicationFactory
    • Ribbons 继承于Microsoft.Office.Tools.Ribbon.RibbonCollectionBase

    ThisAddIn有时会在开发者自己编写的代码中引用,而Factory基本只会在自动生成的代码中引用。

    而Ribbons返回当前VSTO中所包含的所有Ribbons对象。可以使用var r = Globals.Ribbons.GetRibbon(typeof(Ribbon1));Globals.Ribbons.Ribbon1 来引用Ribbon对象。

    回到ThisAddIn.cs

    模板中,提供给用户的默认只有两个函数ThisAddIn_Startup和ThisAddIn_Shutdown,并且在默认自动折叠的生成代码InternalStartup中绑定到this.Startup和this.Shutdown事件,这两个事件如同上面所说是基类的私有成员_inner的事件,在插件启动和关闭时会回调这两个函数。

    image.png

    ThisAddIn.Designer.xml

    <hostitem:hostItem hostitem:baseType="Microsoft.Office.Tools.AddInBase" hostitem:namespace="MyExcelAddIn" hostitem:className="ThisAddIn" hostitem:identifier="ThisAddIn" hostitem:primaryCookie="AddIn" hostitem:master="true" hostitem:factoryType="Microsoft.Office.Tools.Excel.ApplicationFactory" hostitem:startupIndex="0" xmlns:hostitem="http://schemas.microsoft.com/2004/VisualStudio/Tools/Applications/HostItem.xsd">
      <hostitem:hostObject hostitem:name="Application" hostitem:identifier="Application" hostitem:type="Microsoft.Office.Interop.Excel.Application" hostitem:cookie="Application" hostitem:modifier="Internal" />
      <hostitem:hostControl hostitem:name="CustomTaskPanes" hostitem:identifier="CustomTaskPanes" hostitem:type="Microsoft.Office.Tools.CustomTaskPaneCollection" hostitem:primaryCookie="CustomTaskPanes" hostitem:modifier="Internal" />
      <hostitem:hostControl hostitem:name="VstoSmartTags" hostitem:identifier="VstoSmartTags" hostitem:type="Microsoft.Office.Tools.SmartTagCollection" hostitem:primaryCookie="VstoSmartTags" hostitem:modifier="Internal" />
    </hostitem:hostItem>
    

    先看了ThisAddIn的代码,再来看这个文件是不是就很清晰。在模板工程创建时经过一些神秘的步骤,某些工具根据这个文件里的内部生成了ThisAddIn.Designer.cs代码。

    Ribbon1.cs

    接下来看看Ribbon1,它主要功能也是在自动生成的Ribbon1.Designer.cs文件中,里面的代码不多,主要是构造函数、控件成员变量、界面初使化函数。

      partial class Ribbon1 : Microsoft.Office.Tools.Ribbon.RibbonBase
        {
       public Ribbon1()
                : base(Globals.Factory.GetRibbonFactory())
            {
                InitializeComponent();
            }
    ...
    

    这个类是继承于RibbonBase,同AddInBase一样实际上是调用了Factory的CreateOfficeRibbon作为私有成员,并且暴露出一些函数。

    Ribbon1构造调用堆栈

    PS

    在测试过程中,我发现我创建的多个Excel AddIn工程,即使以不一样的tab label 和name,也会合并到同一个选项卡里。

    尝试玩活儿

    终上,按模板的结构Word和Excel的扩展得使用统一的一个,而我想创建出同时支持Excel和Word等的通用扩展。

    首先尝试直接更改代码将Excel的改成Word插件。

    1. 将工程文件里的Excel都换成Word(注意别将MyExcelAddIn也换掉了),#excel.exe这个换成 #winword.exe
    2. ThisAddIn.Designer.csThisAddIn.cs中Excel改为Word(全字和大小写匹配)。
    3. Ribbon1在界面中选中最外层界面,然后在属性中将RibbonType增加一个Microsoft.Word.Document类型,这样生成的代码中就是this.RibbonType = "Microsoft.Excel.Workbook, Microsoft.Word.Document";了。

    然后F5启动,哒哒哒,竟然成功了,在Word中也可以显示了。

    尝试直接支持多产品

    上面改过之后,启动Excel,结果Excel并未加载扩展,根据调试显示未能创建ThisAddIn入口,按上文Factory的类型不一样,所以应该是找不到对应的构造函数,于是继续玩活儿。

    使用git对比代码,将部分改名的代码合并回来。

    1. 工程文件将引用Microsoft.Office.Tools.Excel和Microsoft.Office.Interop.Excel加回来。OfficeApplication不能改。
    2. ThisAddIn.cs中同时引用Excel和Word的命名空间。
    3. ThisAddIn.Designer.cs这一步是最多的改动,值得单独弄一段。

    复制构造函数将factory类型改掉,Globals.Factory及_factory的类型都改为基类Microsoft.Office.Tools.Factory以实现兼容。

    然后生成工程(这时不能F5了,否则F5还是启动Word)。单独启动Excel程序,哒哒哒,也显示我们定义的Ribbon了。

    image.png

    但似乎Manifest不通用。后续待研究。。

    相关文章

      网友评论

          本文标题:Office VSTO AddIn模板研究

          本文链接:https://www.haomeiwen.com/subject/sgvtsrtx.html