簡介
最近我編寫了很多智能客戶端應用,總結了一些能夠使應用程序在後台調用Web Service時不凍結前台界面的異步調用方法。雖然當前.NET Framework本身已經提供了異步調 用的機制,但我發現在Windows應用中這一機制比較難於把握,因為這時你需要正確的控制用 戶界面線程處理。
在這篇文章中,我將教給您一種在Windows應用程序中實現異步調 用Web服務的簡單方法,通過這一方法,您不用再考慮後台線程與前台界面線程的交互關系了 。
服務代理
Visual Studio® .NET會生成較好的Web服務代理類,通過它可以 異步的使用Web服務,但是這個代理類實現的是.NET Framework本身的異步調用機制,如上所 述,這一機制對於Windows應用來說並不十分方便。由於這個原因,我一般不直接使用生成的 代理類,而是在中間增加服務代理類。
服務代理類就是增加了額外功能的類,這些功 能可以幫助客戶端程序與Web服務進行交互。服務代理類實現了許多有用的功能,包括數據緩 存,安全身份管理,離線操作支持等等。本文中創建的服務代理類比.NET Framework本身的 普通代理類實現了更簡便的異步調用模式。
用戶界面線程
應用程序從一個創建和 管理用戶界面的線程起始,這一線程被稱為用戶界面線程。大多數開發者本能的會使用用戶 界面線程完成所有的工作,包括進行Web服務調用,遠程對象調用,訪問數據庫等等,大多數 使用和性能方面的問題是由這一不恰當的方法引起的。
問題的本質是你永遠不可能精 確的預知訪問Web服務,遠程對象,或者數據庫所需的時間。而且當你在用戶界面線程中進行 這類的調用時,用戶界面就有可能會產生令人惱怒的凍結。
自然而然的,你會把這一 類的調用放置在一個單獨的線程中,但我更進了一步,建議您把所有的非用戶界面工作坊制 在一個分離的線程中。我的觀點是,用戶界面線程只用來管理用戶界面,而所有那些你不能 保證良好響應時間的對象調用都應該是異步的,無論是進程內的,跨進程的,還是跨計算機 的。
無論如何,盡量使用戶界面線程處理的異步調用模式簡單化,我已經實現了一個 與Visual Studio 2005裡某個特性類似的簡單異步調用模式。作為開始,我們首先解釋一下 當前.NET Framework中異步調用模式的工作原理。
.NET異步調用模式
系統生成的 Web服務代理類的每個Web函數都有一個Begin和一個End方法,每個支持.NET Framework異步 調用模式的對象都和這個類似。開始進行異步調用時,客戶端調用Begin方法時就立即響應, 或者在建立了訪問Web服務的獨立線程後馬上響應。在這之後的某個時間,當Web服務訪問完 成後,客戶端再調用End方法。
但客戶端如何知道什麼時候調用End方法呢?Begin方 法會返回一個IAsyncResult對象,可以幫助你跟蹤異步調用的過程,也可以明確的等待後台 線程完成,但如果在用戶界面線程中進行這些工作,會降低整個系統的同步性。更好的方法 是,在用戶界面進程中注冊一個回調函數,當其它工作完成時產生一個自動通知。
讓 我們看一段樣例代碼,在這段代碼中,我們從一個Web服務中獲取一些客戶數據,這些功能通 過Web服務代理類裡的GetCustomerData方法完成。我們可以啟動這個Web服務調用,並且用以 下代碼注冊一個回調函數,用來在用戶界面線程中產生與應用程序進行交互的功能。
private void SomeUIEvent( object sender, EventArgs e )
{
// Create a callback delegate so we will
// be notified when the call has completed.
AsyncCallback callBack = new
AsyncCallback( CustomerDataCallback );
// Start retrieving the customer data.
_proxy.BeginGetCustomerData( "Joe Bloggs", callBack, null );
}
Web服務調用最終返回CustomerDataCallback方法,在這個方法中,我們需要調用 真正用於獲取客戶數據的Web服務代理類中的End方法,這個方法可以實現如下:
public void CustomerDataCallback( IAsyncResult ar )
{
// Retrieve the customer data.
_customerData = _proxy.EndGetCustomerData( ar );
}
現在,你必須注意,這一方法是被後 台工作線程調用的。如果想在前台界面上使用剛剛獲得的信息(例如在一個data grid控件中 顯示那些客戶數據),一定不要忘記在用戶界面線程中進行更新。如果忘了這樣做,就會發 生很多莫名其妙的錯誤,而且調試起來還相當的不易。
那麼我們怎麼切換線程呢?我 們可以使用服務代理的方法,所有的控件都源自這些對象的實現。我們可以實現一個從用戶 界面線程調用的方法,在這個方法內我們可以安全的更新我們的界面。使用Control.Invoke 方法,我們必須給用戶更新方法傳遞一個委托,現在,CustomerDataCallback方法的代碼更 新如下:
public void CustomerDataCallback( IAsyncResult ar )
{
// Retrieve the customer data.
_customerData = _proxy.EndGetCustomerData( ar );
// Create an EventHandler delegate.
EventHandler updateUI = new EventHandler( UpdateUI );
// Invoke the delegate on the UI thread.
this.Invoke( updateUI, new object[] { null, null } );
}
UpdateUI方法可以按如下辦法實現:
private void UpdateUI( object sender, EventArgs e )
{
// Update the user interface.
_dataGrid.DataSource = _customerData;
}
這並不是十分非常嚴謹科學,因此可以使這一“兩 次跳轉”的復雜問題簡單化起來。關鍵在於異步方法的原始調用(以WinForm類為例) 用來負責轉換線程,並且需要另一個委托以及Control.Invoke方法。
一個簡化的異步 調用模式
我經常使用一項技術來減少創建異步調用時的復雜度和代碼量,那就是把線程切 換和委托的實現放入一個中間類中。這就使得我們從用戶界面類中進行異步調用時,不用再 去考慮什麼線程和委托的問題。我把這項技術叫做自動回調。使用這項技術,上面的樣例可 以進行如下改進:
private void SomeUIEvent( object sender, EventArgs e )
{
// Start retrieving the customer data.
_serviceAgent.BeginGetCustomerData( "Joe Bloggs" );
}
當 Web服務訪問完成後,以下的方法就會自動被調用:
private void GetCustomerDataCompleted( DataSet customerData )
{
// This method will be called on the UI thread.
// Update the user interface.
_dataGrid.DataSource = customerData;
}
回調函數的名稱由原始異步 調用的名稱來決定(因此就不再需要創建和傳遞委托了),並且可以保證被正確的線程所調 用(也就不再需要使用Control.Invoke了),這些方法都很簡單並且不容易出錯。
天 下沒有免費的午餐,實現這個簡單模型的神奇代碼是需要我們來編寫的。下面所列的就是被 編寫進ServiceAgent類中的這些代碼:
public class ServiceAgent : AutoCallbackServiceAgent
{
private CustomerWebService _proxy;
// Declare a delegate to describe the autocallback
// method signature.
private delegate void
GetCustomerDataCompletedCallback( DataSet customerData );
public ServiceAgent( object callbackTarget )
: base( callbackTarget )
{
// Create the Web service proxy object.
_proxy = new CustomerWebService();
}
public void BeginGetCustomerData( string customerId )
{
_proxy.BeginGetCustomerData( customerId,
new AsyncCallback( GetCustomersCallback ), null );
}
private void GetCustomerDataCallback( IAsyncResult ar )
{
DataSet customerData = _proxy.EndGetCustomerData( ar );
InvokeAutoCallback( "GetCustomerDataCompleted",
new object[] { customerData },
typeof( GetCustomersCompletedCallback ) );
}
}
這個樣例中服務代理類的代碼是簡單容易的,而且完全可以重用,我們所要做的 就是給WinForm類編寫一套類似的通用代碼。我們有效的提升了線程管理工作的重要性,並且 把它同編寫後台異步調用對象代碼的工作以及編寫前台客戶端代碼的工作分離了開來。
AutoCallbackServiceAgent基類是一個實現了InvokeAutoCallback方法的簡單類,代 碼如下:
public class AutoCallbackServiceAgent
{
private object _callbackTarget;
public AutoCallbackServiceAgent( object callbackTarget )
{
// Store reference to the callback target object.
_ callbackTarget = callbackTarget;
}
protected void InvokeAutoCallback( string methodName,
object[] parameters,
Type delegateType )
{
// Create a delegate of the correct type.
Delegate autoCallback =
Delegate.CreateDelegate( delegateType,
_callbackTarget,
methodName );
// If the target is a control, make sure we
// invoke it on the correct thread.
Control targetCtrl = _callbackTarget as
System.Windows.Forms.Control;
if ( targetCtrl != null && targetCtrl.InvokeRequired )
{
// Invoke the method from the UI thread.
targetCtrl.Invoke( autoCallback, parameters );
}
else
{
// Invoke the method from this thread.
autoCallback.DynamicInvoke( parameters );
}
}
}
以上這些代碼創建了一個回調函數的委托,並且判斷是在調用線程,還是在用戶 界面線程中調用它。如果調用的目標是一個控件對象,那麼它就會在需要的時候從用戶界面 線程來調用回調函數。
探究這些有趣的細節,如果你仔細的查看代碼,你會發現我們 可以通過不在基類中指定自動回調委托來進行簡化。如果我們不需要對回調委托進行簽名, 我們就可以幾乎自動化的處理所有的事情,把基礎的服務代理類簡化成只在 BeginGetCustomerData方法中實現一行代碼。
那我們為什麼還要指定這個委托呢?那 是因為我們還需要使用Control.Invoke方法。不幸的是.NET Framework的開發者並沒有為這 一方法提供一個MethodInfo對象,而恰恰是它可以使編寫基礎代碼的工作變得簡單許多。
一個替代的辦法是指定一個標准的委托類型,把它用於所有的回調函數簽名。舉例來 說,我們可以要求所有的自動回調函數都使用一個方法簽名,這個方法簽名用來維護原始的 對象組,並且向客戶端回傳Web服務的參數。委托的聲明方法如下:
public delegate void AutoCallback( object[] parameters );
使用這個委托我們可以 極大地簡化服務代理類的代碼,但是必須在客戶端代碼中把返回的數據轉換成一定的格式。
這樣做值得麼?
有必要像上面一樣實現一個服務代理類嗎?這取決於你想多大程 度上簡化用戶界面開發人員的工作。編寫一個如上的服務代理類不一定會減少代碼量,但可 以使界面開發人員和後台服務開發人員的分工更加明確有效。
除了提供這種簡單的異 步調用模式外,我們還可以往服務代理類中添加更多的有用功能。以後我會繼續在這個思路 的基礎上加以擴展,向你展示如何在服務代理類上實現諸如自動本地數據緩存等高級功能。 在服務代理類上實現這些高級功能意味著用戶界面開發人員可以更加輕松的完成工作了。