手始めに Silverlight+WCFの組み合わせで実験
Silverlight+PHP(NuSOAP)の組み合わせを試す前に、Silverlight+WCFを試さないといけない。
ので、試してみた結果をメモ書き。
■最初はWindowsフォーム+WCFで
この手のお試しは確実なところからやっていくのが良いので、手堅くWindowsフォームからWCFを使えるかどうかの実験からスタートします。
1.WCFサービスライブラリのプロジェクト作成
2.初期状態で「Service1」と「IService1」が作成されるので名前を変える。
ここでは「SampleService」、「ISampleService」と変更。
名前の置換は、Visual Studio 2008のリファクタリングの機能を使うと楽。
3.インターフェース(ISampleService.cs)はこんな感じ
[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 というクラス構造を受け渡しているので、定義する。
[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)を行う。
先のインターフェースに従ってメソッドの内容を書く。
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 を付けると通るようになる。
namespace SampleSL2WCF { [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] public class SampleService : ISampleService { ... } }
■Windowsフォームの場合
WindowsフォームからWCFを呼び出すときは、
1.サービス参照で、先のWCFサービスを参照する。
名前空間を「SampleServiceReference」と指定した。
2.この状態でバインドができているので、サービスの呼び出しは簡単。
// サービスのクライアントを作成 SampleServiceClient client = new SampleServiceClient(); // オープン client.Open(); // メソッド呼び出し Product pro = client.GetProduct(id); // クローズ client.Close();
3.先ほど、使ったインターフェース(ISampleService)のメソッドを使う。
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にデータグリッドを付けたウィンドウを作る。
<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プロパティ」を使う。
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を以下のように書きかえる。
<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 とほぼ同じ。
<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と大きく異なる。
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を非同期で呼び出すために完了を待つ仕組みなのだが、、、これが結構うざったい。
ひとつのメソッドにまとめるならば、ラムダ式を使えばよいのだが、、、
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
<?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
を設定してパケットをキャプチャする。
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>
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 が結構使えるのではないか、と期待できる。
続きは後日。