探索 Word 2007 开发 II(一):引用 .NET Framework Class Library 在线文档

探索 Word 2007 开发 II(一):引用 .NET Framework Class Library 在线文档

 

Written by Allen Lee

 

再临之时

每当我在文章中提到.NET Framework的某个类、属性或者方法时,我会为它添加一个超链接,引用.NET Framework Class Library的在线文档。首先,我用浏览器打开MSDN Library的页面,接着,在左边的导航目录树中定位到对应的节点,然后,把该节点的超链接复制下来,最后回到Word 2007中插入这个超链接。然而,我已经对这个繁琐的过程感到厌倦了,是否有办法把.NET Framework Class Library的在线文档集成到Word 2007中,从而让这个过程变得更加简单呢?在思考这个问题的同时,我的脑海也萌生了把其它有用的链接服务集成到Word 2007的想法,继而催生了《探索 Word 2007 开发 II》系列文章的创作,以便和大家分享我在探索中的思考和心得。

 

需求 X 需求

下图是用Paint.NET弄出来的示意图:

Figure 1

我希望把MSDN Library上.NET Framework Class Library那部分导航目录树以侧边栏的方式集成到Word 2007里,这样,当我右击某个节点,点击Insert菜单项时,将在当前文档的光标处插入该节点对应的连接。

作为用户,我提出了上面这些需求,那么,作为开发人员,为了实现这项功能,我又该提出哪些"需求"呢?

第二课堂
Announcing the MTPS Content Service,Craig Andera
Consuming MSDN Web Services,Craig Andera

 

多重影分身术

多重影分身术是一种实体复制忍术,如果你看过《火影忍者》,应该能够理解它的意思。现在我要做的和这个差不多,为MSDN Library的.NET Framework Class Library导航目录树产生出多个实体分身,并把它们嵌入Word 2007里,不同的是,多重影分身术是由本体实施,而这里的实体复制是由身为第三方的我来实施。

本节的焦点是用MTPS返回的数据填充侧边栏里的TreeView控件。下面是这个插件的需求列表:

  • 一开始,TreeView里面只有一个顶节点——.NET Framework Class Library,且该节点是折叠的。
  • 使用延迟填充技术,每个可展开节点会且只会在第一次展开时访问MTPS获取数据,并填充子节点。

这两个需求看起来很简单,然而,一旦和MTPS 结合使用就比较复杂了,你需要区分MPTS所使用的两套标识系统,一套是用于标识节点的导航的,另一套是用于标识节点所指向的内容的。如果你还没接触过MTPS,那么我强烈建议你先阅读Craig Andera的两篇文章,否则你有可能无法理解后面的内容。这里我做一个简单的介绍,MTPS有两种节点:导航节点和内容节点,导航节点与MSDN Library/TechNet Library左侧的导航目录树中的节点一一对应;内容节点则是与该导航节点对应的用于显示在右侧的内容。这两种节点使用了同一种标识模型,每一个标识包含了三个部分:标识符、区域信息和版本信息。其中标识符又分五种类型:short ID、content alias、content GUID、content URL和asset ID。并非所有节点都同时具备这五种标识符,也并非所有情况下MTPS都返回/接受这五种标识符,于是,如果你需要在不同的标识符之间切换,你可能需要再次访问MTPS。有鉴于此,上述两个需求的复杂性主要体现在处理不同种类的节点以及用于标识这些节点的不同种类的标识符上。

下面,我们来看看插件的实现,首先,参考《探索 Word 2007 开发(一):我的博客》这篇文章里提到的方法在Visual Studio 2008 Beta 2中创建一个项目骨架,里面包含一个空白的Ribbon(取名ReflinksRibbon)和一个空白的User Control(取名TocView),并在TocView上放置一个TreeView控件:

Figure 2

我不打算用普通的TreeNode来填充这个TreeView,而是使用根据MTPS的节点模型创建的自定义节点类来填充,当然,这个自定义节点类继承自TreeNode类:

Code 1

TOC全称Table of Content,TocNode与MTPS的TOC导航节点模型向对应:

  • TocNode.Text:节点标签(Label),对应于toc:Title;
  • TocNode.Target:与该导航节点对应的内容节点的标识符,对应于toc:Target;
  • TocNode.Locale:与该导航节点对应的内容节点的区域信息,例如zh-CN,对应于toc:TargetLocale;
  • TocNode.Version:与该导航节点对应的内容节点的版本信息,例如VS.90,对应于toc:TargetVersion;
  • TocNode.SubTree:如果该导航节点包含子节点,则该属性为子节点树片断的标识符,对应于toc:SubTree。

我希望使用《TreeView 四技》这篇文章里提到的延迟填充技巧,并让节点自行负责子节点的填充,如果某个节点拥有子节点,它也必须负责通知TreeView对其做出适当的渲染。下面是通过MTPS获取当前节点的子节点(注意,MTPS仅返回下一级的子节点):

Code 2

我们可以通过检查SubTree是否为null知道当前节点有否子节点,然而,我们还是无从得知子节点的装载是否已经执行过。重复装载无疑导致不必要的网络访问,于是,我为TocNode添加了一个类型为bool的m_Loaded字段。这样,仅当SubTree不为null以及m_Loaded不为false时,我们才装载子节点。所有这些操作都是在用户点击节点前面那个+号时才执行的,但由于子节点还没填充,TreeView是不会为该节点渲染+ 号的,于是,我们需要为该节点添加一个"占位子节点",以便TreeView能够正确渲染。添加占位子节点的最佳时机是当我们给SubTree属性赋值时,所以我把SubTree属性修改如下:

Code 3

有了这些准备,我们就可以实现Load()方法来装载子节点了:

Code 4

这个方法将会在TreeView的BeforeExpand事件委托里调用:

Code 5

值得提醒的是,当子节点填充完毕后,别忘了删除之前加入的占位子节点,并把m_Loaded的值设为true。另外,这里使用了GetAttribute()辅助方法来获取XAttribute的值:

Code 6

这样,TreeView的填充就变成简单地添加一个根节点了,这将在TocView的Load事件委托里完成:

Code 7

好了,现在来看看效果:

Figure 3

噢,TreeView的位置有问题!这个问题可以通过设置TreeView的Anchor属性以及TocView的Dock属性解决。我们知道,控件的Anchor属性的默认值为Top和Left的组合,然而,我们并不希望TreeView的右边和下边留下这么多空白,这样就可以把它的值设为Top、Left、Right和Bottom的组合。或者你认为把TreeView的Dock属性的值设为Fill更省事,但我还是喜欢四周留有一些"余地",至于留空多少,你可以通过Margin属性指定。而TocView的Dock属性可以在添加侧边栏时设置:

Code 8

现在,重新编译并运行,看看效果如何:

Figure 4

嗯,这次就好多了。

 

插入类库超链接

MTPS是用来返回内容的,所以我们无法从返回的数据中得知节点所对应的超链接。那么,我们如何获取节点所对应的超链接呢?如果无法获得,那么我们之前所做的就白费了。Google一下"MSDN MPTS"关键字,发现Tim Ewald曾经在某期MSDN杂志上介绍过MSDN的超链接的设计思路,并从中得知如何推断出节点所对应的超链接。

第二课堂
Designing URLs for MSDN2,Tim Ewald

推断节点所对应的超链接需要我们提供该节点的short ID,然而,除了我们手动赋之予short ID的根节点外,其下任一节点的Target属性所包含的标识符都是asset ID,这就意味着我们需要做一次转换了:

Code 9

getContentResponse.contentId公共字段并非指代内容节点的ID,而是指代short ID,无论该节点是导航节点还是内容节点,这是历史遗留问题,如果你读了第二课堂栏推荐的文章,你会发现这点微小的混乱。

接着,我为TocNode提供一个TargetUrl属性,用于返回当前节点所指向的内容的超链接:

Code 10

值得注意的是,我在这里使用了m_TargetUrl对超链接进行缓存,以保证该方法对于每个节点仅产生一次网络访问。

接下来就是本回的主角了,我希望右击TreeView上的节点时弹出一个菜单,里面包含一个Insert菜单项,当我点击这个菜单项时,在当前文档的光标处插入这个节点对应的连接。下面是Insert菜单项的Click事件委托:

Code 11

最后,要使Insert菜单项生效,我们必须把菜单挂接到节点上,这可以通过处理TreeView的NodeMouseClick事件做到:

Code 12

 

故技重施

虽然插入类库连接的功能已经可用,然而TocView也遇到了"我的博客"侧边栏曾经遇到的问题:

  • 随Word启动,而不是由用户根据需要启动;
  • 当打开多个Word窗口时,仅显示在第一个打开的窗口里。

对于这两个问题,我将会使用《探索 Word 2007 开发(二):扩展 Ribbon》《探索 Word 2007 开发(三):管理侧栏》这两篇文章里提到的方法来处理。首先,创建一个Ribbon:

Figure 5

然后,就是移植MyBlogsPaneManager过来管理TocView了。我当然可以直接把MyBlogsPaneManager里面关于"我的博客"侧边栏的东西换成与TocView对应的,然而,我希望让MyBlogsPaneManager更通用,以便将来在遇到关于侧边栏管理问题可以直接使用。MyBlogsPaneManager的核心部分要数GetMyBlogsPane()方法了,要让它变得通用,必须把里面和"我的博客"侧边栏相关的东西泛化,下面是该方法的改版代码:

Code 13

需要说明的是,m_CustomTaskPanePool来自Globals.ThisAddIn.CustomTaskPanes,而后者里只存放当前插件添加进去的侧边栏,再加上同一个Word窗口里相同类型的侧边栏只会出现一次,于是,在上述代码里,我通过侧边栏的父窗口和所含控件的类型来进行判等是合理的。另外,由于添加侧边栏需要提供标题,而标题和控件的类型又是一一对应的,于是,我使用了一个Dictionary<Type, string>来存放这些对应信息。要注册该信息,可以使用下面这个方法:

Code 14

那么,谁应该注册该信息,以及在哪里注册该信息呢?我认为注册该信息的责任应该落在控件的头上,再者,该信息是与控件的类型而不是实例相关的,于是,我把注册该信息的代码放在控件的静态构造函数里:

Code 15

剩下的就是挂接相关的事件委托和处理状态的同步了,这些都和"我的博客"侧边栏的做法大同小异,这里就不重复了。

 

更多选择

还差点什么呢?语言!留意下面这幅截图:

Figure 6

Class、Members、Constructor、Methods以及节点所指向的内容均为英文,我希望可以选择用于显示的语言。于是,我为插件创建了一个选项窗口:

Figure 7

并在它的Load事件委托里处理当前语言下拉列表的初始化,并读取配置文件中的语言信息:

Code 16

我们确实可以避免在这里硬编码,然而,就目前来说,提供简体中文、繁体中文和英文已经足够了,除非你打算把它变成产品,并进军全球市场。当用户选好语言并点击OK按钮时,语言信息将会保存到配置文件里:

Code 17

Class Library Options窗口是通过侧边栏右下角新添加的Options按钮启动的,当语言信息改变了,TreeView也应该刷新根节点:

Code 18

需要说明的是,我不再指定Version的值,这将告诉MTPS获取最新的发布版本 (非预发行版,包括Beta版和CTP版),而Locale则与配置文件里的语言信息相对应,这意味着插入的超链接所指向的页面将与配置文件里德语言信息相对应。下面是运行效果的截图:

Figure 8

 

至此,Class Library侧边栏的开发要告一段落了,虽然此时我的脑海里又冒出了新的需求,或者你也对它有了新的想法。最后,给大家介绍两个别人写的MTPS应用:

第二课堂
Package This is a GUI tool written in C# for creating help files (.chm and .hxs) from the content obtained from the MSDN Library or the TechNet Library via the MSDN Content Service.
msdnman is a command-line viewer for documentation stored in the MSDN/TechNet Publishing System (MTPS).
posted @ 2007-10-20 15:50  Allen Lee  阅读(5383)  评论(12编辑  收藏  举报