手始めに 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=&quot;Visual Studio 20X0&quot;, Price=40000 });
            m_products.Add( new Product() { ID=2, Name=&quot;SQL Server 20X0&quot;, Price=50000 });
            m_products.Add( new Product() { ID=3, Name=&quot;Expression Blend X&quot;, 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=&quot;ClientWPF.Window1&quot;
    xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
    xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
    Title=&quot;Window1&quot; Height=&quot;310&quot; Width=&quot;517&quot;>
    <Grid>
        <Button Height=&quot;23&quot; HorizontalAlignment=&quot;Left&quot; Margin=&quot;12,12,0,0&quot; Name=&quot;button1&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;75&quot; Click=&quot;button1_Click&quot;>ID指定</Button>
        <Button HorizontalAlignment=&quot;Left&quot; Margin=&quot;12,0,0,178&quot; Name=&quot;button2&quot; Width=&quot;75&quot; Height=&quot;23&quot; VerticalAlignment=&quot;Bottom&quot; Click=&quot;button2_Click&quot;>すべて</Button>
        <TextBox Margin=&quot;12,41,0,0&quot; Name=&quot;textBox1&quot; Height=&quot;24&quot; HorizontalAlignment=&quot;Left&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;75&quot;>2</TextBox>
        <my:DataGrid AutoGenerateColumns=&quot;True&quot; Margin=&quot;93,12,12,12&quot; Name=&quot;dataGrid1&quot; xmlns:my=&quot;clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit&quot; />
    </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=&quot;SampleSL2WCF.Service1Behavior&quot; name=&quot;SampleSL2WCF.SampleService&quot;>
<!--        <endpoint address=&quot;&quot; binding=&quot;wsHttpBinding&quot; contract=&quot;SampleSL2WCF.ISampleService&quot;> -->
          <endpoint address=&quot;&quot; binding=&quot;basicHttpBinding&quot; contract=&quot;SampleSL2WCF.ISampleService&quot;>
            <identity>
            <dns value=&quot;localhost&quot; />
          </identity>
        </endpoint>
        <endpoint address=&quot;mex&quot; binding=&quot;mexHttpBinding&quot; contract=&quot;IMetadataExchange&quot; />
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name=&quot;SampleSL2WCF.Service1Behavior&quot;>
          <!-- メタデータ情報の開示を避けるには、展開する前に、下の値を false に設定し、上のメタデータのエンドポイントを削除します -->
          <serviceMetadata httpGetEnabled=&quot;true&quot; />
          <!-- デバッグ目的で障害発生時の例外の詳細を受け取るには、下の値を true に設定します。例外情報の開示を避けるには、展開する前に false に設定します -->
          <serviceDebug includeExceptionDetailInFaults=&quot;false&quot; />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  <serviceHostingEnvironment aspNetCompatibilityEnabled=&quot;true&quot; /></system.serviceModel>
</configuration>

# まぁ、考えてみれば、インターネット越しで Windows 認証を操ることはほぼ無いので、
# ベーシック認証にしておく(認証なし)にするのが良かろう。

2.Silverlightにデータグリッドを付けたウィンドウを作る。

このあたりは、WPF とほぼ同じ。

<UserControl
    xmlns=&quot;http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
    xmlns:x=&quot;http://schemas.microsoft.com/winfx/2006/xaml&quot;
    xmlns:data=&quot;clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data&quot; x:Class=&quot;ClientSilverlight.Page&quot;
    Width=&quot;400&quot; Height=&quot;300&quot;>
    <Grid x:Name=&quot;LayoutRoot&quot; Background=&quot;White&quot;>
     <Button x:Name=&quot;button1&quot; HorizontalAlignment=&quot;Left&quot; Margin=&quot;8,8,0,0&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;75&quot; Content=&quot;ID指定&quot; Click=&quot;button1_Click&quot;/>
     <TextBox x:Name=&quot;textBox1&quot; HorizontalAlignment=&quot;Left&quot; Margin=&quot;9,34,0,0&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;74&quot; Text=&quot;2&quot; TextWrapping=&quot;Wrap&quot; />
     <Button x:Name=&quot;button2&quot; HorizontalAlignment=&quot;Left&quot; Margin=&quot;9,62,0,0&quot; VerticalAlignment=&quot;Top&quot; Width=&quot;75&quot; Content=&quot;すべて&quot; Click=&quot;button2_Click&quot;/>
     <data:DataGrid Margin=&quot;88,8,8,8&quot; Name=&quot;dataGrid1&quot;/>
    </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=&quot;1.0&quot; encoding=&quot;utf-8&quot;?>
<access-policy>
   <cross-domain-access>
       <policy>
           <allow-from http-request-headers=&quot;SOAPAction&quot;>
               <domain uri=&quot;*&quot;/>
           </allow-from>
           <grant-to>
               <resource path=&quot;/&quot; include-subpaths=&quot;true&quot; />
           </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=&quot;http://schemas.xmlsoap.org/soap/envelope/&quot; >
 <s:Body>
  <GetProduct xmlns=&quot;http://tempuri.org/&quot;>
   <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=&quot;http://schemas.xmlsoap.org/soap/envelope/&quot;>
 <s:Body>
  <GetProductResponse xmlns=&quot;http://tempuri.org/&quot;>
   <GetProductResult xmlns:a=&quot;http://schemas.datacontract.org/2004/07/SampleSL2WCF&quot; xmlns:i=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;>
    <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 が結構使えるのではないか、と期待できる。

続きは後日。

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