手始めに Silverlight+WCFの組み合わせで実験

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

続きは後日。

カテゴリー: 開発 パーマリンク