|
采用VSTO或者Shared Add-in等技術(shù)開發(fā)Excel插件,其實(shí)是在與Excel提供的API在打交道,Excel本身的組件大多數(shù)都是COM組件,也就是說通過Excel PIA來與COM進(jìn)行交互。這其中會(huì)存在一些問題,這些問題如果處理不好,通常會(huì)導(dǎo)致在運(yùn)行的時(shí)候會(huì)拋出難以調(diào)試的COM異常,從而導(dǎo)致我們開發(fā)出的Excel插件的不穩(wěn)定。 和普通的WinForm程序一樣,Excel也是一種STA(Single Thread Apartment)線程的應(yīng)用程序,Excel插件是寄宿在Excel中運(yùn)行的,這也就意味著插件也是一種STA線程的應(yīng)用程序。插件在操作Excel的時(shí)候,如果是在Excel的主線程中,可以直接獲取Excel對象進(jìn)行操作,比如寫入單元格值,對單元格進(jìn)行格式化等操作。但是通常,我們會(huì)在多線程或者后臺(tái)工作線程中去處理一系列復(fù)雜的數(shù)據(jù)或者邏輯,待處理完成獲得結(jié)果之后,再像WinForm那樣,回到UI線程中,去更新界面信息,對于Excel插件來說,就是回到Excel的主線程上來,然后再更新界面。但是Excel又是一種不同于一般Winform 類型的STA,它是COM并且Excel插件是寄宿在其上的,所以還有一些需要注意的地方。 本文首先介紹什么是STA應(yīng)用程序及其工作原理,然后介紹一般的Winform程序的界面刷新邏輯,以及在這其中非常重要的一個(gè)名為SynchronizationContext對象,最后介紹在Excel插件中如何獲取Excel主線程,以及這其中需要注意的地方。 Excel插件的最難處理的地方在于其應(yīng)用程序的穩(wěn)定性,了解了Excel中的線程以及其機(jī)制對增強(qiáng)系統(tǒng)的穩(wěn)定性會(huì)有很大的幫助。
1. STA(Single Thread Apartment)COM組件的線程模型被稱之為Apartment模型,即COM對象初始化時(shí)其執(zhí)行上下文(Execution Context),他要么和單個(gè)線程關(guān)聯(lián)STA(Single Thread Apartment ) 要么和多個(gè)線程關(guān)聯(lián)MTA(Multi Thread Apartment)。 通常COM對象為了保護(hù)其自身維護(hù)的數(shù)據(jù)不被破壞,需要運(yùn)行時(shí)來保證其不被多個(gè)線程同時(shí)調(diào)用;另外也需要運(yùn)行時(shí)來保證對COM對象的調(diào)用不會(huì)阻塞UI線程。Apartment 就是COM對象生存的地方,一個(gè)Apartment可以包含一個(gè)或者多個(gè)線程。對一個(gè)COM對象的調(diào)用可以由該COM生存的Apartment中的任何一個(gè)線程接受和處理。如果一個(gè)Apartment中只有一個(gè)線程,那么就是STA線程,否則就是MTA,這個(gè)是在程序初始化COM組件的時(shí)候即確定下來的。一個(gè)進(jìn)程可以包含多個(gè)STA,但是只有一個(gè)MTA。 STA模型是COM對象使用的一種非線程安全的模型,這意味著他不能處理自己的線程同步,通常在UI組件中使用這種模型。因此,如果其他線程需要和UI對象進(jìn)行交互,需要將消息封送(marshall)到STA線程中。在Windows 窗體應(yīng)用程序中,這一過程是通過窗口消息隊(duì)列 (message pumping system)來實(shí)現(xiàn)的。當(dāng)客戶線程以STA 模式啟動(dòng)時(shí),系統(tǒng)將為STA創(chuàng)建一個(gè)隱藏窗口類,所有的對COM對象的調(diào)用都會(huì)放到這個(gè)隱藏窗口的消息隊(duì)列中。 如果COM對象能夠處理其本身的同步邏輯,那么就是MTA模型了,他似的多個(gè)線程能夠同時(shí)和對象進(jìn)行交互,而不需要進(jìn)行消息調(diào)用的封送。 COM組件在創(chuàng)建的時(shí)候采用哪種模型,可以在注冊表項(xiàng)的ThreadingModel值中指定: COM組件在注冊表項(xiàng)中的ThreadingModel屬性中會(huì)有一下四個(gè)屬性:
對于.NET Framework來說,通常在任何創(chuàng)建UI的線程上使用[STAThread]自定義屬性來標(biāo)識(shí)其為STA線程。工作線程通常使用MTA模型,但是如果該工作線程需要與表示為Apartment的COM組件一起使用,那就需要標(biāo)識(shí)為STAThread。 我們可以給Thread對象的ApartmentState屬性指定ApartmentState枚舉類型來給定該Thread屬于那種類型的線程。 那么如何在其他線程中往STA線程中封送消息呢?這個(gè)就要使用SynchronizationContext對象了。
2. SynchronizationContext關(guān)于SynchronizationContext類,Understanding SynchronizationContext (Part I) 這篇文章講解的比較好,建議直接閱讀原文。這里簡要說一下,為后面講解做鋪墊。 SynchronizationContext類主要是用來進(jìn)行線程間進(jìn)行通訊的, 比如我有Thread1和Thread2,Thread1在做一些事情,完了之后,Thread1希望將結(jié)果傳遞給Thread2,希望在Thread2上執(zhí)行操作。一種可行的方式是獲取Thread2的SynchronizationContext對象,然后在Thread1中調(diào)用SynchronizationContext的Send或者Post方法,這樣需要做的操作就會(huì)在Thread2上執(zhí)行的。需要注意的是,并不是所有的線程都有一個(gè)SynchronizationContext與之聯(lián)系,只有UI線程上才有SynchronizationContext,通常是在線程中,第一次創(chuàng)建UI控件的時(shí)候,就會(huì)將SynchronizationContext對象附加到當(dāng)前的線程中。 在進(jìn)行Winform開發(fā)的時(shí)候,我們知道不應(yīng)該在UI線程上執(zhí)行耗時(shí)的操作,因?yàn)閁I線程是一種STA線程,是通過消息隊(duì)列來實(shí)現(xiàn)的,如果某一操作耗時(shí)的話會(huì)阻塞其他的消息處理,影像用戶交互。所以我們一般需要將一些耗時(shí)操縱放到后臺(tái)線程中去處理,完了之后將結(jié)果Post回UI線程來進(jìn)行界面刷新,我們常在非UI線程中使用Control的Invoke和BeginInvoke來實(shí)現(xiàn)UI界面的刷新。而Invoke和BeginInvoke在內(nèi)部其實(shí)是通過繼承自SynchronizationContext的對象來發(fā)送消息實(shí)現(xiàn)的。 通常,可以通過SynchronizationContext.Current的靜態(tài)屬性來獲取當(dāng)前線程的SynchronizationContext對象 有了UI線程的SynchronizationContext對象我們就可以在其他線程上通過該對象將我們需要在UI線程上進(jìn)進(jìn)行的操作Post到UI所在的線程上的消息隊(duì)列中了。 下面的代碼中我們在button2中新建了一個(gè)新的進(jìn)程,然后在該進(jìn)行的方法中傳入了當(dāng)前UI線程的SynchronizationCotext對象, 然后在工作線程中通過該SynchronizationContext對象的Post方法更新UI界面上的Combox對象: private void button2_Click(object sender, EventArgs e) { // let's see the thread id int id = Thread.CurrentThread.ManagedThreadId; Trace.WriteLine("Button click thread: " + id); // grab the sync context associated to this // thread (the UI thread), and save it in uiContext // note that this context is set by the UI thread // during Form creation (outside of your control) // also note, that not every thread has a sync context attached to it. SynchronizationContext uiContext = SynchronizationContext.Current; // create a thread and associate it to the run method Thread thread = new Thread(Run); // start the thread, and pass it the UI context, // so this thread will be able to update the UI // from within the thread thread.Start(uiContext); } private void Run(object state) { // lets see the thread id int id = Thread.CurrentThread.ManagedThreadId; Trace.WriteLine("Run thread: " + id); // grab the context from the state SynchronizationContext uiContext = state as SynchronizationContext; for (int i = 0; i < 10; i++) { // normally you would do some code here // to grab items from the database. or some long // computation Thread.Sleep(10); // use the ui context to execute the UpdateUI method, // this insure that the UpdateUI method will run on the UI thread. uiContext.Post(UpdateUI, "line " + i.ToString()); } } /// <summary> /// This method is executed on the main UI thread. /// </summary> private void UpdateUI(object state) { int id = Thread.CurrentThread.ManagedThreadId; Trace.WriteLine("UpdateUI thread:" + id); string text = state as string; comboBox1.Items.Add(text); } 運(yùn)行結(jié)果如下,我們可以看到Button以及UpdateUI的方法都是在UI線程上運(yùn)行的,他們具有相同的線程ID 9,而我們新建的工作線程ID為10。 Button click thread: 9 Run thread: 10 UpdateUI thread:9 UpdateUI thread:9 UpdateUI thread:9 UpdateUI thread:9 UpdateUI thread:9 UpdateUI thread:9 UpdateUI thread:9 UpdateUI thread:9 UpdateUI thread:9 UpdateUI thread:9 SynchronizationContext對象有Send和Post兩個(gè)方法可以被我們調(diào)用。Send方法是同步的,他會(huì)等待Send進(jìn)去的代理方法執(zhí)行完成之后,再執(zhí)行后面的代碼,而Post方法則是異步的,Post之后會(huì)繼續(xù)執(zhí)行后續(xù)的代碼,Post和Send方法會(huì)異常的捕獲。其內(nèi)部的實(shí)現(xiàn)大致如此: public virtual void Send(SendOrPostCallback d, Object state) { d(state); } public virtual void Post(SendOrPostCallback d, Object state) { ThreadPool.QueueUserWorkItem(new WaitCallback(d), state); } 實(shí)際上在Winform以及WPF中,我們獲取到的是繼承自SynchronizationContext的對象,在Winform中是System.Windows.Forms.WindowsFormsSynchronizationContext在WPF中則是 System.Windows.Threading.DispatcherSynchronizationContext。比如在Winform中是Control.BeginInvoke,在WPF或者Silverlight中是Dispatcher.BeginInvoke,這些類都重寫了Post和Send方法。并提供了各自的“消息隊(duì)列”(message pump)機(jī)制比如Windows API中的SendMessage 和PostMessage方法來實(shí)現(xiàn)各自的消息分發(fā)和處理。我們在上面代碼中通過SynchronizationContext的Current獲取到的實(shí)際上是一個(gè)WindowsFormsSynchronizationContext對象。真實(shí)的SynchronizationContext類不做任何實(shí)現(xiàn),他更應(yīng)該是一個(gè)虛類。所以通過手動(dòng)new一個(gè)SynchronizationContext,然后賦予當(dāng)前的線程是沒有任何意義的。
3. Excel中的線程同步前面講過STA以及SynchronizationContext,這是因?yàn)镋xcel也是一種STA線程的應(yīng)用程序,寄宿在Excel之上的Automation程序也是STA的,了解這一點(diǎn)非常重要。 通常在Excel的插件開發(fā)中,我們的業(yè)務(wù)邏輯可能比較復(fù)雜,這些復(fù)雜的計(jì)算一般不應(yīng)該放到Excel的主UI線程中,我們需要新建工作線程,然后在里面進(jìn)行計(jì)算。獲得了結(jié)果之后,我們應(yīng)該在回到Excel的UI線程中去更新界面。但是我們采用.NET技術(shù)開發(fā)Excel的Automation有一個(gè)特殊性在于,我們可以直接在非UI線程中去調(diào)用Excel的COM對象,在正常情況下,如果Excel比較空閑,沒有任何問題,但是如果Excel此時(shí)比較忙,就會(huì)拋出COM異常,這種異常難以捕捉。這也是導(dǎo)致插件不穩(wěn)定的一個(gè)非常重要的因素。這種情況通常出現(xiàn)在以下情形中:
Excel中運(yùn)行我們再工作線程中通過Excel 的Application對象來直接更新UI界面元素給了我們一個(gè)假象。原因在于這樣是很不穩(wěn)定的,非Excel主線程的每一次COM調(diào)用中都需要檢查是否拋出異常,在調(diào)用過程中Excel很可能處于忙碌狀態(tài),Excel也可能在任何情況下拒絕線程對COM調(diào)用的請求,尤其是在用戶正與Excel進(jìn)行交互的時(shí)候。通常我們至少要捕獲和處理一下三種COM異常:
在其他線程中直接調(diào)用Excel對象不僅會(huì)導(dǎo)致性能損失,而且會(huì)增加插件的復(fù)雜性和不穩(wěn)定性。 正確的做法是,在工作線程中獲取Excel主線程對象的SynchronizationContext,然后將待操作的步驟Post到Excel主線程的消息隊(duì)列中等待處理。但是作為一個(gè)Addin,在一般情況下如果直接獲取SynchronizationContext對象,該對象是為空的,只有在插件加載后,手動(dòng)創(chuàng)建一個(gè)Winform窗體或者控件才能夠獲取到主線程的SynchronizaitonContext對象。這個(gè)From窗體通常就是我們插件的登錄窗體。 比如如果要在非Excel 主線程中調(diào)用RTD函數(shù)的UpdateNotify方法,我們可以首先定義一個(gè)SynchronizationContext用來保存Excel主線程的同步上下文。 private SynchronizationContext ExcelContext; 然后在RTD啟動(dòng)時(shí)獲取當(dāng)前Excel主線程的上下文。 public int ServerStart(IRTDUpdateEvent CallbackObject) { this.ExcelContext = new SynchronizationContext(); xlRTDUpdater = CallbackObject; } 最后工作線程中,通過傳進(jìn)來的ExcelContext,然后將需要做的操作Send或者Post回Excel主線程中執(zhí)行。 ExcelContext.Post(delegate(object obj) { xlRTDUpdater.UpdateNotify(); }, null); 所以其他非UI線程中需要操作Excel COM對象的方法經(jīng)過如此封裝將需要做的操作以消息的形式封送到UI線程,這樣就可以解決之前調(diào)用COM組件可能出現(xiàn)的COM異常,能夠極大提高Excel插件的穩(wěn)定性。 本文很多內(nèi)容涉及到COM組件的相關(guān)知識(shí),這里只是簡單的講解了一些與Excel插件開發(fā)中可能與之相關(guān)的一些問題,介紹了如何正確的在工作線程中更新Excel UI操作的一些正確做法,希望這些知識(shí)對您有所幫助。
參考資料本文參考了很多資料,如果您想深入了解,以下文章對您或許有幫助。
|
|
|