Prelude
小T今年刚从大学毕业。由于上学的时候不好好学习,毕业时候找工作非常困难。好在平时上C语言课帮女生写作业还积攒了一点经验值,好歹最后还是找到了一份写程序的工作。上班第一天,他接到一个任务,给一个word 2003的插件添加一个command bar,上面再添加一个command bar button。倒霉的小T,悲惨的生活从此拉开了序幕。
Section 1
每一个新手,都应该感谢google。没有他,真不知道该怎么把事情搞定。小T也不例外。在一番寻找之后,不花多少力气。颇有基础的小T已经能够创建一个Command Bar了。代码虽然是Ctrl+C, Ctrl+V过来的,不过能用就行。
commandBar.Visible = true;
搞定了Command Bar,胜利还会远吗?我们小T还蛮仔细的,特意多启动了几次Word。确保每次它都在那,而且只有一个。小T暗自得意,并顺带对M$公司心生敬意。真是一个伟大的公司啊。设计的软件可扩展性这么好,简单易学。
继续放狗。不多时,Command Bar Button也搞定了。
CommandBarButton commandBarButton = (CommandBarButton)commandBarControl;
commandBarButton.Caption = "TestCommandBar";
commandBarButton.Style = MsoButtonStyle.msoButtonCaption;
commandBarButton.Visible = true;
恩,那Type.Missing是什么东西?哎,早知道用VBA写,可能会容易一些。接下来就是响应Click事件啦。
我KAO!居然一次成功。可以用了,交差吧。小T走到QA面前,把程序给她装上。开心地去吃午饭了,准备庆祝一下这难得的开门红。
Section 2
午饭归来。QA就报了一个bug。这可能是小T从业以来处理的第一个Bug吧。恭喜,你的与虫斗其乐无穷的生活正式宣告开始了。Bug还蛮诡异的,就是你不停地点。到一定次数,MessageBox就不再出来了。小T心想,我KAO,这是什么鸟问题啊?好吧,我来试试。我点一下。我点两下。我点三下。。。。喂?玩我那?没问题啊。于是小T把Jira上的Bug Report标记为Can not reproduce。心中暗暗不爽,什么鸟QA嘛。
不多久,QA过来告诉他,重现啦,又重现啦。小T望着reopen的Bug Report,觉得很不是滋味。好吧,我多试试。点了N下。耶?真的诶,这是咋回事?放狗吧,搜了半天,没发现啥有用的。在网络和debugger之间耗费了接近一个下午之后,终于找到了一个同样悲惨的人在一篇blog的小字中提到了这个问题的原因。正确的写法麻烦多了:
private CommandBarButton commandBarButton;
private void InitializeCommandBar(Application application)
{
CommandBar commandBar = application.CommandBars.Add("TestCommandBar", MsoBarPosition.msoBarTop, false, true);
commandBar.Visible = true;
CommandBarControl commandBarControl = commandBar.Controls.Add(MsoControlType.msoControlButton, Type.Missing, Type.Missing, Type.Missing, true);
commandBarButton = (CommandBarButton)commandBarControl;
commandBarButton.Caption = "TestCommandBar";
commandBarButton.Style = MsoButtonStyle.msoButtonCaption;
commandBarButton.Visible = true;
handler = new _CommandBarButtonEvents_ClickEventHandler(commandBarButton_Click);
commandBarButton.Click += handler;
}
private void commandBarButton_Click(CommandBarButton Ctrl, ref bool CancelDefault)
{
MessageBox.Show("Hello");
GC.Collect();
}
好,这下就算强制GC也不会出问题了。写完这些代码,小T开始抱怨起来了。这么多人骂M$,那不是没有道理的啊。。。
Section 3
第二天早上。QA又报Bug了。创建一个新文档,在新创建的文档里,按钮不好使了。小T第一反应是怎么可能,代码都是一样的,我又没判断当前是哪篇文档?难道他们是两个command bar?不过铁的事实摆在那里,还真有问题。小T的第一个怀疑就是,两篇word文档是两个word进程。然后有传说中的并发问题?怀疑很快被否定了,Word在大部分情况下只有一个进程(嘿嘿,大部分情况。。。)。那会是什么问题呢?哎,一早上的好心情就给它糟蹋了。放狗吧。这下就更加没有头绪了。你说我怎么把这个现象用关键字描述出来呢?找都不好找。等到快下班,功夫不负有心人。在一次无意的尝试之后,发现了问题的解决方案,那就是“一行代码”:
我KAO,居然这么简单……但是文档就是不写(MSDN唱到:就不告诉你,就不告诉你,就不告诉你~)
Section 4
事情到这里还没完。一天BA告诉小T,需求有变化。客户说这按钮能不能加到我平常用的Command Bar上啊。那么多Command Bar我不喜欢。好吧,那就给它加到Standard Command Bar上吧。改一行代码就行,嘿嘿,知道什么叫Well-Design了吧:
CommandBar commandBar = application.CommandBars["Standard"];
嗯,挺好的。工作正常。咦?等我再试一下。shit!怎么有两个Test Command Bar Button出来?我KAO,又多了一个。。。无语。之前的代码咋就没问题呢?那就在添加之前判断一下吧,如果之前的button存在,就不添加了。
{
if (ctrl.Tag.Equals("TestCommandBar"))
{
return;
}
}
小T觉得很不爽。添加按钮的时候我不是说了是Temporary嘛:
呵呵,谁让你相信MSDN的啊?真是天真的傻孩子。事实证明,Temporary的行为极其诡异。
Section 5
好日子似乎来临了。插件装在客户的机器上,一直良好。版本2.0就快要发布了。这个时候问题出现了。QA发现,V1.0卸载之后,按钮还在那里。嗯,这个问题我知道,小T心想。就是那个Temporary的问题嘛。看来不光是不能添加两遍,还得考虑怎么删掉它。那不如每次程序退出的时候就删了吧,省得夜长梦多。
events.Quit += delegate {
CommandBar commandBar = application.CommandBars["Standard"];
commandBar.Reset();
};
但是。。。没有效果。有些时候,是没有解释的。看来只能写一个Uninstaller来卸载啦。
{
public static void Main(string[] args)
{
Application application = new Application();
application.Visible = true;
application.CommandBars["Standard"].Reset();
}
}
小T禁不住想问,为什么基本同样的代码在不同的地方执行就有不同的效果呢?
画外音:嗯,This is a good question~~这正式Office插件开发的奇妙之处啊。
这个时候,我们的小T已经不再是菜鸟了。但是你以为你已经知道了一切了吗?嘿嘿。。。
Section 6
难以伺候的客户又抱怨了。这个按钮为什么不能自定义啊。哦,我们伟大的word 2003,它有强大的用户自定义功能。小T写的按钮必须在Standard Command Bar上,拖到别的地方去下次又会创建出一个新的出来。公司的UD人员看到了,en,这可用性太差啦。不行,得给我改。好吧,小T,心想,我真是命苦啊。改进后的代码,不再强制要求在Standard Command Bar下了。
if (foundControl != null)
{
return;
}
嘿嘿,循环都省了。
Section 7
小T在上一个项目干得不错。于是公司觉得小T干这个比较在行,所以第二个项目也是一个word 2003的插件。小T这次总结上次的经验,一开始就关注这卸载的问题。在无数个皓首穷经,不眠不休的夜晚之后,终于让小T找到了更可靠的使用word api的办法。那就是不用word api。word 2003的command bar的改动其实是存储在template和document上的。所以小T把之前添加一大堆代码浓缩成了一句话:
application.AddIns.Add(@"c:\my_template.dot", ref install);
这类型为ref object的函数参数实在是。。。(无语,[ref object]就更加牛x了。。。)。然后再结合FindControl,监听事件。齐活,小T终于感觉自己摸到了门道。
Section 8
新项目的需求中包括让不同文档窗口上的按钮状态独立。就像你按下B,当前输入的文字就粗体了那样。这下可把小T难住了。不过,不死的小T还是最终活下来了。其实也没几行代码,关键就是你知道不知道。word application有一个隐藏属性(其实没隐藏,只是文档上没说,官方说法是It is for internal use。。。)CustomizationContext。所以:
{
application.CustomizationContext = document;
InitializeCommandBar(application);
}
小T这次真的有些得意了。这我都能搞定,太天才了。拥有多年开发经验的PM走过来,三击了一下word的快捷方式。瞬间启动了三个word窗口。僵死了半分钟之后,发现两个窗口上有command bar,一个没有。出错日志表明,word api抛出了ComException。。。小T,顿时觉得有一些无奈,word出错,我能怎么办?把CustomizationContext那行去掉,一切正常。。。
Conclusion
Command Bar的王道是用.dot加CustomizationContext。但是要认识到这一点,不写烂两三个项目是做不到的。小T的原型也就是本人,在写两个Outlook 2007插件,一个Outlook 2003插件,一个Excel 插件,N个PowerPoint插件,一个Word插件,然后再来写一个Word插件的时候才认识到这一点。。。也许只是我太笨了。
以上只是故事的很少一部分。关于command bar的故事还有很多,比如怎么样取消built-in command bar button的行为,OnAction有什么用,Command bar与Accessibility api的关系,以及著名的Outlook+Word+Command Bar问题。限于个人体力值有限,就不继续展开讲了。每个故事背后,都是无数人的心酸,血泪与青春。M$的东西一如既往地易学难精,而且没有道理。关键是最终你不能获得什么真本事,只能获得一堆“知识”。这些“知识”只对Office开发有用,如果不做office开发根本没有什么价值。在这里,奉劝大家,如果不能换一个项目的话,那就提早离职吧。不要把时间浪费在Office这个愚蠢的平台上了。如果你还真把它当做一个平台的话。