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|...)等,这里也是很重要的一个节点,在后面被引用。

  
    
    Excel
  

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


  
  
  
    
      
        
        
          
        
      
    
  

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(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


  
  
  

先看了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模板研究)