ClientBase
protected void InvokeAsync(
ClientBase.BeginOperationDelegate beginOperationDelegate,
Object[] inValues,
ClientBase.EndOperationDelegate endOperationDelegate,
SendOrPostCallback operationCompletedCallback,
Object userState
)
特別に結論のある話ではなくて、要点だけをまとめるなら「SilverlightのUIスレッドモデルには気をつけるべし」といったところだろうか。UIと一見無関係なものも関係することがあるかもしれない、という話でもある。
SilverlightのUIスレッドモデル
silverlight2ではAJAXではないフツーのマルチスレッドが使えるのだけど、Windows Formsがそうであったように、UIの更新はUIスレッドで行われなければならない。SilverlightのUIは、Win32APIやX11と同様メッセージループ(イベントループ)として実装されていて、UIに対する命令はキューに格納されて逐次実行される1。
アプリケーションを作成するとき、UI上である操作を呼び出した時に、その結果が返ってくるまで、UIをブロッキングすることが、昔は普通だったし、いまでもたまにあるけど(キャンセル・中断ができない状態)、これをWebブラウザ上で行われると、その処理中は別の操作(たとえばページのスクロールとか)が全く出来ないので、けっこう困る。というわけで、Silverlightでは、同期処理は基本的に排除され、さらにはBeginFoo()みたいにEndFoo()がブロッキングするような実装にもならず、FooAsync(fooCompletedCallback)みたいに、終了コールバックを使って、処理の継続が指示できるように作られるのが、典型的なモデルだ。
WCFのClientBase
WCFのサービス クライアントは、client proxyを生成することで、サービスを表すinterfaceの呼び出しのみで全て行えるように作られている。
実際には、client proxyは以下の2種類が存在する。System.ServiceModel.ClientBase
前者を具体的に言えば、IServiceContractFooというサービス インターフェースのクライアントは、ClientBase
class ServiceContractFooSoapClient
: ClientBase, IServiceContractFoo
{
....
}
ClientBase
Visual Studioやsvcutilを使った自動生成には、WSDLが使われるので、WSDLを扱うクラスが存在しないSilverlightでは、これを行うことができない。従って、Silverlightのclient proxy生成にも、.NET 3.0が使われているはずだ。
ちなみに初期の開発版Silverlight2にはslwsdl.exeというツールがあったが、いつしか消えて無くなってしまった。まあそれは無理からぬことで、実のところVisual Studioで自動生成されているWebサービス参照は、CLR (full)を利用して生成されているはずだから、Silverlight2では実装できなかったはずである。
おまけ: ChannelFactory
ちなみに、WCFにはChannelFactory
このChannelFactory
ClientBaseのAPI変更: 非同期モデル
SilverlightのWCFは、.NET 3.0のWCFから膨大かつ無駄なWS-*の大部分を削り落としてスリム化した一方で、client proxyについてはけっこう作り替えている部分が多い。具体的には、それまで同期モデル中心で提供されていたAPIが、上記のような要請から、非同期モデルを前提に作り直されているのである。
SilverlightのClientBaseでは、非同期モデルのサービス呼び出しメソッドInvokeAsync()が登場している。.NET 3.0には同期モデルのメソッドも存在せず、派生クラスとして自動生成されたサービス実装クラスのサービス実装メソッドがClientBase.Channel(として内部で動的生成されたproxyクラス)のメソッドを呼び出していた。非同期モデルに様変わりしてはいるけど、Silverlightでもこれと同じことが行われている。
SilverlightにはSystem.ServiceModel.ClientBase
monoのソースで言えば、System.ServiceModel.ClientRuntimeChannelというのがその基底クラスになって、System.ServiceModel.ClientProxyGeneratorというクラスがMono.CodeGenerationというライブラリを使ってclient proxyを生成していた。これがSilverlightになると、ClientBase.ChannelがClientBase
BrowerHttpWebRequest
さて、ここで一見寄り道に見える話をしよう。.NET 3.0とSilverlightでは、WebRequestまわりも少なからず変更されている。一番わかりやすいのがHttpWebRequestだ。このクラスはもはやSystem.dllではabstractとしてしか定義されていない。
WebRequest.Createで生成されるHttpWebRequest(の派生クラス)のインスタンスは、System.Windows.Browser.dllの中で定義されている。このdllは、(名前から想像できるように)ブラウザ プラグインAPI(IEならActiveXだろうし、moonlightではNPAPIを使っているので多分Silverlightもそうだろう)に依存する機能をまとめたものである。要するに、SilverlightのHttpWebRequestは、ブラウザのネイティブなHTTP発行命令を利用するかたちで実装されているのである。
開発版のSilverlightでは、これはSystem.Windows.Browser.Net.BrowserHttpWebRequestというpublicなクラスで実装されていたが、最終版では非公開になって、Silverlightの内部で間接的に生成されるのみになった。便宜上、本文ではこのHttpWebRequest実装をBrowserHttpWebRequestと呼ぶことにする。
BrowserHttpWebRequestの注意すべき点は、UIスレッドの制約だ。Webブラウザそのものは、Silverlight本体とは異なり、本来的にマルチスレッドをサポートしていないので、ブラウザのAPIを経由してリクエストを発行するということは、UIスレッド上にまずメッセージ(イベント)を登録し、ループのあるタイミングでHTTPレスポンスが返ってきたら、それを呼び出し元に返す、という処理になることを意味する。
BasicHttpBinding
ClientBaseのInvokeAsync()の話をする前に、もう一つ片付けておかなければならない前提がある。
.NET 3.0には数多くのTransportBindingElementが存在するが、Silverlightに存在するのはHttpTransportBindingElement(とHttpsTransportBindingElement)のみである。Bindingの選択肢も多くはないが、すべてHttpBindingElementに依存することになる。Silverlightには.NET 3.5で追加されたWebHttpBindingがなぜか含まれていないが、これもやはりHttpWebRequestを使用していた。
SiverlightのようなクライアントサイドにおけるBindingElementの役割は、ChannelFactory経由でIRequestChannelやIOutputChannelのインスタンスを提供することだ。そしてこのHttpTransportBindingElementは、(ごく自然に)前述のBrowserHttpWebRequestを使用することになる。
.NET 3.0のWCFとSilverlightのWCFでは、HttpTransportBindingElementのソースコード上の違いは小さいかもしれないが、そのクライアントチャネルの実行モデルは大きく異なっているのである。
ClientBase.InvokeAsync()をやっつける
さて、前提条件はすべて整った。以下、InvokeAsync()のやっつけ方について書こうと思う。
.NET 3.0には、同期呼び出しの他に、BeginFoo(), EndFoo()を使った非同期呼び出しのサポートも含まれている。非同期メソッドの一番簡単なやっつけ方は、非同期終了メソッドEndXxx()で同期メソッドXxx()を呼び出してしまうことだ。非同期処理ではないが、ほとんどの場合はこれで機能はする。
でもこれはInvokeAsync()については上手くいかない。なぜなら、同期メソッドでリクエストを発行してから応答を受け取るまで、そのスレッドはブロックされることになるが、同期メソッドがUIスレッドから呼び出されていれば、そのスレッドはApplicationのメッセージループに戻ることがなく、BrowserHttpWebRequestで登録されたリクエスト発行メッセージも処理されないからだ。リクエストが発行されなければ、応答を受け取ることもできない。
ひとつの正しいやっつけ方としては、同期メソッドをDelegateにして、BeginInvoke()で呼び出して、終了時にendOperationDelegateやoperationCompletedCallbackを呼び出すようにしてしまう、というものだろう(これは実際にはSilverlightのmscorlibでは不可能なので、BackgroundWorkerなどを使うことになるだろう)。InvokeAsync()に渡されるbeginOperationDelegateは、ClientBase
operationCompletedCallbackの実行スレッド
これで問題なく実装完了かと思ったら、そうでもなかった。operationCompletedCallbackには、アプリケーションが実際にWCFのサービス呼び出しの完了を受けて実行されるコードが渡されており、これはUIスレッド(正確にはInvokeAsync()を呼び出したスレッド)で実行されなければならない。これを回避するには、結局UIスレッドで処理を実行するコードパスを通さなければならない。
System.Windows.dllには、System.Windows.Threading.Dispatcherというクラスがあって、これがUIスレッドのメッセージループを管理している。System.Windows.Browser.HtmlWindowのプロパティDispatcherをリフレクション経由で2叩けば、それで上手くいくかもしれない。ちなみにInvokeAsync()はこれでは上手くいかなかったのだけど、仲間がinternalメンバのDispatcher.Mainを使えと教えてくれたので、それを使ったら上手く処理できるようになった(もちろんmoonlightの実装依存である)。
というわけで、↓こんなのもmoonlight2で動くようになりますた。
追記
ちなみに、この辺のデバッグはちっとめんどい。メインスレッド以外で行われているサービス呼び出しのエラーはunhandledだと握りつぶされるし、2.1ランタイムなのでdebuggerもAttach APIも使えないので、gdbでちまちま見るか(面倒すぎるので僕はほとんどやっていない)、Console.WriteLine()で出すしかない。まあ、WriteLineできているだけ、Silverlight上でやるよりはマシかもしれない。
