手始めに 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 が結構使えるのではないか、と期待できる。
続きは後日。
