在《Office with .Net (二) ――― 使用.Net访问Office编程接口》一文中,已经介绍了使用Office Automation(Office自动化)技术,在.Net代码中通过Office PIA直接访问Office编程接口。比如,在那篇文章中,我们创建了一个C#编写的WinForms程序,在程序中直接启动Word,用代码操作Word自动完成一些工作,然后再用代码将Word关闭。
凡是涉及到使用Office Automation,即通过自定义的代码启动Office,操作Office编程接口完成一些工作(不管是在WinForms程序,或是ASP.NET程序中),都不可避免的会遇到一个问题,就是如何“彻底干净的”将代码启动的Office程序关闭掉。实际上,如果没有处理好这个问题,那么会造成应用程序所在计算机上,相关的Office进程始终无法关闭,而如果应用程序运行在一台服务器上,那么造成的后果也更加严重,甚至可能导致服务器资源耗尽而宕机。
一、服务器端场景
服务器端Office Automation就是指我们在一个位于服务器端运行的程序中,访问Office编程接口,启动Office程序,操纵Office完成某些自动化操作。比如,在一个ASP.NET程序,或者在一个Windows Service中,都是服务器端Office Automation场景。
服务器端Office Automation的第一准则就是:不要在服务器端进行Office Automcation操作!甚至在服务器上进行Office Automcation操作是不被微软所Support的!
没错,因为在服务器端进行Office Automcation操作是非常非常危险的行为,Office原本就不是被设计成以无人值守方式运行的,就是说,Office程序在设计的时候,总是默认假定有一个真正的用户坐在计算机前,用鼠标和键盘与Office程序进行交互。而如果我们用代码来操作Office,那么实际上已经打破了这个假定。打破这个假定可能带来哪些问题呢?下面列举了一些常见的问题:
(1)由于Office总是假定当前有一个真正的“用户”在使用它,所以,它可能在某些时候会主动弹出一些窗口,要求用户与之交互。比如,当某个操作没有成功完成,或发生一些非预见情况时(比如Office要打印却发现没有打印机、要保存一个文件却发现已存在同名文件),Office会显示一个模式窗口,提示或询问用户一些信息,而在哪些时候会出现这些窗口是不能被完全预见的。由于我们的代码不能取消这样的模式窗口,那么当前进程会被完全堵塞,失去响应。
(2)作为一个在服务器端运行的组件,必须事先被设计成能够被多个客户端重复使用、开销尽可能少,而Office恰恰相反(因为Office原本就是设计成在客户端被使用),每个Office应用程序都占用大量的资源,也很难被重复使用。
(3)大家日常使用Office的时候,应该能够经常看到Office会出现一个“正在准备安装…”的对话窗口,这是因为Office引入了一种叫做“首次使用时安装”的安装模式,某些组件有可能在第一次被使用到时才去安装它。而如果在服务器端出现这样的情形,那么服务器的稳定性就很难保证了。
(4)Office总是假定当前的运行环境中,是一个真实用户的账号身份,但是服务器端Office Automation有时候却是使用一些系统账号(比如Network Service、IUser_Machine之类的)来运行Office,这时Office很可能会无法正常启动,抛出错误。
所以,除非万不得已,不要进行服务器端Office Automation操作!但是,有时候很多事情并不是由程序员决定的,所以还是有不少人铁了心、咬着牙,非得在服务器端做这个操作不可。如果你真的已经下定了决心,并且有信心克服遇到的一切困难,那么下面提供一些服务器端Office Automation的建议,供大家参考。
(1)尽可能的预防Office主动弹出一些用户交互窗口。比如,修改Application的AskToUpdateLinks、AlertBeforeOverwriting、DisplayAlerts、FeatureInstall这些属性的值,都能够预防一些用户交互窗口的弹出。另外,在编写代码时主动进行防御也很重要,比如在保存一个文件之前,先用代码检测一下是否已经有同名文件存在,打开一个文件之前,也用代码先检测一下是否文件确定存在。
(2)将运行Office的环境隔离起来。不要直接在ASP.NET代码中创建Office应用程序的实例,否则出现问题以后,IIS都可能宕掉。创建一个单独的应用程序,来进行Office Automation的操作,然后让ASP.NET程序与这个单独的应用程序通讯,间接访问Office的功能。如果有条件,甚至最好将进行Office Automation操作的单独应用程序放在一台单独的服务器上运行,这样如果真的出现异常情况,可以直接重新启动这台服务器而不影响真正业务系统的正常运行。另外,让那个单独的应用程序使用一个特定的账号运行(比如新创建并设置好的一个可以进行客户交互的账号)。
(3)最好创建一个单独的守护进程,检测是否Office没有被正确关闭,如果发现这样的情况,在守护进程中直接关闭掉Office相应进程。
二、在代码中关闭Office应用程序
当我们在.Net代码中访问Office编程接口时,COM Interop在底下会创建一个RCW(Runtime Callable Wrapper,运行时远程访问包装器),来维护对Office COM组件的引用。为了让Office能够被正常关闭,关键就是要在代码中释放掉对Office 相关对象的引用。
下面介绍了多种保障措施,让Office应用程序能够被正常关闭,在某些情况下,使用最简单的一种方式即可,而在某些情况下,则可能需要将多种方式综合起来使用。
0、记得调用Application.Quit()方法
呵呵,还真有程序员忘记调用这个方法来退出Office应用程序,不管最后用哪种方法保障关闭Office,却忘记调用这个Quit()方法,那什么都是白搭。
1、让垃圾回收完成所有工作
由于.Net Framework提供了垃圾回收器来进行内存的自动管理,所以原理上,只要我们的代码中释放掉对Office相关对象的引用(将其赋值为null),那么垃圾回收最终会将这个对象回收,那时RCW会相应的释放掉Office COM组件,使Office被关闭。为了保证关闭的即时性,我们最好主动调用垃圾回收,让垃圾回收立即进行。
Microsoft.Office.Interop.Word.Application wordApp = new Microsoft.Office.Interop.Word.Application();
// 进行某些操作…
Object missing = Type.Missing;
wordApp.Quit(ref missing, ref missing, ref missing);
wordApp = null;
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
在大部分情况下,这种方法已经可以让我们的代码释放掉对Office的引用,使Office被关闭掉。
2、调用System.Runtime.InteropServices.Marshal.ReleaseComObject()方法
ReleaseComObject()方法可以使RCW减少一个对COM组件的引用,并返回减少一个引用后RCW对COM组件的剩余引用数量。我们用一个循环,就可以让RCW将所有对COM组件的引用全部去掉。
先创建一个单独的方法,释放一个Office相关对象的所有引用。
private void ReleaseAllRef(Object obj)
{
try
{
while (ReleaseComObject(obj) > 1);
}
finally
{
obj = null;
}
}
然后,调用这个ReleaseAllRef()方法即可。
Microsoft.Office.Interop.Word.Application wordApp = new Microsoft.Office.Interop.Word.Application();
// 进行某些操作…
Object missing = Type.Missing;
wordApp.Quit(ref missing, ref missing, ref missing);
ReleaseAllRef(wordApp);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
(3)明确单独声明和释放每一个中间对象变量
中间对象变量就是指代码中直接通过一个对象的属性得到的一个对象,不单独声明它,而再次直接使用它。比如:
Document doc = wordApp.Documents.Add(…);
上面的代码中,wordApp.Documents这个属性实际是一个Microsoft.Office.Interop.Word.Documents类型的对象,但是上面的代码没有单独声明这个对象,而是直接使用了它的Add()方法。如果要单独声明它,则需要更改成如下:
Documents docs = wordApp.Documents;
Document doc = docs.Add(…);
在使用完这些对象后,使用(2)中所描述的方法,再一一释放掉它们。
doc.Close(...);
ReleaseAllRef(doc);
ReleaseAllRef(docs);
wordApp.Quit(...);
ReleaseAllRef(wordApp);
三、最后的总结
这篇文章的标题是《“彻底干净的”关闭Office程序》,之所以在“彻底干净的”这个修饰上打上引号,原因就是其实是没有任何一劳永逸的、100%有效的方法,关闭掉Office程序。任何进行了Office Automation操作的代码,都必须被仔细测试和评估,将其对我们的程序所造成的影响,降到最低。