WPFでのドラッグ&ドロップの最初の一歩

現在作っているアプリケーションはSilverlightでUIを作っています。
アプリ上で用意したコントロールを自由に移動・リサイズ・プロパティの編集をできるようにしているわけですが、それを作っているのは自分ではなくチームのもう一人のメンバーです。
先日、だいたい動くものができたとの事でコミットされたソースを見たのですが・・・うまく理解できない;;
というか、ここまで動的なUI開発をした事が正直無いので(つまりマウスイベントをたくさん取ったりするやつです)、完全にハマッてしまいました。
ちなみに作ったメンバーはゲーム畑出身なのです、さすがです。
そんな訳で、自分でも少しは付いていけるようにするためにまずはドラッグ&ドロップから勉強していきます。

Thumbコントロールで行う簡単なドラッグ&ドロップ

WPFSilverlight両方にThumb(読み:サム)というコントロールが用意されてるみたいです。
ListBoxをExpressionBlendで分解してる時に出てきたのを覚えてますが、「ドラッグ可能なコントロール」って事だったんですね。確かにスクロールバーのつまみにつかわれてました。
ドラッグ可能というだけあって以下のようなイベントが最初から用意されてます。以下MSDNから引用

DragStarted Thumb コントロールが論理フォーカスおよびマウス キャプチャを受け取ると発生します。
DragDelta Thumb コントロールに論理フォーカスおよびマウス キャプチャがあるときにマウスの位置が変更されると、1 回以上発生します。
DragCompleted Thumb コントロールでマウスのキャプチャがなくなると発生します。
Thumbでのドラッグ&ドロップのサンプル

手っ取り早くCanvas上の絶対配置でのサンプルとして作ります。
XAML

<Window x:Class="TumbSample.TumbSampleWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="TumbSampleWindow" Height="300" Width="300">
    <Canvas x:Name="LayoutRoot">
        <Thumb x:Name="thumb1" 
               Canvas.Left="80" Canvas.Top="80" 
               Background="Green" Width="20" Height="20" 
               DragDelta="onDragDelta" 
               DragStarted="onDragStarted" 
               DragCompleted="onDragCompleted"
               />
    </Canvas>
</Window>

コードビハインド

using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;

namespace TumbSample {
    /// <summary>
    /// TumbSampleWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class TumbSampleWindow : Window {
        public TumbSampleWindow() {
            InitializeComponent();
        }

        void onDragDelta(object sender, DragDeltaEventArgs e) {
            // 位置の変化分を加算して再設定
            Canvas.SetLeft(thumb1, Canvas.GetLeft(thumb1) + e.HorizontalChange);
            Canvas.SetTop(thumb1, Canvas.GetTop(thumb1) + e.VerticalChange);
        }

        void onDragStarted(object sender, DragStartedEventArgs e) {
            // カーソルを手形に変更
            Cursor = Cursors.Hand;
        }

        void onDragCompleted(object sender, DragCompletedEventArgs e) {
            // カーソルを矢印に戻す
            Cursor = Cursors.Arrow;
        }
    }
}

びっくりするぐらい簡単ですね!
ただ四角形がマウスで移動できて、移動時にはカーソルが手形になるだけですけれど、思っていたよりずっと簡単でした。

通常のコントロールCanvas上で行う簡単なドラッグ&ドロップ

次にThumbを使わずに通常のコントロールでドラッグ&ドロップを行ってみます。
Thumbとの大きな違いは、
移動させるコントロール自身が自身の移動量を知っているのではなく、
移動するパネル(今回はCanvas)でマウスの移動値からコントロールの移動量を算出するという部分みたいです。(もしかしたらいい方法があるのかもしれませんが。。)

使用するイベント

今回の簡単なサンプルであれば以下で十分みたいです。以下MSDNから引用。

PreviewMouseLeftButtonDown マウス ポインタがこの要素上にあるときにマウスの左ボタンが押されると発生します。
PreviewMouseMove マウス ポインタがこの要素上にあるときに移動すると発生します。
PreviewMouseLeftButtonUp マウス ポインタがこの要素上にあるときにマウスの左ボタンが離されると発生します。

Preview〜」でなくても今回の例では大丈夫でした。
ちなみにPreviewが付くとトンネルイベント、Previewが無いとバブルイベントとして動作するみたいです。
左マウスのイベントは下のコントロールには通知しないものとして、トンネルイベントで実装しそこで止めてしまう事にしています。

通常コントロールでのドラッグ&ドロップのサンプル

XAML

<Window x:Class="SimpleDragDropSample.SimpleDragDropSample"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="SimpleDragDropSample" Height="300" Width="300">
    <Canvas x:Name="LayoutRoot"
            PreviewMouseLeftButtonDown="canvas_PreviewMouseLeftButtonDown"
            PreviewMouseMove="canvas_PreviewMouseMove"
            PreviewMouseLeftButtonUp="canvas_PreviewMouseLeftButtonUp"
            Background="AliceBlue">
        <Ellipse
                 Fill="Green" Width="50" Height="50" 
                 Canvas.Left="80" Canvas.Top="80"/>
        <Ellipse
                 Fill="Yellow" Width="50" Height="50" 
                 Canvas.Left="150" Canvas.Top="80"/>
    </Canvas>
</Window>

コードビハインド側

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace SimpleDragDropSample {
    /// <summary>
    /// SimpleDragDropSample.xaml の相互作用ロジック
    /// </summary>
    public partial class SimpleDragDropSample : Window {
        /// <summary>
        /// ドラッグ中かどうか
        /// </summary>
        private bool isDragging;
        /// <summary>
        /// 現在のマウス位置
        /// </summary>
        /// <remarks>ドラッグ中のみ更新</remarks>
        private Point currentMousePoint;
        /// <summary>
        /// ドラッグ中のUI要素
        /// </summary>
        private UIElement dragginUIElement;

        public SimpleDragDropSample() {
            InitializeComponent();
        }

        private void canvas_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
            if (LayoutRoot.Equals(e.Source)) {
                return;
            }

            this.isDragging = true;
            this.dragginUIElement = e.Source as UIElement;
            this.currentMousePoint = e.GetPosition(null);
            // カーソルを手形に変更
            Cursor = Cursors.Hand;

            // これ以上トンネルイベントが通知されないように
            e.Handled = true;
        }

        private void canvas_PreviewMouseMove(object sender, MouseEventArgs e) {
            if (!this.isDragging) {
                return;
            }

            // 位置の変化分を算出
            Point newMousePoint = e.GetPosition(null);
            double horizontalChange = newMousePoint.X - this.currentMousePoint.X;
            double verticalChange = newMousePoint.Y - this.currentMousePoint.Y;

            // 位置の変化分を加算して再設定
            Canvas.SetLeft(this.dragginUIElement, Canvas.GetLeft(this.dragginUIElement) + horizontalChange);
            Canvas.SetTop(this.dragginUIElement, Canvas.GetTop(this.dragginUIElement) + verticalChange);

            // 現在位置更新
            this.currentMousePoint = newMousePoint;
        }

        private void canvas_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) {
            this.isDragging = false;
            this.dragginUIElement = null;
            // カーソルを矢印に戻す
            Cursor = Cursors.Arrow;

            // これ以上トンネルイベントが通知されないように
            e.Handled = true;
        }
    }
}

イベントの感知元が移動コントロールを持つCanvasになるため今回は最低限として以下の判定を入れています

  • マウス左クリック押下時のコントロールイベント発生元がCanvas自身ではないか
  • マウス移動時にドラッグ中であるか

Canvasは背景や透過度等を何も設定しない状態だとマウスクリックイベントを発生させないので注意してください。詳しくはココ

作ってみれば、Thumbの時とそれほど違わない感じで作成できました。
しかも、このサンプルではWindow外にマウスカーソルが移動した時とか、色々問題はありそうです。

よりドラッグ&ドロップっぽくしてみる

ここまでサンプルを作ってはみましたが、どちらも「コントロールを移動しているだけ」って事に気づきました。なんかドラッグ&ドロップっていうと、半透明っぽい分身が現れて、そいつが移動したりしますよね。ですよね!

修飾のためのAdornerクラス

最初は移動させるUIElementをコピーさせて〜とか考えてましたが、調べてみるとAdornerというクラスを使うサンプルが多いようです。せっかくなので使ってみましょう。
AdornerはUIElementにバインドされ、バインド先を装飾するという事のようです。でも、単純にバインド先に張り付くというよりはバインド元の装飾レイヤに乗っかるだけって事みたいです。だから配置する場所も自由、それをドラッグ中のコントロールに見せかけるって魂胆みたいですね。
以下MSDNのAdornerからの引用です。

装飾は、UIElement にバインドされるカスタム FrameworkElement です。装飾は、装飾対象の要素、または装飾対象の要素のコレクションの上に常に存在するレンダリング サーフェイスである装飾層で描画されます。装飾の描画は、装飾のバインド先の UIElement の描画からは独立しています。通常、装飾は、装飾対象の要素の左上に位置する標準の 2 次元座標の原点を使用して、バインド先の要素に対して相対的な位置に配置されます。

さて、Adornerは抽象クラスなので継承クラスを作る必要があるわけですが、いまいちどうすればいいのか掴めません。まず簡単な表示オンリーサンプルを作ってみます。

Adornerの簡単なサンプル

Ellipseを装飾するAdornerクラス

public class EllipseAdorner : Adorner {

    private Ellipse adornerChild;

    public EllipseAdorner(UIElement adornedElement)
        : base(adornedElement) {
        // 装飾用の小さなEllipseを作る
        adornerChild = new Ellipse();
        adornerChild.Width = 30;
        adornerChild.Height = 30;
        adornerChild.Fill = Brushes.Aqua;
    }

    protected override Size MeasureOverride(Size constraint) {
        adornerChild.Measure(constraint);
        return adornerChild.DesiredSize;
    }

    protected override Size ArrangeOverride(Size finalSize) {
        adornerChild.Arrange(new Rect(finalSize));
        return finalSize;
    }

    protected override int VisualChildrenCount {
        get { return 1; }
    }

    protected override Visual GetVisualChild(int index) {
        return adornerChild;
    }
}

XAML

<Window x:Class="AdornerSample.AdornerSample"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="AdornerSample" Height="300" Width="300">
    <Canvas x:Name="LayoutRoot"
            Background="AliceBlue">
        <Ellipse x:Name="ellipse1"
                 Fill="Green" Width="50" Height="50" 
                 Canvas.Left="80" Canvas.Top="80"/>
    </Canvas>
</Window>

コードビハインド側

using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Shapes;

namespace AdornerSample {
    /// <summary>
    /// AdornerSample.xaml の相互作用ロジック
    /// </summary>
    public partial class AdornerSample : Window {
        public AdornerSample() {
            InitializeComponent();

            this.Loaded += new RoutedEventHandler(AdornerSample_Loaded);
        }

        void AdornerSample_Loaded(object sender, RoutedEventArgs e) {
            // 装飾クラスを作り、装飾対象の装飾レイヤに追加
            EllipseAdorner adorner = new EllipseAdorner(this.ellipse1);
            AdornerLayer layer = AdornerLayer.GetAdornerLayer(this.ellipse1);
            layer.Add(adorner);
        }
    }
}

実行結果

なるほど、装飾対象の左上に配置されてますね。
では、次に場所を移動させてみます。今回は以下の2つの方法で試してみます。

  • Adorner#ArrangeOverride時の装飾要素のArrange設定に位置情報も加えてみる
  • Adorner#GetDesiredTransformをオーバーライドし変換要素に平行移動変換(TranslateTransform)を追加する。

ではやってみます。

ArrangeOverride時に設定する場合
public class EllipseAdorner : Adorner {

    private Ellipse adornerChild;

    public EllipseAdorner(UIElement adornedElement)
        : base(adornedElement) {
        // 装飾用の小さなEllipseを作る
        adornerChild = new Ellipse();
        adornerChild.Width = 30;
        adornerChild.Height = 30;
        adornerChild.Fill = Brushes.Aqua;
    }

    protected override Size MeasureOverride(Size constraint) {
        adornerChild.Measure(constraint);
        return adornerChild.DesiredSize;
    }

    protected override Size ArrangeOverride(Size finalSize) {
        // 左上隅からの位置情報を加えてArrangeに設定する
        Point location = new Point(10, 10);
        Rect rect = new Rect(location, finalSize);
        adornerChild.Arrange(rect);
        return finalSize;
    }

    protected override int VisualChildrenCount {
        get { return 1; }
    }

    protected override Visual GetVisualChild(int index) {
        return adornerChild;
    }
}

実行結果

10pxづつずらす事で中央に配置されました。

GetDesiredTransform時に設定する場合
public class EllipseAdorner : Adorner {

    private Ellipse adornerChild;

    public EllipseAdorner(UIElement adornedElement)
        : base(adornedElement) {
        // 装飾用の小さなEllipseを作る
        adornerChild = new Ellipse();
        adornerChild.Width = 30;
        adornerChild.Height = 30;
        adornerChild.Fill = Brushes.Aqua;
    }

    protected override Size MeasureOverride(Size constraint) {
        adornerChild.Measure(constraint);
        return adornerChild.DesiredSize;
    }

    protected override Size ArrangeOverride(Size finalSize) {
        adornerChild.Arrange(new Rect(finalSize));
        return finalSize;
    }

    protected override int VisualChildrenCount {
        get { return 1; }
    }

    protected override Visual GetVisualChild(int index) {
        return adornerChild;
    }

    public override GeneralTransform GetDesiredTransform(GeneralTransform transform) {
        // 変換を複数にするためグループにして、左上から10pxづつ平行移動させる変換を追加
        GeneralTransformGroup transformGroup = new GeneralTransformGroup();
        transformGroup.Children.Add(base.GetDesiredTransform(transform));
        transformGroup.Children.Add(new TranslateTransform(10, 10));
        return transformGroup;
    }
}

実行結果はさっきのと同じです。
せっかくなので変換の種類をまとめておきます。

回転 RotateTransform
スケーリング ScaleTransform
傾斜 SkewTransform
平行移動 TranslateTransform

装飾要素(adornerChild)を複数にする時は、UIElementのリストとかで管理するとoverrideメソッドの実装が簡単になります。

GetDesiredTransformでドラッグに追従する装飾を作る

さて、どちらの方法でもドラッグ時に装飾要素を移動させる事はできそうですが、イベントの発火回数を見ているとArrangeOverrideはGetDesiredTransformを一回多く発火されているみたい。(すいません、理由はわかりません。)
加えて、複数の要素を修飾要素とした場合にはArrangeで要素一つずつ設定するよりも、変換としてまとめて移動とかができる方が今のところ楽そうなのでGetDesiredTransformの方が使いやすそうです。

ここからはMSDNにあったサンプルを元にコードを改造してみます。

まずは装飾クラスです。ポイントは

  • 装飾要素は装飾元のサイズ・塗りつぶし色を引継ぎ、移動中を表すように半透明にする。
  • 装飾要素の移動は装飾元の左上からの相対座標となるため、OffSetとして表現する。
  • OffSetが更新されるたびに装飾レイヤを更新し、変換を適用させる。
public class EllipseAdorner : Adorner {

    private Ellipse adornerChild = null;
    private double leftOffset = 0;
    private double topOffset = 0;

    public EllipseAdorner(UIElement adornedElement)
        : base(adornedElement) {
        VisualBrush _brush = new VisualBrush(adornedElement);

        adornerChild = new Ellipse();
        adornerChild.Width = adornedElement.RenderSize.Width;
        adornerChild.Height = adornedElement.RenderSize.Height;
        adornerChild.Fill = _brush;
        adornerChild.Opacity = 0.5;
    }

    protected override Size MeasureOverride(Size constraint) {
        adornerChild.Measure(constraint);
        return adornerChild.DesiredSize;
    }

    protected override Size ArrangeOverride(Size finalSize) {
        adornerChild.Arrange(new Rect(finalSize));
        return finalSize;
    }

    protected override Visual GetVisualChild(int index) {
        return adornerChild;
    }

    protected override int VisualChildrenCount {
        get { return 1; }
    }

    public double LeftOffset {
        get { return leftOffset; }
        set {
            leftOffset = value;
            UpdatePosition();
        }
    }

    public double TopOffset {
        get { return topOffset; }
        set {
            topOffset = value;
            UpdatePosition();
        }
    }

    private void UpdatePosition() {
        AdornerLayer adornerLayer = this.Parent as AdornerLayer;
        if (adornerLayer != null) {
            adornerLayer.Update(AdornedElement);
        }
    }

    public override GeneralTransform GetDesiredTransform(GeneralTransform transform) {
        GeneralTransformGroup result = new GeneralTransformGroup();
        result.Children.Add(base.GetDesiredTransform(transform));
        result.Children.Add(new TranslateTransform(leftOffset, topOffset));
        return result;
    }
}

次にこれを使うソースですが、XAML通常コントロールでのドラッグ&ドロップのサンプルと同じです。コードビハインドのみ修正します。

using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;

namespace AdornerDragDropSample {
    /// <summary>
    /// AdornerDragDropSample.xaml の相互作用ロジック
    /// </summary>
    public partial class AdornerDragDropSample : Window {
        /// <summary>
        /// マウスが押下されているかどうか
        /// </summary>
        private bool isMouseDown;
        /// <summary>
        /// ドラッグ中かどうか
        /// </summary>
        private bool isDragging;
        /// <summary>
        /// ドラッグ開始時のエレメントの左上位置
        /// </summary>
        private Point startDragElementPoint;
        /// <summary>
        /// ドラッグ開始時のマウスの位置
        /// </summary>
        private Point dragStartMousePoint;
        /// <summary>
        /// ドラッグ元のUI要素
        /// </summary>
        private UIElement dragsourceUIElement;
        /// <summary>
        /// ドラッグ中のオーバーレイ要素
        /// </summary>
        private EllipseAdorner overlayElement;

        public AdornerDragDropSample() {
            InitializeComponent();
        }

        private void canvas_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
            if (LayoutRoot.Equals(e.Source)) {
                return;
            }

            this.isMouseDown = true;
            this.dragsourceUIElement = e.Source as UIElement;
            this.dragStartMousePoint = e.GetPosition(null);
            // カーソルを手形に変更
            Cursor = Cursors.Hand;
            // これ以上トンネルイベントが通知されないように
            e.Handled = true;
        }

        private void canvas_PreviewMouseMove(object sender, MouseEventArgs e) {
            if (!this.isMouseDown) {
                return;
            }

            if (!this.isDragging) {
                DragStarted();
            } else {
                DragMoved();
            }
        }

        private void canvas_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) {

            DragFinished();
            // カーソルを矢印に戻す
            Cursor = Cursors.Arrow;
            // これ以上トンネルイベントが通知されないように
            e.Handled = true;
        }

        private void DragStarted() {
            isDragging = true;
            // 最終的な移動値を得るために開始時の要素の位置を保存する
            startDragElementPoint.X = Canvas.GetLeft(dragsourceUIElement);
            startDragElementPoint.Y = Canvas.GetTop(dragsourceUIElement);

            overlayElement = new EllipseAdorner(dragsourceUIElement);
            AdornerLayer layer = AdornerLayer.GetAdornerLayer(dragsourceUIElement);
            layer.Add(overlayElement);
        }

        private void DragMoved() {
            Point currentMousePosition = System.Windows.Input.Mouse.GetPosition(this.LayoutRoot);

            // 現在とドラッグ開始時のマウス位置からオフセット値を算出し更新
            overlayElement.LeftOffset = currentMousePosition.X - dragStartMousePoint.X;
            overlayElement.TopOffset = currentMousePosition.Y - dragStartMousePoint.Y;
        }

        private void DragFinished() {
            if (isDragging) {
                AdornerLayer.GetAdornerLayer(overlayElement.AdornedElement).Remove(overlayElement);

                // ドラッグ開始時の要素の位置にオーバーレイ要素のオフセットを加算して、オーバーレイ要素の位置に移動させる
                Canvas.SetLeft(dragsourceUIElement, startDragElementPoint.X + overlayElement.LeftOffset);
                Canvas.SetTop(dragsourceUIElement, startDragElementPoint.Y + overlayElement.TopOffset);

                overlayElement = null;
            }
            isDragging = false;
            isMouseDown = false;
        }
    }
}

ほぼサンプルを拝借した感じになってしまいましたが、ドラッグ開始時にドラッグ対象要素の左上位置を保存するのが注意ですね。代わりに左上位置とドラッグ開始時のマウスカーソルの差分を保存しておいても同じですが、要素の左上位置を持っておくほうがわかりやすいですね。

カーソルが見えないのでわかりにくいですが実行画面です。半透明になってるのがドラッグ途中のものです。

まとめ

最初の一歩として調べ調べやってましたが、やはり大変ですね。。
まだパネル間のドラッグ&ドロップはできてないので次回はその辺を調べていかないと。

Sandcastleを最低限使うための最初のメモ

縦割りじゃなくて横割りの開発をする場合には、層ごとに公開するAPIのドキュメントは必須ですよね。
そんな訳で、Silverlight開発というか、C#開発でのAPIドキュメントの作り方を調べます。

どんな方法があるのか?

Javaで開発をしていた頃はチームの方針でJavadocを使っていました。Eclipseからもさっくり作れて簡単でした、きっとMS様のことなのでさらに簡単に作れる方法を提供してくれている事でしょう。ワクワク

とりあえず2つ

見つかりました。

  1. Sandcastle
  2. NDoc

こちらのエントリによりますと、ドキュメント作成の方法が後2つぐらいあるのと、NDocがメンテされてないSandcastleが定番という事らしいです。なるほど。

そんな訳で

Sandcastleを採用する事にします。

Sandcastleの導入

インストールしたのは以下の二つです。リンクはTopページにしてあるのでDownloadページでダウンロードしてください。
インストールはインストーラが付いているので何も考えなくてOK。

  1. Sandcastle May 2008 Release (Version 2.4.10520)
  2. Sandcastle Help File Builder 1.8.0.1 Beta

Sandcastle Help File Builder はコマンドラインSandcastleGUIで使えるようにしてくれるようです。便利!!

Sandcastleを使ってみる

自分としてはJavadocのブラウザからさくっと見られる感じが好きだったので、普通のHTML形式での出力でやってみます。

0. 準備

ドキュメントを作りたいプロジェクトをVisual Studioで作っておきましょう。今回はVisual Studio 2008 Proでやってますが、Visual Studioからは何もやってないのでバージョンは何でもいいです。
加えて以下が必要です。

  1. プロジェクトのプロパティの「ビルド」の項目において、リリースのXMLドキュメントを作成するようにチェックを付ける
  2. 参照設定されているもの全てのローカルコピーを「True」にする。(これはSandcastle側でRefelense設定するのが面倒だから。)
  3. リリースビルドをして正常終了する事を確認し、出力されたDLLとXMLファイルの場所を確認しておく。
1. 新規プロジェクトを作る


File -> New Projectを選択すると、選択画面が出てくるのでドキュメントファイル作成のルートとなる場所に名前を付けてSandcastleのプロジェクトファイル(.shfbproj)を作ります。
プロジェクト単位で作りたいなら.projファイルがある場所に同名で作る、複数プロジェクトのドキュメントを同時に作るなら、.slnファイルがある場所に同名で作ればいいんじゃないかな。(適当...)

今回はEntityDataModelSampleっていう過去に作ったプロジェクトに対してやっています。よってこんな感じ。

EntityDataModelSample/
   + EntityDataModelSample.proj
   + EntityDataModelSample.shfbproj
   + Bin/
   |  + Release/
   |      + EntityDataModelSample.dll
   |      + EntityDataModelSample.XML
   + ソースとか..

すると、こんな感じの画面が出てきます。

2. ドキュメント生成対象を設定する。

Project ExplorerからDocumentation Sourceを右クリックしAdd Documentation Source...を選択します。

ファイル選択画面になりますので、準備で生成しておいたdllを選択します。
すると、dllとxmlがProject Explorerに追加されます。

Referenceには何も設定しません、ローカルコピーで全て上記で設定したdllと同じ場所にあるからです。
(というか、正直Referenceの設定に関してはまだよく理解できていないので、とりあえずローカルコピーTrueで成功したからそうしているだけですorz..)

2.5 詳細なプロジェクトのプロパティを設定する前に..

この時点で一度ドキュメントを作ってみます。ただし、HTML Helpのコンパイラが無いと無理だけど。。持ってると仮定して。(C:\Program Files\HTML Help Workshopとかあればたぶん大丈夫。)
リリースモードにして、Documentation -> Build Projectを選択すればOKです。Build Outputタブが現れてゴリゴリと作ってくれます。

reference resolve errorみたいなのが出てたら参照がまずいと思います。ローカルコピーを確認してみてください。
さて、成功したら早速確認してみましょう!まずどこにできているかですが、以下の場所にできています。

EntityDataModelSample/
   + EntityDataModelSample.proj
   + EntityDataModelSample.shfbproj
   + Bin/
   |  + Release/
   |      + EntityDataModelSample.dll
   |      + EntityDataModelSample.XML
   + ソースとか..
   + Help  <== ココ!!
      + Documentation.chm
      + LastBuild.log

Documentationというのはデフォルトの名前なので変更可能です。

3 詳細なプロジェクトのプロパティを設定する(少しだけ..)
プロパティカテゴリ プロパティ名 変更値と理由
Build FrameworkVersion 3.0
  HelpFileFormat Website (素のHTMLで欲しいので)
Comments NamespaceSummaries 名前空間の説明を書きたい
  ProjectSummary プロジェクトの説明を書きたい
Help File Help Title タイトルを設定したい
  Language 日本語
  NamingMethod MemberName

とりあえずこれぐらいでしょうか、後はおいおい調べていかないと。。

4 Websiteとして出力する

さて、3の設定で出力をWebsite形式に変えたので、次のビルドでは出力が変わるはずです。さっそくビルドしてみます。
...こんな小さなプロジェクトでも2分近くかかりますね。。
ではフォルダ構成はどうなっているでしょうか。

EntityDataModelSample/
   + EntityDataModelSample.proj
   + EntityDataModelSample.shfbproj
   + Bin/
   |  + Release/
   |      + EntityDataModelSample.dll
   |      + EntityDataModelSample.XML
   + ソースとか..
   + Help
      + Index.html
      + Web.config
      + Index.aspx
      + .....etc

Web.configやaspx拡張子のものができている事から分かるとおり、ASP.NETのデータもできています。これをIISにホストしてもいいのですが、うちのサーバーマシンはあいにくLinuxですし、検索とは必要ないので単純に静的なページを閲覧できるだけでよいわけです。
では、Index.htmlをブラウザで開いてみましょう。APIドキュメントが表示されたでしょうか?

5 まとめ

GUIから非常に簡単にAPIドキュメントが生成できました。しかもAPIドキュメント用のプロジェクトとして残るので、名前空間やプロジェクトのSummaryも一緒にバージョン管理する事ができます。う〜ん、素晴らしい。

会社での作業はだいたい機能単位の縦割りで作業するのであんまりメンバーにAPIを公開をする必要ってないんですが(それはそれで気楽でいい)、横割りだとAPI考えたりと、また違った楽しみがありますね。

インターフェースの明示的実装

XMLコメントを書かないと警告がすごいことになる

開発現場って往々にして警告に対してもすごい敏感だったりする。
今の現場もそうで、「警告が一つでもあったら納品コードとして認めない」みたいな事を言っていらっしゃるのですが、一つ問題がある。
それはXMLコメントだ。

private以外のものに関してはXMLコメントを付けないと通常はコンパイラが警告として扱ってくるわけですが、インターフェース実装のクラスに対しても当然のように要求してくるのです。

Javaだと、インターフェース実装のクラスのコメントは書かなくてもJavaDocのツールが勝手に引っ張ってきてくれます。Eclipseだと勝手にリンクのコメント付けてくれたりするからなお楽。
でもVisual Studioにはそれが無い。。自分が知らないだけかもしれないが。

そんな訳で現時点でも警告は2000近く行ってるわけで、どうすんだろね。

明示的に実装するとコメントいらないらしい

先輩と話していたのだが、インターフェースを明示的に実装すると実装クラスではコメントがいらないようだ。
「これは便利だ!!」
とばかりに書き換え始めようとしたのだが、落とし穴があった。

インターフェースの明示的な実装をすると起こる事

インターフェースの明示的な実装は英語ではEIMI(Explicit Interface Method Implementation)と言うらしい、長ったらしいので今後はEIMIと表記するよ。

さて、ではEIMIを使う場面はどこなのだろう。それは、名前もシグネチャも同じメソッドを持つ複数のインターフェースを実装する場合だろう。

public interface A {
   void Execute(string arg);
}

public interface B {
   void Execute(string arg);
}

public class AB : A, B {
   void A.Execute(string arg) {
      Console.WriteLine("Execute A");
   }

   void B.Execute(string arg) {
      Console.WriteLine("Execute B");
   }
}

実は、ジェネリックインターフェースがある現在では、EIMIを使うケースはこのケースのみで、これ以外では極力使わない方がよいらしい。

EIMIを使うと起こる悪影響

プログラミング .NET Framework 第2版によると、EIMIを使う事で起こる悪影響は以下の3つであると書いてあります。

  1. 型がEIMIメソッドをどのように実装しているか説明されているドキュメントがなく、Microsof Visual StudioのIntelliSenseも利用できない。
  2. 値型のインスタンスはインターフェースにキャストすつときにボックス化される
  3. EIMIは派生型から呼び出せない

詳細な説明は書籍を一読してもらうとして、まず1の影響は顕著でした。Visual Studioで開いているクラスのメソッド一覧を出すドロップダウンにEIMIのメソッドが出てこない!これは本当に焦りました、一瞬消してしまったのかと。。
2は個人的には遭遇する事はあまり無いのですが、このドキュメントを読んで、やっとC#「インターフェース経由でインターフェースのメソッドを呼ぶ」というのをやらせたがるのか分かりました。
Javaから来た自分としては、なぜわざわざ「インターフェース経由」で呼ばなければならないのかさっぱりわからなかったのです。
というか、今でも「C#はインターフェースを重視していない言語」って思ってしまいます。だって、ListをIListで受けると、ほとんどのメソッドが使えないんですもん。。やっぱり、その時々に応じてインターフェース経由で呼ぶって方法が正しいんですかね。

結局XMLコメントの問題は解決しない

結局EIMIでのコメントの省略の夢は潰えました。。

確かにコメントは無いよりあった方がいいとは思いますが、結局実装クラスに書くコメントってほとんどの人がインターフェースのコメントのコピーで、しかもそのコメントはほとんど意味の無いものばかり。
だったら、付けない方がまだマシ。

もちろん過密スケジュールでまともにインターフェース設計もできないような状況でインターフェースに細かく事前条件やらを書いていくのは1実装者としてキツイです。てゆうか自分もできてない。。

管理者の方は「警告があったら納品物として認めない」とか言うのは簡単だけど、ホントに必要なものと無くてもいいものをちゃんと見極めて指示を出して欲しいな〜。

written by dany (role of younger brother)

Dictionaryでforeach

自分は今までDictionaryを走査する時には以下のようなコードを書いていました。
たぶん今携わってるプロジェクトでもこう書いてます。

class Program {
    static void Main(string[] args) {
        var sampleDict 
            = new Dictionary<string, string> { { "Key1", "Val1" }, { "Key2", "Val2" }, { "Key3", "Val3" } };

        foreach (var key in sampleDict.Keys) {
            Console.WriteLine(string.Format("Key : {0} / Value : {1}", key, sampleDict[key]));
        }
    }
}

ですが、やっぱりこの走査の仕方には疑問があったわけです。
Dictionary(というかIDictionary実装)はKeyとValueのペアとして持っているのに、Key単独でループさせるのって何か直感的じゃない。

そんな訳で重い腰を上げて調べてみると、、やっぱりありました。直感的な方法が。
てゆうか自分が無知なだけ 0;)

以下がその方法で書き換えた版

static void Main(string[] args) {
    var sampleDict 
        = new Dictionary<string, string> { { "Key1", "Val1" }, { "Key2", "Val2" }, { "Key3", "Val3" } };

    foreach (KeyValuePair<string, string> pair in sampleDict) {
        Console.WriteLine(string.Format("Key : {0} / Value : {1}", pair.Key, pair.Value));
    }

    Console.ReadLine();
}

うん、やはりしっくりきます。

補足として、非ジェネリッククラスのHashtableではDictionaryEntryクラスがキーと値のペアをobject型格納するようです。
こんな感じ。

foreach(DictionaryEntry entry in sampleHashTable) {
    Console.WriteLine(entry.Key + " / " + entry.Value);
}

てゆうかなんで俺は今までペアで受ける方法を知らずに生きてきたんだ。。
明日からはこっちを使おう!

written by dany(role of younger brother)

続続・WCF最初の一歩

前回svcutil.exeを使ってクライアントのプロキシを作成しました。

基本的なWCFサービスと呼び出しを作る6つのタスクと前回までの終了分

  • サービスコントラクトを定義する
  • サービス コントラクトを実装する
  • 基本的なサービスをホストおよび実行する
  • クライアントを作成する
  • クライアントを構成する
  • クライアントを使用する

クライアントを構成する

ここでは、クライアント作成時に一緒に作成した構成ファイルについて見ていくみたいです。
では、構成ファイルを見てみましょう。app.configという名前でClientプロジェクトに作成しました。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <system.serviceModel>
        <bindings>
            <wsHttpBinding>
                <binding name="WSHttpBinding_ICalculator" closeTimeout="00:01:00"
                    openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
                    bypassProxyOnLocal="false" transactionFlow="false" hostNameComparisonMode="StrongWildcard"
                    maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
                    messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true"
                    allowCookies="false">
                    <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
                        maxBytesPerRead="4096" maxNameTableCharCount="16384" />
                    <reliableSession ordered="true" inactivityTimeout="00:10:00"
                        enabled="false" />
                    <security mode="Message">
                        <transport clientCredentialType="Windows" proxyCredentialType="None"
                            realm="" />
                        <message clientCredentialType="Windows" negotiateServiceCredential="true"
                            algorithmSuite="Default" establishSecurityContext="true" />
                    </security>
                </binding>
            </wsHttpBinding>
        </bindings>
        <client>
            <endpoint address="http://localhost:8000/ServiceModelSamples/Service/CalculatorService"
                binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_ICalculator"
                contract="ICalculator" name="WSHttpBinding_ICalculator">
                <identity>
                    <userPrincipalName value="Luigi\Owner" />
                </identity>
            </endpoint>
        </client>
    </system.serviceModel>
</configuration>

はい、いきなりこんなに出されてもよくわかりませんね。
MSDNチュートリアルでは今回重要な部分だけ絞ったものを説明してくれてます。
こんな感じで抜き出されてました。

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.serviceModel>
    <bindings>
      <wsHttpBinding>
        <binding name="WSHttpBinding_ICalculator">
        </binding>
      </wsHttpBinding>
    </bindings>
    <client>
      <endpoint
           address="http://localhost:8000/ServiceModelSamples/Service/CalculatorService"
           binding="wsHttpBinding"
           bindingConfiguration="WSHttpBinding_ICalculator"
           contract="ICalculator"
           name="WSHttpBinding_ICalculator">
      </endpoint>
    </client>
  </system.serviceModel>
</configuration> 
要素

要素を説明する前に前々回のサービス作成時に以下のようなコードを書いた事を思い出してください。

// selfHostはServiceHostのインスタンス
selfHost.AddServiceEndpoint(
    typeof(ICalculator),
    new WSHttpBinding(),
    "http://localhost:8000/ServiceModelSamples/Service/CalculatorService"
);
ServiceMetadataBehavior serviceMetadataBehavior = new ServiceMetadataBehavior() { HttpGetEnabled = true };
selfHost.Description.Behaviors.Add(serviceMetadataBehavior);

ここでもendpointという言葉が使われていましたよね。
そして「どのサービスコントラクトを(typeof(ICalculator))」「どの形式でネットワークに公開し(new WSHttpBinding())」「どの場所で提供するか("http://localhost:8000/ServiceModelSamples/Service/CalculatorService")」を指定してました。
つまり、クライアントの構成ファイルにはサービスのエンドポイント情報をそのまま記述しているという事ですね。

要素

要素は内部にWSHttpBinding_ICalculatorという値のname属性を持つ要素を持ちます。
MSDNから抜粋すると

システム指定の WsHttpBinding で構成された通信に Microsoft.ServiceModel.Samples.ICalculator コントラクトを使用することを指定します。

こういう事?

とりあえず、公開したサービスに対応するクライアントの構成ファイルができている事が確認できました。
では最後にクライアントを使用しましょう。

クライアントを使用する

まずMSDNの本文の抜粋

Windows Communication Foundation クライアントを使用するには

   1. 呼び出すサービスのベース アドレスの EndpointAddress インスタンスを作成して、WCF Client オブジェクトを作成します。
   2. Client 内からクライアント操作を呼び出します。
   3. WCF クライアントに対して Close を呼び出し、ユーザーがアプリケーションを終了するために Enter キーを押すまで待機します。

では順番に実行していきましょう!と言いたいところですが、まずいきなり一つ目からわかりませんね。。

EndpointAddressって?

EndPointAddressインスタンスを作成して」とか普通に書いてありますがどう作るんでしょう?
まず一つの手がかりになりそうなのが、生成したクライアントプロキシクラスのコンストラクタの引数に渡せるケースがあるようです。

    public CalculatorClient(string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) : 
            base(endpointConfigurationName, remoteAddress)
    {
    }
    
    public CalculatorClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) : 
            base(binding, remoteAddress)
    {
    }

remoteAddressって書いてありますね、おそらくサービス側のアドレス情報なのかな。Endpointってところからもそんな感じ。
では次にEndpointAddressクラスのドキュメントを見てみます。

サービスのエンドポイントを一意に識別するエンドポイント アドレス。

エンドポイント アドレスは、サービス エンドポイントに属し、バインディングコントラクト、およびエンドポイントの動作を含みます。

EndpointAddress は、URI とアドレス プロパティ (ID、WSDL 要素、およびオプションのヘッダーのコレクションを含む) を格納します。オプションのヘッダーは、エンドポイントの識別または対話に使用される、より詳細なアドレス指定情報を提供するために使用されます。たとえば、複数のサービスインスタンスが使用できる場合に、これらのヘッダーを使用して、特定ユーザーからの受信メッセージの処理に使用するインスタンスを示すことができます。

サービスのエンドポイントアドレスは、コードを使用して命令的に、または構成を通じて宣言的に指定できます。設置済みサービスのバインディングおよびアドレスは一般的に、サービスの開発中に使用されるものとは異なるので、コード内でエンドポイントを定義することは通常、実用的ではありません。サービスエンドポイントの定義には、構成を使用する方がより実用的です。

EndpointAddress は ISerializable インターフェイスを実装しません。したがって、シリアル化可能ではありません。エンドポイントをサービスコントラクトの一部として公開するには、エンドポイントがシリアル化可能であると同時に、Web Service Addressing (WS-Addressing) プロトコルに準拠している必要があります。WS-Addressing のバージョン 1.0 と August 2004 バージョンに準拠するシリアル化可能なエンドポイントは、それぞれ EndpointAddress10 クラスと EndpointAddressAugust2004 クラスによって提供されます。

正直よくわかりませんね。まず「
EndpointAddress は、URI とアドレス プロパティ (ID、WSDL 要素、およびオプションのヘッダーのコレクションを含む) を格納します。」から、URIとそれの付属情報を持っていそうです。
ですが大事なのは

サービスのエンドポイントアドレスは、コードを使用して命令的に、または構成を通じて宣言的に指定できます。設置済みサービスのバインディングおよびアドレスは一般的に、サービスの開発中に使用されるものとは異なるので、コード内でエンドポイントを定義することは通常、実用的ではありません。サービスエンドポイントの定義には、構成を使用する方がより実用的です。

こっちみたいです。つまり、コードで指定するよりも、構成ファイルで指定する方が実用的みたいです。
今回はsvcutilで構成ファイルも生成しています。そして、クライアントプロキシクラスには、EndpointAddressクラスを指定する必要の無いコンストラクタがありました。
てことは、おそらく構成ファイルから自動でEndpointAddress情報を読み出してくれる(?)と思っていい感じです。

クライアント実装

さて、では上記を踏まえて一気に実装してみます。
以下がクライアントプロジェクトのProgram.csの実装です。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;

namespace SuperlightBrothers.Sample {
    class Program {
        static void Main(string[] args) {
            // クライアントプロキシクラス
            CalculatorClient client = null;
            try {
                client = new CalculatorClient();

                double value1 = 10.00D;
                double value2 = 5.55D;

                // Add
                double addresult = client.Add(value1, value2);
                Console.WriteLine("Add : {0}", addresult);
                // Subtract
                double subtractresult = client.Subtract(value1, value2);
                Console.WriteLine("Subtract : {0}", subtractresult);
                // Multiply
                double multiplyresult = client.Multiply(value1, value2);
                Console.WriteLine("Multiply : {0}", multiplyresult);
                // Divide
                double divideresult = client.Divide(value1, value2);
                Console.WriteLine("Divide : {0}", divideresult);

                Console.WriteLine("Enterキーで終了");
                Console.ReadLine();
            } catch {
                Console.WriteLine("Error!");
            } finally {
                // Close
                client.Close();
            }
        }
    }
}

では実行してみましょう。
以下の手順で確認できます。
1. サービス側の実行ファイルService.exeを実行します。(「Enterキーで終了」が出ている状態を維持)
2. クライアント側の実行ファイルClient.exeを実行します。

では結果を見てみましょう。

Add : 15.55
Subtract : 4.45
Multiply : 55.5
Divide : 1.8018018018018
Enterキーで終了

おお!結果が表示されてますね!
おそらく.NETのXMLWebServiceで作った事ある人は当たり前なんでしょうけど、こんなに簡単にクライアントとの通信ができるなんて。。以前JavaでAxisを使ったプロジェクトに関わっていた事があるんですが、こっちのが全然簡単ですね。。

チュートリアルのまとめ

さて、チュートリアルとは言っても結構大変でした。

  • サービスをWSHttpBindingで公開する
  • セルフホストでサービスを公開する
  • svcutilでクライアントプロキシと構成ファイルを生成する

の3つがチュートリアルから今後の発展のキーになるのではと思います。
つまり

  • まず、WSHttpBinding以外の形式で公開するには?
  • 次にセルフホスト以外の方法で公開するには?
  • そして、svcutilを使わずにクライアントを作成するには?

が、情報として必要そうです。

最終的には、Silverlightとの連携を行う必要があるので、今後はその目的に沿った学習をする必要もありそうです。

続・WCF最初の一歩

基本的なWCFサービスと呼び出しを作る6つのタスクと前回までの終了分

  • サービスコントラクトを定義する
  • サービス コントラクトを実装する
  • 基本的なサービスをホストおよび実行する
  • クライアントを作成する
  • クライアントを構成する
  • クライアントを使用する

そんなわけで、今回はクライアントを作っていきます。

クライアントを作成する

プロジェクト作成

前回サービスを作ったプロジェクトと同じソリューションにクライアントのプロジェクトを作成します。クライアントもコンソールアプリケーションで作るみたいです。
だいたいこんな感じになるはずです。

WCFTutrial.sln
Client/
  Client.proj
  Program.cs
Service/
  Service.proj
  ICalculator.cs
  Program.cs
参照追加

ClientプロジェクトにSystem.ServiceModelの参照を追加します。
次にClientプロジェクトの生成時にできたProgram.csにSystem.ServiceModel名前空間を追加します。

サービスを作動させる

前回作成したサービスを起動します。
今回はとりあえずbin/Debugの下にできているService.exeを起動させておきます。
一応ブラウザでhttp://localhost:8000/ServiceModelSample/Serviceを見てサービスが動いているか確認しておきます。

Service Model Metadata Utility Toolでクライアント用のサービスプロキシを作成する

手順(ほぼチュートリアルのまんま)
1. スタートすべてのプログラムMicrosoft Visual Studio 2008Visual Studio ToolsVisual Studio 2008 コマンド プロンプトを起動(チュートリアルではWindows SDKにあると記述してあるが無かったので)
2. 起動したコマンドプロンプトsvcutilにパスが通っているか確認。使い方が出てきたらOK

c:\Program Files\Microsoft Visual Studio 9.0\VC>svcutil

3. Clientプロジェクトまでコマンドプロンプトで移動
4. Clientプロジェクトにいる状態で以下のコマンドを発行

C:\WCFTutorial\Client>svcutil /language:cs /out:generatedProxy.cs /config:app.config http://localhost:8000/ServiceModelSample/Service

5. ClientプロジェクトにgeneratedProxy.csが生成されているのでプロジェクトに追加する。

■svcutilのオプションと引数
詳細な説明はMSDNServiceModel メタデータ ユーティリティ ツール (Svcutil.exe)を参照するといいかも。

オプション 説明 既定値 短縮形
/language: 生成するコードの言語を指定 c#、cs、csharp、vbvisualbasicc++、cpp csharp /l
/out: 生成されるコードのファイル名を指定 任意 WSDLの定義名・サービス名から派生。今回ならCalculatorService.csが既定 /o
/config: 生成される構成ファイルの名前を指定 任意 output.config 無し

引数は、今回はオンラインからのメタデータのダウンロードの方法を使っているのでサービスエンドポイントのurlを直接指定している。

svcutil実行結果

メタデータをダウンロードし、ファイルを生成しているようです。

C:\WCFTutorial\Client>svcutil /language:cs /out:generatedProxy.cs /config:app.config http://localhost:8000/ServiceModelSample/Service
Microsoft (R) Service Model Metadata Tool
[Microsoft(R) Windows(R) Communication Foundation、バージョン 3.0.4506.2152]
Copyright (c) Microsoft Corporation.  All rights reserved.

WS-Metadata Exchange または DISCO を使用して 'http://localhost:8000/ServiceModel
Sample/Service' からメタデータをダウンロードしようとしています。
ファイルを生成しています...
C:\Documents and Settings\Owner\My Documents\Visual Studio 2008\Projects\WCFTuto
rial\Client\generatedProxy.cs
C:\Documents and Settings\Owner\My Documents\Visual Studio 2008\Projects\WCFTuto
rial\Client\app.config

C:\WCFTutorial\Client>
生成ファイル

今回の生成ファイルを見る限りでは、それほど大掛かりな自動生成をしているわけではなさそう。型付けDataSetみたいのを予想してたので意外。これなら手書きでも書けそう、というか探してみると、書く手法も普通にあるみたい。

[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
[System.ServiceModel.ServiceContractAttribute(Namespace="http://SuperlightBrothers.Samples", ConfigurationName="ICalculator")]
public interface ICalculator
{
    
    [System.ServiceModel.OperationContractAttribute(Action="http://SuperlightBrothers.Samples/ICalculator/Add", ReplyAction="http://SuperlightBrothers.Samples/ICalculator/AddResponse")]
    double Add(double n1, double n2);
    
    [System.ServiceModel.OperationContractAttribute(Action="http://SuperlightBrothers.Samples/ICalculator/Subtract", ReplyAction="http://SuperlightBrothers.Samples/ICalculator/SubtractResponse")]
    double Subtract(double n1, double n2);
    
    [System.ServiceModel.OperationContractAttribute(Action="http://SuperlightBrothers.Samples/ICalculator/Multiply", ReplyAction="http://SuperlightBrothers.Samples/ICalculator/MultiplyResponse")]
    double Multiply(double n1, double n2);
    
    [System.ServiceModel.OperationContractAttribute(Action="http://SuperlightBrothers.Samples/ICalculator/Divide", ReplyAction="http://SuperlightBrothers.Samples/ICalculator/DivideResponse")]
    double Divide(double n1, double n2);
}

[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
public interface ICalculatorChannel : ICalculator, System.ServiceModel.IClientChannel
{
}

[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
public partial class CalculatorClient : System.ServiceModel.ClientBase<ICalculator>, ICalculator
{
    
    public CalculatorClient()
    {
    }
    
    public CalculatorClient(string endpointConfigurationName) : 
            base(endpointConfigurationName)
    {
    }
    
    public CalculatorClient(string endpointConfigurationName, string remoteAddress) : 
            base(endpointConfigurationName, remoteAddress)
    {
    }
    
    public CalculatorClient(string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) : 
            base(endpointConfigurationName, remoteAddress)
    {
    }
    
    public CalculatorClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) : 
            base(binding, remoteAddress)
    {
    }
    
    public double Add(double n1, double n2)
    {
        return base.Channel.Add(n1, n2);
    }
    
    public double Subtract(double n1, double n2)
    {
        return base.Channel.Subtract(n1, n2);
    }
    
    public double Multiply(double n1, double n2)
    {
        return base.Channel.Multiply(n1, n2);
    }
    
    public double Divide(double n1, double n2)
    {
        return base.Channel.Divide(n1, n2);
    }
}

今回は一つしか進められませんでした0;)
続きは次回。

WCF最初の一歩

と題しまして、じぇんじぇんわからないのでまずはチュートリアル的なものをやってみます。
お題はMSDNWindows Communication Foundation チュートリアル入門を使います。

基本的なWCFサービスと呼び出しを作る6つのタスク

  • サービスコントラクトを定義する
  • サービス コントラクトを実装する
  • 基本的なサービスをホストおよび実行する
  • クライアントを作成する
  • クライアントを構成する
  • クライアントを使用する

と言われてもどうすればいいのかサッパリわかりませんよと。
では順番にやっていきます。

とりあえずプロジェクトを作る

チュートリアルではコンソールアプリとして作っています。
参照設定にSystem.ServiceModelを追加する以外は普通のコンソールアプリです。

サービス コントラクトを定義する

まずは作成するサービスの内容をインターフェースで定義するみたいです。
Service.csファイルを新規作成し、以下のように記述します。

using System;
using System.ServiceModel;

namespace SuperlightBrothers.Samples {
    [ServiceContract(Namespace = "http://SuperlightBrothers.Samples")]
    public interface ICalculator {
        [OperationContract]
        double Add(double n1, double n2);
        [OperationContract]
        double Subtract(double n1, double n2);
        [OperationContract]
        double Multiply(double n1, double n2);
        [OperationContract]
        double Divide(double n1, double n2);
    }
}
付加した属性について

簡単に言うとこんな感じ(?)

ServiceContractAttribute WCFサービスを提供するという定義
OperationContractAttribute WCFサービスの操作を提供するという定義

ServiceContractに付けてるNameSpaceチュートリアルに従ってつけているだけです。

サービス コントラクトを実装する

次に定義したサービスインターフェースの実装を作ります。今回はチュートリアルに従って同じファイルに記述します。

using System;
using System.ServiceModel;

namespace SuperlightBrothers.Samples {
    [ServiceContract(Namespace = "http://SuperlightBrothers.Samples")]
    public interface ICalculator {
        [OperationContract]
        double Add(double n1, double n2);
        [OperationContract]
        double Subtract(double n1, double n2);
        [OperationContract]
        double Multiply(double n1, double n2);
        [OperationContract]
        double Divide(double n1, double n2);
    }

    public class CalculatorService : ICalculator {
        public double Add(double n1, double n2) {
            return n1 + n2;
        }

        public double Subtract(double n1, double n2) {
            return n1 - n2;
        }

        public double Multiply(double n1, double n2) {
            return n1 * n2;
        }

        public double Divide(double n1, double n2) {
            return n1 / n2;
        }
    }
}

基本的なサービスをホストおよび実行する

次にWCFサービスをホストしてみるみたいです。チュートリアルではとりあえずHTTPでセルフホストするようです。
他にもサービスの提供方法はTCPやMSMQとかいろいろあるみたい。ホストもセルフ以外にIISとかでもできるみたい。
コンソールアプリプロジェクト生成時に追加されたProgram.csファイルを以下のように編集します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Description;

namespace SuperlightBrothers.Samples {
    class Program {
        static void Main(string[] args) {
            var selfHost = new ServiceHost(typeof(CalculatorService), new Uri("http://localhost:8000/ServiceModelSample/Service"));

            try {
                selfHost.AddServiceEndpoint(
                    typeof(ICalculator),
                    new WSHttpBinding(),
                    "http://localhost:8000/ServiceModelSamples/Service/CalculatorService"
                );
                ServiceMetadataBehavior serviceMetadataBehavior = new ServiceMetadataBehavior() { HttpGetEnabled = true };
                selfHost.Description.Behaviors.Add(serviceMetadataBehavior);

                selfHost.Open();

                Console.WriteLine("Enterキーで終了");
                Console.ReadLine();

                selfHost.Close();

            } catch (CommunicationException ex) {
                Console.WriteLine(ex.Message);
                selfHost.Abort();
            }
        }
    }
}

言われるままに記述。BehaviorのServiceへの追加方法がチュートリアルに記述されていないので注意。たぶんこれでいいはず。

ホストの手順(チュートリアルのまま)
  1. ServiceHostインスタンスを作成。サービスコントラクトの実装クラスの型と、ホストするベースのアドレス(URI)を引数に与える。
  2. ServiceHostにエンドポイントを追加。エンドポイントでは「どのサービスコントラクトを」「どの形式でネットワークに公開し」「どの場所で提供するか」を指定します。(今回はチュートリアルのなすがまま)
  3. メタデータ交換を有効にするため、サービスメタデータの動作を追加。(サービスの動作についてはよくわからないため補足無しで0;)
  4. ホストオープン
  5. ストクローズ
実行する

この段階で実行し、ブラウザでhttp://localhost:8000/ServiceModelSample/Serviceを見ると、『CalculatorService サービス』のサービスを作成した内容とクライアントの作成方法が書かれたページが表示される。
公開方法にWsHttpBindingを指定したためだと思うが、WSDLまで自動生成できるようになってた。すげぇな。

とりあえず今日はここまで、続きは次回。