忍者ブログ

Memeplexes

プログラミング、3DCGとその他いろいろについて

[PR]

×

[PR]上記の広告は3ヶ月以上新規記事投稿のないブログに表示されています。新しい記事を書く事で広告が消えます。



WCFチュートリアル カスタム型を使う

WCFのコントラクトではそのままでは、引数や戻り値にカスタムなクラスを使うことが出来ません。クライアントからサーバーへ独自に定義したクラスを(そのままでは)送れませんし、サーバーからクライアントへ、独自に定義したクラスを(そのままでは)返せないのです。

× ダメなコントラクトの例(マネしないで!) ×
using System.ServiceModel;

namespace Contracts
{
    [ServiceContract]
    public interface ICalculator
    {
        //四角形の面積を計算します
        [OperationContract]
        double GetArea(Rectangle rect);
    }

    //Error!
    //(throws InvalidDataContractException)
    public class Rectangle
    {
        public double Width;
        public double Height;
    }
}


もちろん全くムリということではありません。だいたいInt32だってクラス(構造体)の一つなんですからね。ある型をWCFでの引数や戻り値に使うためには、いくつかの条件のうちの一つを満たしている必要があります。そのうちの最も基本的なのは、System.Serializable属性が付けられていることでしょう。この属性は、Int32、Single(float)、String、DateTimeなど、.netの多くの型につけられています(ですからこれらの型はそのままWCFのコントラクトに使用することが出来ます)

SerializableAttributeをつける

カスタムクラスをWCFで使えるようにする方法のひとつがこれです。つまり、作ったカスタムクラスにSerializableAttributeを付けるのです。SerializableAttributeをつけられた型は、リフレクションによりprivateだろうとpublicだろうとすべてのフィールドがシリアライズされ、ネットの向こう側に送られます(もちろん同じパソコンに送られる場合もありますけどね)。さっきの例を正しくするとこうなります。

コントラクト:
using System.ServiceModel;

namespace Contracts
{
    [ServiceContract]
    public interface ICalculator
    {
        //四角形の面積を計算します
        [OperationContract]
        double GetArea(Rectangle rect);
    }

    [System.Serializable]
    public class Rectangle
    {
        public double Width;
        public double Height;
    }
}

せっかくなのでサーバーとクライアントも書いておきます。サーバーのプロジェクトもクライアントのプロジェクトも、このコントラクトのプロジェクトを参照しています。

サーバー:
using System;
using System.ServiceModel;

using Contracts;

namespace Server
{
    class MyCalculator : ICalculator
    {
        public double GetArea(Rectangle rect)
        {
            return rect.Width * rect.Height;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            ServiceHost serviceHost = new ServiceHost(typeof(MyCalculator));
            serviceHost.AddServiceEndpoint(
                typeof(ICalculator),
                new NetTcpBinding(),
                "net.tcp://localhost:8001/Calculator"
                );
            serviceHost.Open();

            Console.WriteLine("サービスがオープンしました。終了するにはEnterを押してください。");
            Console.ReadLine();

            serviceHost.Close();
        }
    }
}




クライアント:
using System;
using System.ServiceModel;
using System.ServiceModel.Channels;

using Contracts;

namespace Client
{
    class Program
    {
        static void Main(string[] args)
        {
            ICalculator remoteService = new ChannelFactory(
                new NetTcpBinding(),
                "net.tcp://localhost:8001/Calculator"
                ).CreateChannel();

            Console.WriteLine(
                "2 * 3 = "
                + remoteService.GetArea(new Rectangle{Width = 2, Height = 3})
                );

            ((IChannel)remoteService).Close();
        }
    }
}

実行するとクライアント側で"2 * 3 = 6"というふうに表示されるはずです。Rectangleオブジェクトがバイト列にシリアライズされ、サーバープログラムに送られているわけですね。

(Serializable属性を使う場合に限らず、このようにしてサービスとクライアントの間を飛び交うオブジェクトはちょうどC言語の構造体のようなものになります。つまり、メソッドを持ちませんし、値がそのままコピーされるのです。メソッドを持たないというのはどういうことかというと、データだけだということです。これは「データとその操作を一緒にまとめる」というオブジェクト指向に反しているように見えますが、WCFのサービスコントラクトが使う型はメソッドを持っていても仕方が無いですからね。また、サーバークライアント間を移動するときにデータが反対側でシリアライズされるという方法を取るので、片方のオブジェクトの値を変更してももう片方のオブジェクトには影響を与えません。参照型ではなくて値型みたいなもんなんですね。)


DataContractAttributeとDataMemberAttributeをつける

しかしこのSerializableAttributeを使う方法はWCFとしてはあまり勧められないのだそうです。WCFのうたうサービス志向にのっとっていないからでしょう。コントラクトはなるべく明示的であるべきということでしょうか・・・。それにまあ、SerializableAttributeはWCFより以前からあった属性ですしね。

WCF特有の方法には、DataContractAttributeDataMenberAttributeを使う方法があります。DataContractは型に付け、DataMemberはそのフィールドまたはプロパティに付けます。この2つは両方ともSystem.Runtime.Serialization名前空間内にあり、使うにはSystem.Runtime.Serialization.dllの参照を加える必要があります(意外ですね・・・・・・)。

さっきのSerializable属性を使った方法をこの方法で書き換えるとこうなります:

using System.ServiceModel;

namespace Contracts
{
    [ServiceContract]
    public interface ICalculator
    {
        //四角形の面積を計算します
        [OperationContract]
        double GetArea(Rectangle rect);
    }

    [System.Runtime.Serialization.DataContract]
    public class Rectangle
    {
        [System.Runtime.Serialization.DataMember]
        public double Width;

        [System.Runtime.Serialization.DataMember]
        public double Height;
    }
}


さっきの方法と違うのはコントラクトだけです。サーバーとクライアントはそのまま。















拍手[0回]


WCFチュートリアル かんたんなサーバーとクライアント

ちょっとWCFでデッドロック地獄に陥ったりふんだりけったりなので、自分の頭の中を整理するためにWCFのチュートリアルみたいなのを書いてみたいと思います。

あらかじめ言い訳をしておくと、僕がWCF関連の技術で使ったことがあるのは.Net Remotingだけで、それも数年前にちょっと触った程度です。ですから・・・たぶん勘違いところがあるかもしれませんのでそのときは教えてくださいね!

このブログでは主にXNAなどゲーム関連の技術について書いてきたので、WCFもそうなのかと思う人も居るかもしれません。僕が見た感じでは使える場合もあるし使えない場合もあるといったところでしょうか。WCFによる通信は単にTcpで行った場合よりもパフォーマンスは悪いはずなので、FPSのネット対戦にはまあ、向かないでしょう。しかし少々通信に時間がかかってもいいようなボードゲームなんかには向いているといえるかもしれません。まあその場合はXNAを使う必要はなく、WPFを使うことになるでしょうけどね。


WCFとは
WCF(Windows Communication Foundation)とは.Net Framework3.0の三本柱(WPF、WCF、WF)の一つで、サービス志向に基づいたアプリケーションを作るフレームワークです。要はネットワークを通じて別々のコンピュータが通信を行うことが出来るわけです。なんだかゲームに使えそうですね!

以前にも似たような技術として.Net Remotingがあったのですが、どうやら問題があるとか何とかでWCFがでたようです。.Net Remotingに無かったもの、つまり.Net RemotingとWCFの違いは、サービス志向とやらなのだそうです。サービス志向とはカプセル化を極めまくったものらしく、これでプログラムを書くと、通信に使うインターフェースをプログラマは強く意識することになります(.net Remotingではどれがリモートのオブジェクトなのかということはほとんど意識する必要はありませんでした)

カプセル化を行うと普通は物事をあまり気にしなくていいようになるものですが、サービス志向の場合は違って、サーバー側が内部で.Net Frameworkを使っているということまで外部から隠さなければならないため、通信に使うインターフェースは限られた機能しか持たないようなものになり、結果としてプログラマはどれがリモートオブジェクトなのかを強く意識しなければいけなくなるようです。通信相手はC#じゃなくてJavaかもしれないということですね。例えば.Net Remotingでは何がローカルにあるオブジェクトで何がリモートにあるオブジェクトなのか気にする必要はありませんでしたが、WCFではインターフェースに属性を付けて、「これを使ってリモートオブジェクトと通信するんだ」ということを明示しなければいけません。どこでリモートと通信するのかを意識することによって無駄な通信を避けるという意味合いもあるそうです。

また、サーバーの内部を隠蔽、ということはメソッドの引数や戻り値に(そのままでは)独自のクラスを用いることが出来ません。クライアント側が使わないような型の引数や戻り値を使われては困るので、カスタムクラスを使う場合は、属性を使って明示しなければいけないのです。

ようするに、WCFではこれまでと違って、通信の境界面に注意を払うということですね。



簡単な例

思うにWCFには、まぁ他の技術もそうですが、簡単なソースコードの実例がかけています。というわけでここではWCFを使って足し算をするサービスを作って見ます。本当なら「このコンピューターを足し算するサーバーにして、あのコンピュータから2 + 3の計算をリクエストする」見たいなことをやるべきなのでしょうが、ここでは話を簡単にするために1台のパソコンで出来るようにしましょう。

サーバープログラムと、それにアクセスするクライアントプログラムの2つを作るので、2つプロジェクトを作るといいでしょう。プロジェクトを作ったらその両方に、Project -> Add ReferenceでSystem.ServiceModel.dllを追加します。WCFを使うにはこれが必要です。

AddingWCFComponent.jpg

コードはこんな具合です。
サーバー側
using System;
using System.ServiceModel;

namespace Server
{
    [ServiceContract]
    interface ICalculator
    {
        [OperationContract]
        int Add(int a, int b);
    }

    class MyCalculator : ICalculator
    {
        public int Add(int a, int b)
        {
            return a + b;
        }
    }

    class Program
    {
        static void Main()
        {
            ServiceHost serviceHost = new ServiceHost(typeof(MyCalculator));
            serviceHost.AddServiceEndpoint(
                typeof(ICalculator),
                new NetTcpBinding(),
                "net.tcp://localhost:8001/Calculator"
                );
            serviceHost.Open();

            Console.WriteLine("サービスがオープンしました。終了するにはEnterを押してください。");
            Console.ReadLine();

            serviceHost.Close();
        }
    }
}

クライアント側
using System;
using System.ServiceModel;
using System.ServiceModel.Channels;

namespace Client
{
    [ServiceContract]
    interface ICalculator
    {
        [OperationContract]
        int Add(int a, int b);
    }


    class Program
    {
        static void Main()
        {
            ICalculator remoteCalculator = new ChannelFactory<ICalculator>(
                new NetTcpBinding(),
                "net.tcp://localhost:8001/Calculator"
                ).CreateChannel();

            Console.WriteLine("2 + 3 = " + remoteCalculator.Add(2, 3));

            ((IChannel)remoteCalculator).Close();
        }
    }
}


この2つをビルドしたら、Visual Studioのプロジェクトのフォルダから、まずサーバーのほうの実行ファイルを起動します。サービスがオープンすると"サービスがオープンしました。終了するにはEnterを押してください"と表示されるので、今度はクライアントのほうの実行ファイルを起動します。そうすると、"2 + 3 = 5"と表示されるはずです。

ここでは、サーバー側でもクライアント側でもICalculatorインターフェースを宣言しています。そしてそれやそのメソッドに属性を付けています。インターフェースにはServiceContract属性を、メソッドにはOperationContract属性を付けています。これらの属性を付けられると、WCFで通信に使う約束事、コントラクトを意味するようになります。これらの属性が付けられたもの(つまりコントラクト)以外は、外部から隠蔽されるわけです。ですからOperationContractを付け忘れたりすると、そのメソッドをWCFで呼び出すことは出来なくなります。

ここではこのコントラクトのインターフェース(ICalculator)をサーバーとクライアントの両方で、合わせて2度も定義しています。これをすると、厳密に言うと2つのICalculatorの型はCLR的には違うものとなるのですが、サービス志向の疎結合っぷりのおかげでWCFでは問題なく通信できます。ただ、「同じコードを2度も書くのは面倒だ」ということで、ICalculatorを別のファイルで一回だけ定義して1つのdllにして、それをサーバーとクライアント両方からコンパイル時に参照するという方法も使えます。
 

System.ServiceModel.ServiceContractAttribute属性
 これがくっつけられたインターフェースはWCFのサービスであることを表します。

System.ServiceModel.OperationContractAttribute属性
 これがくっつけられたメソッドがサービスの操作であることを表します。

System.ServiceModel.ServiceHostクラス
 サービスを公開するのに使います。ミニサーバーみたいなものです。これを使わなくてもIISやWASを使えばサービスは公開できますが、このクラスを使うとプログラムから制御できますからね。
ここで使ったコンストラクタはこれです:
public ServiceHost
(
    Type serviceType,
    params Uri[] baseAddresses
)

serviceTypeは公開するサービスを実装したクラスのタイプです。
baseAddressesはここでは使いませんでしたが、AddServiceEndpointのaddressのベースとなるアドレスです。たとえばここに"net.tcp://localhost:8001/"を入れると、AddServiceEndpointのaddressは"Calculator"だけでよくなります。

これを呼んだ後に、serviceTypeによって実装されたサービスを公開するAddServiceEndpointメソッドを呼びます。
public ServiceEndpoint AddServiceEndpoint
(
    Type implementedContract,
    Binding binding,
    string address
)

implementedContractはサービスのコントラクトを表すインターフェースのタイプです。
bindingは通信手段です。ここではゲームに向いてそうなTcpを使いましたが、HttpやMsmqを使う方法もあります。
addressはサービスを公開するアドレスです。

実際にサービスを開始するにはOpenメソッドを呼び、使い終わったらCloseメソッドを呼びます。

System.ServiceModel.ChannelFactory<TChannel>ジェネリッククラス
 クライアント側のプログラムで使う、プロキシオブジェクトを生成するクラスです。サービスを使おうと思ったらこのクラスを使います。(TChannelはサーバー側で実装したコントラクトを表すインターフェースです)

コンストラクタ:
public ChannelFactory
(
    Binding binding,
    string remoteAddress
)

bindingは通信手段です。サーバー側でセットしたものと同じものを指定します。
remoteAddressはアクセスするサービスのアドレスです。これまたサーバー側でセットしたものと同じものを指定します。

インスタンスを生成したら、CreateChannelメソッドでプロキシを生成できます。
public TChannel CreateChannel()

この戻り値のオブジェクトはSystem.ServiceModel.Channels.IChannelインターフェースも実装しています。
プロキシを使い終わったら、キャストしてIChannel.Closeメソッドを呼びます。




拍手[6回]


WPF + WCFでタイムアウト(TimeoutException)

先日WPFとWCFで遊んでいたらこれにはまったのでメモしておきます。

WCFのサーバーをコンソールアプリケーションで作り、クライアントをWPFで作っていたのですが、クライアントからサービスオブジェクトのメソッドを呼ぶと固まって、System.TimeoutExceptionがスローされてしまうのです(WCFでは、デフォルトで1分以内に向こう側から呼ばれたメソッドの処理が終わらないとTimeoutExceptionがスローされます)

実のところこの原因は難しいことではなく、WCFのコールバックオブジェクトのイベント内(UIとは別スレッド)でWPFのButton.Contentを直接変更しようとしたために起きた問題で、Button.Dispatcher.BeginInvokeメソッドで簡単に解決する問題でした。

そうですそうです確かこれはWindows.Formsでも同じことをやった記憶があります。全く進歩がありません。こんなことをブログに書くのは恥ずかしいのですが、ある程度の範囲内なら、より痛い目にあえばより記憶が強力になるような気がしなくもないですからね。

コード

サーバー側のコードには問題がなかったのでクライアント側のコードだけを書きます(恥さらしは少ない方がいい!!)

が、その前にどんな事をしようとしていたのかを簡単に説明しておきましょう。作ろうとしていたのは簡単なゲームで、プレイヤーは2人、それぞれのプレイヤーには最初に20点のライフと自分自身のウィンドウが与えられ、プレイヤーは自分のウィンドウのボタンを押すことで相手のライフを3減らすことができます。自分のライフが0になったら負けなので、ボタンをなるべく連打して相手のライフを早く0にしようとします。遊ぶ時にはそれぞれのプレイヤーは別々のパソコンを使うことになるのでしょう(そのためのWCFです)。そして、ボタンのクリックの速さを競うわけです。

ゲームとしては全然面白くない気がしますが、WCFの練習ならこういうのもいいんじゃないのかなぁと言い訳をしておきます。WPFとWCFに慣れたらもうちょっとフクザツにできるはずですからね。もしかしたらMagic : The Gatheringみたいにできるかもしれません。

で、問題のコードですが、これでした:

player.LifeChanged += delegate
{
    this.YourLifeButton.Content = player.GetLife();
};

playerというのはプレイヤーを表すオブジェクトで、サーバーからのコールバックを受け付けます。WCFではクライアントからサーバーオブジェクトのメソッドを呼び出すことが多いのですが、サーバーからクライアントのオブジェクトのメソッドを呼び出すこともできます。ここではplayerがそのクライアントのオブジェクトで、サーバーはこのplayerのメソッドを呼び出し、ライフを変更したりします。

プレイヤーのライフはウィンドウ上のボタンに表示されます(今思えば全然ボタンである必要はないですね)。なので、プレイヤーのライフが変わった場合にはそのボタンにセットし直すようにプログラムした(つもりだった!)というわけです。

これがまずいのは、このLifeChangedに追加したデリゲートの中身が実行されるのはUIスレッドとは別のスレッドだからですね(サーバーが呼び出すタイミングです)。UIのプロパティの多くは、それを作ったスレッドからしか変更できないからです。詳しいことはMSDNに書いてあります(http://msdn2.microsoft.com/ja-jp/library/ms741870(VS.80).aspxhttp://msdn2.microsoft.com/ja-jp/magazine/cc163328.aspx)。で、まあいろいろあって固まったのでしょう。

正しくは、WPFのDispatcherを使って、その中でButtonを変更しなければならなかったんですね。

player.LifeChanged += delegate
{
    YourLifeButton.Dispatcher.BeginInvoke(
        System.Windows.Threading.DispatcherPriority.Background,
        (System.Action)delegate
        {
            YourLifeButton.Content = player.GetLife();
        }
        );
};


追記

やっぱり全然わかっていませんでした。以上のでどうしようもなかったらクライアントクラスのCallbackBehavior属性のUseSynchronizationContextをfalseにしてみることをおすすめします。

 

拍手[1回]


Visual C# 2008 Express EditionでWPFアプリケーションを作っているときツールバーからアイテムをドラッグ&ドロップできない

先月は引越しやら何やらでゴタゴタまみれですっかりブログのサボり癖がついてしまいました。
リハビリとしてまずはちょっとしたことを書いておきます。

WPFでアイテムをツールバーからドラッグ&ドロップできない

ちょっと前から気になっていたことがありました。
VC#2008でWPFを作ってるときに、アイテムをツールバーからドラッグ&ドロップできないという問題です。

WPF、つまりWindows Presentation Foundationは、その前世代(?)のWindowsFormsと同じように、ユーザーインターフェースをドラッグ&ドロップでかんたんに作ることが出来ます(少なくとも、そういうことになっています)。
creatingWPFApplication.jpg

左側にToolboxというのがあってその中にButtonとかCheckBoxとか色んなアイテムがあり、そのアイテムをマウスでドラッグ&ドロップすることで右側のウィンドウに貼り付けることが出来るわけですね。これはWindows Formsと同じです。

ところが、昨年からそうだったのですが、それをやろうとしても×印が出てドロップできませんでした(仕方が無くそのときはXAMLを手打ちしていました。ひどすぎる)。

解決方法

で、ちょっと調べてみたら出てきました。

http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=2431637&SiteID=1
http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=2616165&SiteID=1

次のような操作をXP Professionalでしたらドラッグ&ドロップが出来るようになったそうです(確かめました。僕もVistaで同じ操作をしたら確かにドラッグ&ドロップできるようになりました。でもまあ念のためやるのならバックアップをお願いします):

1.%userprofile%の中の以下のフォルダを削除する
    Local Settings\Application Data\Microsoft\VSCommon
    Local Settings\Application Data\Microsoft\VCSExpress
    Application Data\Microsoft\VCSExpress

(最後のApplication Data\Microsoft\VCSExpressは僕は消し忘れていたのですが、それでも上手くいきました)

2."My Document"で次のフォルダを削除
    Visual Studio 2008\Settings
    Visual Studio 2005\Settings

(Visual Studio 2005のほうは削除しなくてもいい気がしますが、ついついこっちも削除してしまい、検証は出来ませんでした)

3.regedit.exeでレジストリキー、HKEY_CURRENT_USER\Software\Microsoft\VCSExpressを削除する。

(これは必須です。これを削除しなければ相変わらずドラッグ&ドロップは出来ませんでした。これを削除して初めて、再びドラッグ&ドロップできるようになりました。)

3を終えてからVisual C# 2008 Express Editionを起動すると、初回起動のときの例の「数分かかることがあります」が表示され、
fewseconds.jpg(←例のダイアログ)
再びWPFで上手くアイテムをドラッグ&ドロップできるようになります。

検証していないのでなんともいえませんが、おそらくこの手順には無駄がかなりあります。本当に必要なのは、3とわずかなフォルダの削除だけだと思います。もしかしたら3だけでも上手くいくかもしれません


追記

メインコンピュータでもういっかい試したところ(上で試したのはセカンドのラップトップパソコン)、どうやら削除するフォルダは「1.」の"Local Settings\Application Data\Microsoft\VCSExpress"だけでいいっぽいですね。そのあと「3.」のレジストリキーの削除でうまくいきました。(さいしょは「3.」だけを行ったのですがムリでした。~~VCSExpressを削除して、また「3.」をやって、はじめてうまくいきました)

拍手[0回]