在《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操作的代碼,都必須被仔細測試和評估,將其對我們的程式所造成的影響,降到最低。