手始めに Silverlight+WCFの組み合わせで実験
Silverlight+PHP(NuSOAP)の組み合わせを試す前に、Silverlight+WCFを試さないといけない。
ので、試してみた結果をメモ書き。
■最初はWindowsフォーム+WCFで
この手のお試しは確実なところからやっていくのが良いので、手堅くWindowsフォームからWCFを使えるかどうかの実験からスタートします。
1.WCFサービスライブラリのプロジェクト作成
2.初期状態で「Service1」と「IService1」が作成されるので名前を変える。
ここでは「SampleService」、「ISampleService」と変更。
名前の置換は、Visual Studio 2008のリファクタリングの機能を使うと楽。
3.インターフェース(ISampleService.cs)はこんな感じ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | [ServiceContract] public interface ISampleService { [OperationContract] string GetData( int value); [OperationContract] CompositeType GetDataUsingDataContract(CompositeType composite); // タスク: ここにサービス操作を追加します。 [OperationContract] Product GetProduct( int id); [OperationContract] IList<Product> GetAllProducts(); } |
・サービスを追加するときは「OperationContract」属性を付ければOK。
・取得する場合は、戻り値にする(refが使えるかどうかは不明)
・リストで取りたいときはコレクションを戻り値にする。
ここで Product というクラス構造を受け渡しているので、定義する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | [DataContract] public class Product { private int _id; private string _name; private decimal _price; [DataMember] public int ID { get { return _id; } set { _id = value; } } [DataMember] public string Name { get { return _name; } set { _name = value; } } [DataMember] public decimal Price { get { return _price; } set { _price = value; } } } |
・クラスは DataContract 属性を付ける。
・プロパティは DataMember 属性を付ける。
# 実は、Silverlight の DataGrid で扱いやすいように ObservableCollection を使う予定なのだが、
# 今回は試しなのでこのまま。
これでインターフェースはできたので、サービスメソッドの実装に入る。
4.サービスの実装(SampleService.svc.cs)を行う。
先のインターフェースに従ってメソッドの内容を書く。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | IList<Product> m_products; public SampleService() { m_products = new List<Product>(); m_products.Add( new Product() { ID=1, Name= "Visual Studio 20X0" , Price=40000 }); m_products.Add( new Product() { ID=2, Name= "SQL Server 20X0" , Price=50000 }); m_products.Add( new Product() { ID=3, Name= "Expression Blend X" , Price=60000 }); } public Product GetProduct( int id) { var q = from p in m_products where p.ID == id select p; if ( q.Count<Product>() == 0 ) { return null ; } else { return q.First<Product>(); } } public IList<Product> GetAllProducts() { return m_products; } |
・本来はデータベースから拾ってくるのだが、今回は内部にデータを持ったままテスト。
・GetProduct メソッドは id を渡すと Product オブジェクトを返す。
・GetAllProducts メソッドはコレクションを返す。
大抵のパターンはこれだけで OK。
さて、普通はこれで十分なのだろうが、環境が悪いのか、そのままだと Windows フォームで参照するときに「ASP.NET互換ではない」というエラーが出る。よくわからないが、↓のように AspNetCompatibilityRequirements を付けると通るようになる。
1 2 3 4 5 6 7 8 9 | namespace SampleSL2WCF { [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] public class SampleService : ISampleService { ... } } |
■Windowsフォームの場合
WindowsフォームからWCFを呼び出すときは、
1.サービス参照で、先のWCFサービスを参照する。
名前空間を「SampleServiceReference」と指定した。
2.この状態でバインドができているので、サービスの呼び出しは簡単。
1 2 3 4 5 6 7 8 9 | // サービスのクライアントを作成 SampleServiceClient client = new SampleServiceClient(); // オープン client.Open(); // メソッド呼び出し Product pro = client.GetProduct(id); // クローズ client.Close(); |
3.先ほど、使ったインターフェース(ISampleService)のメソッドを使う。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | using ClientWindowForm.SampleServiceReference; namespace ClientWindowForm { public partial class Form1 : Form { public Form1() { InitializeComponent(); } // IDを指定して検索 private void button1_Click( object sender, EventArgs e) { int id = int .Parse(textBox1.Text); // サービス呼び出し SampleServiceClient client = new SampleServiceClient(); client.Open(); Product pro = client.GetProduct(id); client.Close(); // データグリッドに表示1 List<Product> products = new List<Product>(); products.Add(pro); this .dataGridView1.DataSource = products; } // すべてのデータを取得 private void button2_Click( object sender, EventArgs e) { // サービス呼び出し SampleServiceClient client = new SampleServiceClient(); client.Open(); IList<Product> products = client.GetAllProducts(); client.Close(); // データグリッドに表示1 this .dataGridView1.DataSource = products; } } } |
これをビルドして実行すると、Windowsフォーム+WCFのできあがり。
■次はWPF+WCFの組み合わせ
SilverlightはWPFのサブセットなので、WPFの文法を覚えておくとSilverlightでも応用が利く。
が、サブセットゆえに、違い/落とし穴があるのが難点。
違い/落とし穴を示すために WPF+WCF でも動作させてみる。
1.Windowsフォームと同様にサービス参照でインタフェースを作る。
2.WPFにデータグリッドを付けたウィンドウを作る。
1 2 3 4 5 6 7 8 9 10 11 | < Window x:Class = "ClientWPF.Window1" xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns = "http://schemas.microsoft.com/winfx/2006/xaml" Title = "Window1" Height = "310" Width = "517" > < Grid > < Button Height = "23" HorizontalAlignment = "Left" Margin = "12,12,0,0" Name = "button1" VerticalAlignment = "Top" Width = "75" Click = "button1_Click" >ID指定</ Button > < Button HorizontalAlignment = "Left" Margin = "12,0,0,178" Name = "button2" Width = "75" Height = "23" VerticalAlignment = "Bottom" Click = "button2_Click" >すべて</ Button > < TextBox Margin = "12,41,0,0" Name = "textBox1" Height = "24" HorizontalAlignment = "Left" VerticalAlignment = "Top" Width = "75" >2</ TextBox > < my:DataGrid AutoGenerateColumns = "True" Margin = "93,12,12,12" Name = "dataGrid1" xmlns:my = "clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit" /> </ Grid > </ Window > |
※DataGridのAutoGenerateColumns属性が True になっているのは、初期値の False だとグリッドが正常に表示されないため。
おそらくデータのコレクションが間違っている(IListじゃだめ?)からだと思うのだが。
3.ボタンイベントを記述する。
記述の仕方は、Windows フォームとほぼ同じ。
データグリッドにバインドするときは「ItemsSourceプロパティ」を使う。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | using ClientWPF.SampleServiceReference; namespace ClientWPF { /// <summary> /// Window1.xaml の相互作用ロジック /// </summary> public partial class Window1 : Window { public Window1() { InitializeComponent(); } // IDを指定して検索 private void button1_Click( object sender, RoutedEventArgs e) { int id = int .Parse(textBox1.Text); SampleServiceClient client = new SampleServiceClient(); client.Open(); Product pro = client.GetProduct(id); client.Close(); List<Product> products = new List<Product>(); products.Add(pro); this .dataGrid1.ItemsSource = products; } // すべてのデータを取得 private void button2_Click( object sender, RoutedEventArgs e) { SampleServiceClient client = new SampleServiceClient(); client.Open(); IList<Product> products = client.GetAllProducts(); client.Close(); this .dataGrid1.ItemsSource = products; } } } |
これをビルドして動かすと問題なく動作する。OK。
■いよいよ、Silverlight+WCFの組み合わせを試す
1.Windowsフォームと同様にサービス参照でインタフェースを作る。
※このときに、WCFのデフォルトのバインディング(binding)が「wsHttpBinding」のままだと通らない。
※ので、basicHttpBinding に変更する。
※Silverlight 2 の場合は「basicHttpBinding」のみ有効、という記述があるのだが、Silverlight 3 の記述はない。
※どれが正しいのか不明。
WCFのweb.configを以下のように書きかえる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | < system.serviceModel > < services > < service behaviorConfiguration = "SampleSL2WCF.Service1Behavior" name = "SampleSL2WCF.SampleService" > <!-- <endpoint address="" binding="wsHttpBinding" contract="SampleSL2WCF.ISampleService"> --> < endpoint address = "" binding = "basicHttpBinding" contract = "SampleSL2WCF.ISampleService" > < identity > < dns value = "localhost" /> </ identity > </ endpoint > < endpoint address = "mex" binding = "mexHttpBinding" contract = "IMetadataExchange" /> </ service > </ services > < behaviors > < serviceBehaviors > < behavior name = "SampleSL2WCF.Service1Behavior" > <!-- メタデータ情報の開示を避けるには、展開する前に、下の値を false に設定し、上のメタデータのエンドポイントを削除します --> < serviceMetadata httpGetEnabled = "true" /> <!-- デバッグ目的で障害発生時の例外の詳細を受け取るには、下の値を true に設定します。例外情報の開示を避けるには、展開する前に false に設定します --> < serviceDebug includeExceptionDetailInFaults = "false" /> </ behavior > </ serviceBehaviors > </ behaviors > < serviceHostingEnvironment aspNetCompatibilityEnabled = "true" /></ system.serviceModel > </ configuration > |
# まぁ、考えてみれば、インターネット越しで Windows 認証を操ることはほぼ無いので、
# ベーシック認証にしておく(認証なし)にするのが良かろう。
2.Silverlightにデータグリッドを付けたウィンドウを作る。
このあたりは、WPF とほぼ同じ。
1 2 3 4 5 6 7 8 9 10 11 12 | < UserControl xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml" xmlns:data = "clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data" x:Class = "ClientSilverlight.Page" Width = "400" Height = "300" > < Grid x:Name = "LayoutRoot" Background = "White" > < Button x:Name = "button1" HorizontalAlignment = "Left" Margin = "8,8,0,0" VerticalAlignment = "Top" Width = "75" Content = "ID指定" Click = "button1_Click" /> < TextBox x:Name = "textBox1" HorizontalAlignment = "Left" Margin = "9,34,0,0" VerticalAlignment = "Top" Width = "74" Text = "2" TextWrapping = "Wrap" /> < Button x:Name = "button2" HorizontalAlignment = "Left" Margin = "9,62,0,0" VerticalAlignment = "Top" Width = "75" Content = "すべて" Click = "button2_Click" /> < data:DataGrid Margin = "88,8,8,8" Name = "dataGrid1" /> </ Grid > </ UserControl > |
3.ボタンのイベントを記述する。
ここが、WindowsフォームやWPFと大きく異なる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | using ClientSilverlight.SampleServiceReference; namespace ClientSilverlight { public partial class Page : UserControl { public Page() { InitializeComponent(); } // IDで検索 private void button1_Click( object sender, RoutedEventArgs e) { int id = int .Parse(textBox1.Text); // サービスの作成 SampleServiceClient client = new SampleServiceClient(); client.GetProductCompleted += new EventHandler<GetProductCompletedEventArgs>(client_GetProductCompleted); client.GetProductAsync(id); } // 実行後のコールバック void client_GetProductCompleted( object sender, GetProductCompletedEventArgs e) { Product pro = e.Result; List<Product> products = new List<Product>(); products.Add(pro); this .dataGrid1.ItemsSource = products; } // すべてを取得 private void button2_Click( object sender, RoutedEventArgs e) { SampleServiceClient client = new SampleServiceClient(); client.GetAllProductsCompleted += new EventHandler<GetAllProductsCompletedEventArgs>(client_GetAllProductsCompleted); client.GetAllProductsAsync(); } // 実行後のコールバック void client_GetAllProductsCompleted( object sender, GetAllProductsCompletedEventArgs e) { IList<Product> products = e.Result; this .dataGrid1.ItemsSource = products; } } } |
コードを見ると分かる通り、
・サービスの呼び出し
・サービスの完了通知
の2つのメソッドが必要になる。
WCFを非同期で呼び出すために完了を待つ仕組みなのだが、、、これが結構うざったい。
ひとつのメソッドにまとめるならば、ラムダ式を使えばよいのだが、、、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | private void button3_Click( object sender, RoutedEventArgs e) { int id = int .Parse(textBox1.Text); SampleServiceClient client = new SampleServiceClient(); client.GetProductCompleted += new EventHandler<GetProductCompletedEventArgs>( (s,ee) => { Product pro = ee.Result; List<Product> products = new List<Product>(); products.Add(pro); this .dataGrid1.ItemsSource = products; }); client.GetProductAsync(id); } |
これは根本的な解決にはなっていない。
なぜならば、SilverlightでWCFを扱うときは非同期呼び出ししかないので「完了待ちをしている間、ユーザが何かアクションを起こさないようにブロックする必要がある」ということだ。これは面倒。
「SilverlightはWebアプリだから非同期であって~」の説明をちらほら見掛けるが、騙されてはいけない。WindowsフォームやWPFの場合は同じWCFを使っているのに、同期メソッドとして扱っている。これは、SiverlightがWebアプリかどうかという話ではなくて、Silverlightが扱うWCFのサブセットに「同期メソッドが抜けている」と言ったほうが良い。何故、抜けているのかは不明。
さて、これをビルドして動かそうとすると
「ドメイン間のポリシーが見つかりません」
と言われて動かない。
Silverlight 2 と WCF を使用したサービス駆動型アプリケーション
http://msdn.microsoft.com/ja-jp/magazine/cc794260.aspx
いわゆるクロスドメインの問題なので、WCFを公開するときに ClientAccessPolicy.xml というファイルを用意する。
# クロスドメインのブロックは、サーバーで行われるのではなくて Silverlight 側で自主的に行っている。
# つまり、、、今後「自主的に行わなくなる」という方向も考えられる。これは問題あり。
このファイルなのだが、ドメインのルートに置かないといけない。
たとえば、http://moonmile.net/ClientAccessPolicy.xml のように置く。
つまり、
・http://moonmile.net/~masuda/
・http://moonmile.net/masuda/
のようなレンタルホストの場合は Silverlight で使う WCF は公開できないということになる(レンタルサーバー会社で ClientAccessPolicy.xml を置けば別だが)。
また、他のサイトにあるサービスも簡単には使えない。先のエントリでも書いたが、別途プロキシを作る必要がある。
一番制限の緩い(いけない)設定はこんな感じになる。
ClientAccessPolicy.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 | <? 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 > |
SOAPActionのヘッダに対して、どこのドメインからでもアクセスが可能。
サービスを提供するフォルダも自由(かな?)。
というわけで、Visual Studio上でWCFを公開するとポートは変わるし、先のポリシーファイルを置かないと駄目だし、ということで面倒なので、IIS 7.0 の仮想フォルダを使って設定する。
すると、OK。うまく SilverlightからWCFに接続ができる。
■パケットを見る
Siverlight+PHPの組み合わせを作るためにパケットを覗いてみよう。
Microsoft Network Monitor 3.3
http://www.microsoft.com/downloads/details.aspx?FamilyID=983b941d-06cb-4658-b7f6-3088333d062f&displaylang=en
を使うとパケットが見れる。
microsoftのサイトでダウンロードできるのは英語版のみだが、日本語化もできるそうだ。
Microsoft Network Monitor 3.3 の日本語化
http://d.hatena.ne.jp/wwwcfe/20090824/microsoftnetworkmonitor
Microsoft Network Monitor 3.3 日本語化パッチ
http://applications.web.fc2.com/j10n/networkmonitor.html
フィルタに
tcp.Port == 80
を設定してパケットをキャプチャする。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | POST /SampleSL2WCF/SampleService.svc HTTP/1.1 Accept: */* Content-Length: 157 Content-Type: text/xml; charset=utf-8 SOAPAction: http://tempuri.org/ISampleService/GetProduct Accept-Encoding: gzip, deflate User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; Trident/4.0; SLCC1; .NET CLR 2.0.50727; Media Center PC 5.0; .NET CLR 3.5.30729; .NET CLR 3.0.30618) Host: iomante-pc Connection: Keep-Alive Cache-Control: no-cache <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" > <s:Body> <GetProduct xmlns="http://tempuri.org/"> <id>2</id> </GetProduct> </s:Body> </s:Envelope> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | HTTP/1.1 200 OK Cache-Control: private Content-Type: text/xml; charset=utf-8 Server: Microsoft-IIS/7.0 X-AspNet-Version: 2.0.50727 X-Powered-By: ASP.NET Date: Fri, 16 Oct 2009 06:55:12 GMT Content-Length: 385 <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"> <s:Body> <GetProductResponse xmlns="http://tempuri.org/"> <GetProductResult xmlns:a="http://schemas.datacontract.org/2004/07/SampleSL2WCF" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"> <a:ID>2</a:ID> <a:Name>SQL Server 20X0</a:Name> <a:Price>50000</a:Price> </GetProductResult> </GetProductResponse> </s:Body> </s:Envelope> |
中身が読みやすい SOAP になっているのが分かる(実際は xml の部分は1行で書かれている)。
なので、Siverlight+PHPを作る場合は、
・サービス参照をするための WSDL を作成
・SOAP で応答を返す
だけで良いので、NuSOAP が結構使えるのではないか、と期待できる。
続きは後日。