Office VSTO AddIn模板研究

因工作需要研究了下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不通用。后续待研究。。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容