引き続き、WCF サービス呼び出しの紹介です。
今回は、Silverlight から WCF サービスを呼び出す際の方法について紹介します。
Silverlight からサービスを呼び出す場合には、Silverlight の制限によりサービスは同期呼び出しを行うことができず、必ず非同期呼び出しとする必要があります。
ですが、WCF サービス側には手を加えることなくサービスを非同期呼び出しすることができます。
まずは例として、Silverlight のサンプルプロジェクトを作成します。
『SilverlightServiceSample』という名前で「Silverlight アプリケーション」を作成します。
さらに『WcfService1』という名前で「WCF サービスアプリケーション」をソリューションに追加します。
「SilverlightServiceSample」プロジェクトには、前回同様「System.ServiceModel」を参照に追加しておいてください。
ただし、「WcfService1」は Silverlight プロジェクトではないため参照に追加することはできません。
WCF サービス定義を Silverlight から呼び出す方法は後程説明します。
Silverlight から呼び出し可能なサービスを作成する場合には、単純に「WCF サービスアプリケーション」を作るだけでは Silverlight から呼び出すことはできません。
「WCF サービスアプリケーション」プロジェクトに「プロジェクト」メニューから「新しい項目の追加」を選択し、「Silverlight」テンプレートの「Silverlight 対応 WCF サービス」からサービスを追加する必要がありますので、注意してください。
しかし、追加されたテンプレートコードをよく見ると、以下のようにインターフェイスを持たず「AspNetCompatibilityRequirements」属性が追加されているだけであることがわかります。
namespace WcfService1 { [ServiceContract(Namespace = "")] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] public class Service2 { [OperationContract] public void DoWork() { // 操作の実装をここに追加してください return; } // 追加の操作をここに追加して、[OperationContract] とマークしてください } }
このことから、Silverlight 対応には「AspNetCompatibilityRequirements」属性が付加されていれば参照できるようになることがわかります。
また、ChannelFactory を利用するうえでインターフェイスでメンバ定義が分離されているほうが都合がよいため、「WCF サービスアプリケーション」で作成されるテンプレートコードに「AspNetCompatibilityRequirements」属性を付加して利用することをお勧めします。
※実際には「AspNetCompatibilityRequirements」属性は付加されていなくてもデバッグ実行可能である場合が多いですが、IIS に配置した際に IIS の設定によってはサービスをアクティブ化することができないことがあります。また、「AspNetCompatibilityRequirements」属性は ASP.NET セッションを使用して各クライアントセッションの結果を保持します。これにより、サービスへの複数の呼び出しによる各クライアントの実行結果を保持できます。MSND(ASP.NET 互換性)
上記を踏まえ、サービスを以下のように書き換えます。
○IService1.cs
using System.ServiceModel; using System.ServiceModel.Activation; namespace WcfService1 { [ServiceContract(Name = "IService1", Namespace = "WcfService1.IService1")] public interface IService1 { [OperationContract] string GetData(int value); } }
○Service1.svc.cs
using System.ServiceModel.Activation; namespace WcfService1 { [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] public class Service1 : IService1 { public string GetData(int value) { return string.Format("You entered: {0}", value); } } }
サービスを作成するうえでの注意点として、上記「IService1」インターフェイスの「ServiceContract」属性に、Name および Namespace を追加するようにしてください。
異なるドメインからサービスを呼び出そうとした場合、Namespace の差異によりサービスを呼び出すことができなくなる場合があります。
Name パラメータにはわかりやすくインターフェイス名、Namespace パラメータにはインターフェイスのフルパス名を指定しておきます。
続いて、「WCF サービスアプリケーション」プロジェクトの「Web.config」にサービスの設定を行います。
<system.serviceModel> 配下の <services> セクションを以下のように書き換えます。
○Web.config
<system.serviceModel> <services> <service name="WcfService1.Service1"> <endpoint address="" binding="basicHttpBinding" contract="WcfService1.IService1" /> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" /> </service> </services> </system.serviceModel>
service セクションの name 属性にはサービスのクラス名をフルパスで。(name 属性は省略できませんが、適当な文字列で問題ありません)
endpoint セクションの binding 属性には basicHttpBinding を指定します。(ChannelFactory のインスタンス作成で指定する Binding パラメータと同じ型を指定する)
contract 属性には サービスのインターフェイス名をフルパスで指定します。(ServiceContract 属性を持つクラスまたはインターフェイスのフルパスを指定します)
最後に、作成したサービスと Silverlight アプリケーションのドメインが異なるため、サービスアプリケーションにポリシーファイルを追加します。
詳しい説明は MSDN(ネットワーク セキュリティのアクセス制限)を参照してください。
まず、「WcfService1」のルートに「ClientAccessPolicy.xml」を作成し、内容を以下の通りにします。
<?xml version="1.0" encoding="utf-8" ?> <access-policy> <cross-domain-access> <policy> <allow-from http-request-headers="SOAPAction"> <domain uri="*"/> </allow-from> <grant-to> <resource path="/" include-subpaths="true"/> </grant-to> </policy> </cross-domain-access> </access-policy>
追加後、「ClientAccessPolicy.xml」ファイルの「プロパティ」から「出力ディレクトリにコピー」を「常にコピーする」としておいてください。「コピーしない」のままだった場合、サービスを配置する際にファイルがコピーされず、ポリシーエラーが発生することがあります。
以上でサービスが作成できましたので、続いて Silverlight 側のサービス呼び出しを作成していきます。
最初に、「SilverlightServiceSample」に「 新しい項目の追加」から「クラス」を選択し(「インターフェイス」テンプレートが選択できないため)、例として「IService1Async.cs」という名前でインターフェイスを作成します。
「IService1Async.cs」のコードは「IService1.cs」の内容をそのままコピーしてきて、一部を以下のように書き換えます。
※作成したインターフェイスの名前空間が異なっていても、「ServiceContract」の Namespace 属性を指定していれば問題なく呼び出すことができます。また同じ名前空間であっても、「ServiceContract」の Namespace 属性を指定していなかった場合、呼び出すことができないことがありますので必ず定義するようにしてください。
○IService1Async.cs
using System; using System.ServiceModel; namespace SilverlightServiceSample { [ServiceContract(Name = "IService1", Namespace = "WcfService1.IService1")] public interface IService1Async { [OperationContract(AsyncPattern = true)] IAsyncResult BeginGetData(int value, AsyncCallback callback, object state); string EndGetData(IAsyncResult result); } }
ポイントとなるのは、「OperationContract」属性に「AsyncPattern = true」を指定することで、同期呼び出して作成されているサービスを、「非同期」で呼び出すことを指定します。
また、「GetData」メソッドの定義を以下のように書き換えます。
IAsyncResult BeginGetData(int value, AsyncCallback callback, object state);
string EndGetData(IAsyncResult result);
これは「非同期プログラミングモデル(Asynchronous Programming Model : APM)」と呼ばれるパターンで、「AsyncPattern = true」を指定した場合には赤字の部分は必ずこのように書き換えなければなりません。
これにより、同期呼び出しで作成したサービスを Silverlight の要件である非同期で呼び出し可能なサービスとして再定義することができます。
※「サービス参照の追加」でサービスを作成した場合も、自動生成されたコードの中で同じことをしていることが確認できます。
※ APM で定義した場合であっても、Silverlight アプリケーションに限らず、コンソールアプリケーションでも同様に呼び出すことが出来ます。
続いて、Silverlight から ChannelFactory を利用してサービス呼び出しを行います。
「SilverlightServiceSample」の「MainPage.xaml.cs」を以下のように書き換えます。
○MainPage.xaml.cs
using System; using System.ServiceModel; using System.Windows; using System.Windows.Controls; namespace SilverlightServiceSample { public partial class MainPage : UserControl { private readonly ChannelFactory<IService1Async> _channel = null; private readonly IService1Async _service = null; public MainPage() { InitializeComponent(); this.Loaded += MainPage_Loaded; try { // チャネルファクトリの作成 _channel = new ChannelFactory<IService1Async>( new BasicHttpBinding(), new EndpointAddress("http://localhost:65150/Service1.svc")); if (_channel == null) throw new Exception("チャネルファクトリの作成に失敗。"); // チャネルの取得 _service = _channel.CreateChannel(); if (_service == null) throw new Exception("チャネルの作成に失敗。"); } catch (Exception ex) { MessageBox.Show(ex.Message); } } private void MainPage_Loaded(object sender, RoutedEventArgs e) { try { // サービスを呼び出す var asyncResult = _service.BeginGetData(5, async => { Deployment.Current.Dispatcher.BeginInvoke(() => { try { // 戻り値を取得する var result = ((IService1Async)async.AsyncState).EndGetData(async); MessageBox.Show(result, "ExecuteService", MessageBoxButton.OK); } catch (Exception ex) { // エラーメッセージを取得 MessageBox.Show(ex.Message); } }); }, _service); } catch (Exception ex) { MessageBox.Show(ex.Message); } } } }
ポイントとなるのは、ChannelFactory の作成時に指定する ジェネリクスに「IService1Async」を指定します。
また、サービス呼び出しにはスレッドプールが使用されるため、End メソッドを呼び出す際には必ずメインスレッド上で実行するようにしてください。
これ以外にも、APM を利用する際には次のことに気を配ってください。
○常に End メソッドを呼び出し、且つ必ず一度だけ呼び出す
End メソッドを呼び出さなかった場合、リソースがリークします。CLR は非同期処理の開始時に内部リソースを割り当てますが、処理が完了しても CLR は End メソッドが呼び出されるまでこれらのリソースを保持します。
End メソッドが呼び出されなければ、これらのリソースは割り当てられたままとなり、プロセスの終了時にのみ解放されます。
また、非同期処理を開始するとき、実際には処理が最終的に成功するのか失敗するのかはわかりません。
これを調べる唯一の方法が、End メソッドを呼び出してその戻り値を検査する。または例外がスローされるかを調べる方法です。
次に、非同期処理に対して複数回 End メソッドを呼び出してはいけません。
End メソッドを呼び出すと、内部のリソースにアクセスしてそれらを解放する可能性があります。リソースが解放された場合に再度 End メソッドを呼び出すと、リソースは既に解放されているため結果は予測不能になります。
○ End メソッドの呼び出し時には、常に Begin メソッドで使用したオブジェクトのみを使用する
Begin メソッドの呼び出しにどのようなオブジェクトを使用する場合でも、End メソッドの呼び出しには Begin メソッドの呼び出しと同じオブジェクトを使用してください。
IAsyncResult オブジェクトは内部的にサービスを呼ぶための BeginInvoke の呼び出しで使用された元のオブジェクトへの参照を保持しており、一致しない場合に EndInvoke が InvalidOperationException をスローします。
○ Begin メソッドと End メソッドで ref、out、params 引数を使用する
Begin メソッドと End メソッドのパラメータは、非同期でないメソッドが out もしくは ref パラメータを使用しているか、params キーワードで修飾されるメソッドを持っている場合には、呼び出す方法が異なります。
このような呼び出しは避けることをお勧めします。
以上で、Silverlight から ChannelFactory を使用してサービスを呼び出すことができます。
非同期呼び出しを行う場合には多くの手順を踏む必要がありますが、共通化できる部分も多いので慣れてくるとそれほど手間はかからないと思います。
コメントを残す
コメントを投稿するにはログインしてください。