コンテキストメニュー「送る」C#

私が初めてWindowsアプリを作ったのはMFCダイアログベースのファイル変換ツールでした。ファイル変換ツールなので、ファイルを入力としますが、「送る」メニューに登録することでツールが簡単に使えるようになったのを覚えています。今日はそのC#版。

sendto_example

1.ファイルを受け取るアプリ

まずファイルを受けるソフトから。これは皆様が作るアプリなのでここでは簡単にTXTファイルを受け取って中身を表示するものとして作っておきます。

text_display_form

TextDisplayForm.cs

namespace SendToSupportWinFormsApp
{
    using System;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;
    using System.IO;

    public partial class TextDisplayForm : Form
    {
        public TextDisplayForm()
        {
            InitializeComponent();
        }

        private void TextDisplayForm_Shown(object sender, EventArgs e)
        {
            var args = Environment.GetCommandLineArgs();
            if(args.Length >= 2) {
                textBoxArgs.Text = args.Skip(1).Aggregate((a, b) => a + b);
                if(args.Count() >= 2 && Path.GetExtension(args[1]).ToLower() == ".txt") {
                    textBoxFileText.Text = File.ReadAllText(args[1], Encoding.GetEncoding("Shift-JIS"));
                }
            }
        }
    }
}

[送る]を選択したときのファイルがプログラム名に次ぐ引数として渡されます。引数はEnvironment.GetCommandLineArgs()でstring配列として渡されますので1番目がパスとして扱えばokです。

 

.送るメニューへのショートカット追加

「送る」メニューに上のソフトが表示されるためには、スタートボタンから”プログラムとファイルの検索”に shell:sendto と入力すると開くフォルダにショートカットを作成すればokです。「送る」メニューへの登録/解除はインストール/アンインストールのタイミングが自然でしょうということで、VS2010でセットアッププロジェクトを作成します。

  1. セットアッププロジェクトを右クリック、「追加」->「プロジェクト出力」を選択
  2. 先ほど作ったWinFormsアプリのプロジェクトのプライマリ出力を選択してOK。
  3. 2でできたプライマリ出力をダブルクリック、ファイルシステムが開く
  4. ファイルシステムの左ペーンの”対象コンピューター上のファイルシステム”を右クリックして「特別なフォルダの追加」->「ユーザーの送るメニュー」を選択
  5. 同じくファイルシステムの左ペーンの”アプリケーションフォルダー”を選択、右ペーンに先ほど作ったWinFormsのプライマリ出力があるので右クリックしてショートカットを作成
  6. 5で作ったショートカットを4で作ったユーザーの送るメニューへドラッグアンドドロップ

shortcut_by_project

昔はよかったのですが、.NET4.0以降上記手順で作成されるのは”アドパタイズショートカット”というやつらしく、SendToフォルダへは登録されるものの「送る」メニューに登録されません、ありゃ困った。

 

3.プログラマならコードでしょう

2の問題のため、Orcaを使う方法やvbsを使う方法が紹介されています。が、私はマネージコードしか書きたくないのでC#の方法を紹介。簡単な話で、Installerクラスを使います。このInstallerクラスはセットアッププロジェクトではなくインストールするアプリケーションそのもの、今回でいうとWinFormsアプリのプロジェクトに追加します。ショートカットも作成するので以下を参照追加。

[COM] Windows Script Host Object Model
[.NET] System.Configuration.Install

前者はプロジェクトの参照設定に “IWshRuntimeRubrary” と表示されますが、このプロパティを開き [相互運用機能の埋め込み] をFalseにしておきます。

ひとてま2

コードはこちら。

AddSendToInstaller.cs

namespace SendToSupportWinFormsApp
{
    using System;
    using System.IO;
    using System.Configuration.Install;
    using System.Reflection;
    using System.ComponentModel;
    using IWshRuntimeLibrary;

    [RunInstaller(true)]
    public class AddSendToInstaller : Installer
    {
        private const string shortcutFilePathKey = "shortcutFilePath";
        public override void Install(System.Collections.IDictionary stateSaver) {
            // Debug時は以下を有効に
            // System.Diagnostics.Debugger.Launch();

            string targetPath = Assembly.GetExecutingAssembly().Location;
            string fileName = Path.GetFileNameWithoutExtension(targetPath);
            string shortcutPath = Environment.GetFolderPath(Environment.SpecialFolder.SendTo) + @"\" + fileName + ".lnk";
            IWshShell shell = new WshShellClass();
            IWshShortcut shortcut = shell.CreateShortcut(shortcutPath) as IWshShortcut;
            shortcut.TargetPath = targetPath;
            shortcut.Save();

            stateSaver[shortcutFilePathKey] = shortcutPath;
            base.Install(stateSaver);
        }

        public override void Uninstall(System.Collections.IDictionary savedState) {
            // Debug時は以下を有効に
            // System.Diagnostics.Debugger.Launch();

            if(savedState.Contains(shortcutFilePathKey)) {
                string shortcutFilePath = savedState[shortcutFilePathKey] as string;
                if(System.IO.File.Exists(shortcutFilePath)) {
                    System.IO.File.Delete(shortcutFilePath);
                }
            }
            base.Uninstall(savedState);
        }
    }
}

この上でセットアッププロジェクトを変更します(2の手順で作ったショートカットは無駄ですので削除します)。

  1. セットアッププロジェクトを右クリックし、「表示」->「カスタム動作」をクリック
  2. カスタム動作のインストールとアンインストールそれぞれについて、右クリックしてカスタム動作の追加をクリックして、アプリケーションフォルダを選択してOK。

custom_action

これをしないと上で作ったインストーラクラスが無視されるので注意。これでセットアッププロジェクトを作ってインストールすると「送る」メニューにアプリが登録されます。

tuika2

実行するとこう。めでたし。

exe

ただVS2012以降セットアッププロジェクトがプロジェクトテンプレートからなくなってしまったのでVS2010までの手法ということになります。InstallShield LimitedEditionを使う必要がありますね。

広告

Windowのキャプチャ(WinForms,WPF)

現在、台風19号が近畿圏に再接近中…。今回は息抜きに、WindowsFormsとWPFそれぞれについてwindowのキャプチャ方法を見ておきます。WPFは結構苦労しているような。

 

1.WindowsFormsの場合

ボタン押下でFormのキャプチャをするような関数は以下のような感じになります。

 Form1.cs

namespace WindowsFormsCapture
{
    using System;
    using System.Drawing;
    using System.Windows.Forms;
    using System.Drawing.Imaging;
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void buttonCapture_Click(object sender, EventArgs e)
        {
            using(Bitmap image = new Bitmap(Width,Height))
            using(Graphics graphics = Graphics.FromImage(image))
            {
                graphics.CopyFromScreen(Location, Point.Empty, image.Size);
                image.Save("Capture.bmp", ImageFormat.Bmp);
            }
        }
    }
}

実際のキャプチャ画像はこちら。

Capture

 

2.WPFの場合

WPFの場合も基本は同じ、ウィンドウの位置,サイズの取得方法が変わるだけです。

MainWindow.xaml.cs

namespace WpfCapture
{
    using System.Windows;
    using System.Windows.Media;
    using System.Drawing;
    using System.Drawing.Imaging;
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var brush = new VisualBrush(this);
            using (Bitmap image = new Bitmap((int)ActualWidth, (int)ActualHeight))
            using (Graphics graphics = Graphics.FromImage(image))
            {
                graphics.CopyFromScreen(new System.Drawing.Point((int)Left, (int)Top), System.Drawing.Point.Empty, image.Size);
                image.Save("Capture.bmp", ImageFormat.Bmp);
            }
        }
    }
}

こちらも蛇足ながらキャプチャ画像はこちら。

Capture

CLRイベントとWPF Routedイベントの違い

いきなりですが問題です。CLRイベントとWPFのRoutedイベント、Routedイベントにはトンネル/バブルモードはありますが、それ以外に違いはあるでしょうか?

はい、なにRoutedイベントはXAMLで指定できるがCLRイベントはXAMLで指定できない? えー残念ながら間違いです。例を見ながら確認しておきましょう。

ButtonWithClrEvent.cs

namespace RoutedEventAndClrEvent
{
    using System;
    using System.Windows.Controls;
    public class ButtonWithClrEvent : Button
    {
        // CLR Event
        public event EventHandler SampleClrEvent = (_, __) => { };

        // click -> Some clr event
        protected override void OnClick()
        {
            SampleClrEvent(this, EventArgs.Empty);
            base.OnClick();
        }
    }
}

これは普通のイベントですね。後の確認用に、clickイベントで該イベントを発砲しておきます。

ButtonWithRoutedEvent.cs

namespace RoutedEventAndClrEvent
{
    using System.Windows;
    using System.Windows.Controls;
    public class ButtonWithRoutedEvent : Button
    {
        // Routed Event
        public static readonly RoutedEvent SampleRoutedEvent =
            EventManager.RegisterRoutedEvent("SampleRouted",
            RoutingStrategy.Bubble,
            typeof(RoutedEventHandler),
            typeof(ButtonWithRoutedEvent));

        // CLR accessors
        public event RoutedEventHandler SampleRouted
        {
            add { AddHandler(SampleRoutedEvent, value); }
            remove { RemoveHandler(SampleRoutedEvent, value); }
        }

        // click -> RoutedEvent
        protected override void OnClick()
        {
            RaiseEvent(new RoutedEventArgs(SampleRoutedEvent));
            base.OnClick();
        }
    }
}

次いでRoutedイベント(日本語で読むときはルーティングイベント)、CLRアクセサでAddHandler, RemoveHandlerをラップするのは定石です。まずこれらがXAMLで指定できるか?というところを確認しておきます。

 MainWindow.xaml

<Window x:Class="RoutedEventAndClrEvent.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:me="clr-namespace:RoutedEventAndClrEvent"
        Title="MainWindow" Height="138" Width="235">
    <StackPanel>
        <me:ButtonWithRoutedEvent x:Name="RoutedEventButton" SampleRouted="SampleRoutedEventHandler">Routed Event</me:ButtonWithRoutedEvent>
        <me:ButtonWithClrEvent x:Name="ClrEventButton" SampleClrEvent="SampleClrEventHanlder">CLR Event</me:ButtonWithClrEvent>
    </StackPanel>
</Window>

 MainWindow.xaml.cs

namespace RoutedEventAndClrEvent
{
    using System;
    using System.Windows;
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        // Routedイベントハンドラ
        private void SampleRoutedEventHandler(object sender, RoutedEventArgs e){
            MessageBox.Show("Routed Event Occured int the instance context.");
        }

        // CLRイベントハンドラ
        private void SampleClrEventHanlder(object sender, EventArgs e){
            MessageBox.Show("CLR Event Occured int the instance context.");
        }
    }
}

mainwindow

問題なく動作します。

ではCLRイベントとRoutedイベントは何が違うのでしょうか?上の例ではイベントハンドラにインスタンスメソッドを割り当てていますが、staticメソッドを割り当てた時に違いが見えてきます。大量のコピペになるので省略しますが、両ハンドラを単純にstaticにしてみてください。いずれも実行時にXAMLエラーが発生します。それぞれstaticメソッドをアタッチするときは下のようにします。

 MainWindow.xaml.cs

namespace RoutedEventAndClrEvent
{
    using System;
    using System.Windows;
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            RoutedEventButton.SampleRouted += SampleRoutedEventHandler;

            // Routedイベントハンドラ<クラスレベルハンドラ>
            EventManager.RegisterClassHandler(
                typeof(ButtonWithRoutedEvent),
                ButtonWithRoutedEvent.SampleRoutedEvent,
                new RoutedEventHandler(SampleStaticRoutedEventHandler));
            // CLRイベントハンドラ
            ClrEventButton.SampleClrEvent += SampleStaticClrEventHanlder;
        }

        // Routedイベントハンドラ
        private static void SampleStaticRoutedEventHandler(object sender, RoutedEventArgs e){
            MessageBox.Show("Routed Event Occured in the static context.");
        }
        private void SampleRoutedEventHandler(object sender, RoutedEventArgs e){
            MessageBox.Show("Routed Event Occured int the instance context.");
        }

        // CLRイベントハンドラ
        private static void SampleStaticClrEventHanlder(object sender, EventArgs e){
            MessageBox.Show("CLR Event Occured int the static context.");
        }
    }
}

CLRイベントのほうは何も変わっていないので良いとして、Routedイベントはクラスレベルハンドラというものが登場します。これがCLRイベントとRoutedイベントの違いです!

クラスレベルハンドラとは、他のインスタンスハンドラよりも先だって呼ばれるハンドラです。RoutedEventはRoutedEventArgs内のHandledをtrueにするとイベントルーティングを停止させられますが、当然クラスレベルハンドラにもあります。よって各インスタンスハンドラに渡る前に統一的に処理をする場合に使えるということです。トンネルとバブルも含めてイベントの発生順を図示すると以下のようになります。

event

チュートリアル:WPF時計の作成(3)

WPFのチュートリアル、勝手に続編です。前回はこちら。

 

1.時刻読みあげ機能

こんな機能いるかと思いつつも、今後への布石も兼ねて作ってみました。WPFらしくカスタムトリガとカスタムアクションを使います。「〇〇が起きたときに、△△する」の〇〇の部分がトリガ、△△の部分がアクションです。今回は「時計の秒針が0秒、つまり正分になったときに現在時刻を読みあげる」という機能にします。

カスタムトリガ、カスタムアクションいずれもBlendの SDKを使います。これは少し前にビヘイビアの時に紹介しました。まずはカスタムトリガから。

OclockTrigger.cs

namespace Wpf2DAnalogClock
{
    using System;
    using System.Windows.Shapes;
    using System.Windows.Media;
    using System.Windows.Interactivity;

    public class OclockTrigger : TriggerBase<Path>
    {
        private RotateTransform rotransform;
        private double previous = 0;
        protected override void OnAttached()
        {
            base.OnAttached();
            rotransform = base.AssociatedObject.RenderTransform as RotateTransform;
            if (rotransform != null){
                rotransform.Changed += rotransform_Changed;
            }
        }

        protected override void OnDetaching()
        {
            if (rotransform != null){
                rotransform.Changed -= rotransform_Changed;
            }
            base.OnDetaching();
        }

        void rotransform_Changed(object sender, EventArgs e)
        {
            double current = rotransform.Angle % 360;
            if (current < previous){
                this.InvokeActions(DateTime.Now);
            }
            previous = current;
        }
    }
}

正分なのでoclockという名前はおかしいですが、それはさておき。カスタムトリガはTriggerBase<T>から派生します。Tはトリガ発生元のオブジェクト、今回は秒針の針なのでPathオブジェクトになります。特定のタイミングを判定して、いまだ!という時にthis.InvokeActions()をコールします。引数はActionへ渡すobject型の引数で、今回は現在時刻を渡しています。秒針はDoubleAnimationで動いているためchangedハンドラで取得できる値は0とか1とか整数ではありません。そのため前回角度を保持しておき、360→0の切り替わりポイントを正分と判断しています。次いでカスタムアクション。

 TimeSignalAction.cs

namespace Wpf2DAnalogClock
{
    using System;
    using System.Windows.Media;
    using System.Windows.Shapes;
    using System.Windows.Interactivity;
    using System.Speech.Synthesis;

    public class TimeSignalAction : TriggerAction<Path>
    {
        private RotateTransform rotation;
        private SpeechSynthesizer synthesizer;

        public TimeSignalAction(){
            synthesizer = new SpeechSynthesizer();
        }

        protected override void Invoke(object parameter)
        {
            DateTime currentTime = (DateTime)parameter;
            string prompt = "It is" + currentTime.Hour + "," + currentTime.Minute + ".";
            synthesizer.SpeakAsync(prompt);
        }
    }
}

WPFと同時に紹介されたSpeechAPIを使っていますので、System.Speech.dllを参照追加が必要です。イントネーションを調整したい場合はSSMLを使って調整が可能ですが時刻読みあげならこれで十分です。”It is”を”It’s”にすると”It has”と発音されたり、ピリオドがないと語尾が抜けた感じになったりするのでちょっとばかし注意。デフォルトだと女の人の声です。

これを適用するのはこんな感じです。前回のXAMLの一部を変えています。

 MainWindow.xaml(抜粋)

<Path x:Name="secondArrow" Data="M0,0 -2,15 0,70 2,15 z" Margin="98.75,30.5,155,172.5" RenderTransformOrigin="0.5,1" Stretch="Fill">
    <Path.Fill>
        <LinearGradientBrush EndPoint="1,1" StartPoint="0,0">
            <GradientStop Color="#FFF30E0E" Offset="0"/>
            <GradientStop Color="White" Offset="1"/>
        </LinearGradientBrush>
    </Path.Fill>
    <Path.RenderTransform>
        <RotateTransform x:Name="secondTransform"/>
    </Path.RenderTransform>
    <i:Interaction.Triggers>
        <me:OclockTrigger>
            <me:TimeSignalAction/>
        </me:OclockTrigger>
    </i:Interaction.Triggers>
</Path>

 

2.光の照射方向を調整する

前回までのアナログ時計で、時計の針にはLineargradientBrushを使いました。これが光の影響を模擬したものだと考えると、時計の針が回転しているにも関わらず根元が明るいとか光の当たり方が一定になっておらずへんな感じになります。そこで回転角に応じてグラデーション方向を調整するビヘイビアを作ってみます。

SameDirectionalLightBehavior.cs

namespace Wpf2DAnalogClock
{
    using System;
    using System.Windows;
    using System.Windows.Media;
    using System.Windows.Shapes;
    using System.Windows.Interactivity;

    public class SameDirectionalLightBehavior : Behavior<Path>
    {
        private RotateTransform _transform;
        protected override void OnAttached()
        {
            base.OnAttached();
            _transform = this.AssociatedObject.RenderTransform as RotateTransform;
            if (_transform == null){
                return;
            }
            _transform.Changed += transform_Changed;
        }
        protected override void OnDetaching()
        {
            if (_transform != null){
                _transform.Changed -= transform_Changed;
            }
            base.OnDetaching();
        }

        void transform_Changed(object sender, EventArgs e)
        {
            LinearGradientBrush brush = base.AssociatedObject.Fill as LinearGradientBrush;
            if (brush == null){
                return;
            }
            double radian = _transform.Angle / 180.0 * Math.PI;
            // 以下、改善余地あり
            brush.StartPoint = new Point(0.5+0.5*Math.Cos(radian), 0.5+0.5*Math.Sin(radian));
            brush.EndPoint = new Point(0.5-0.5*Math.Cos(radian), 0.5+0.5-Math.Sin(radian)); 
        }
    }
}

回転角に応じてLineargradientBrushのStartPoint, EndPointを回転させています。が、まだちょっと調整不足感があります。(0,0)-(1,1)の間に設定必要だからSin/Cosでなく45°単位で区切ってTanを使わないといけないのかなぁ、と思ったり。これを使うのは下のような感じ。

MainWindow.xaml(同部分の抜粋)

<Path x:Name="secondArrow" Data="M0,0 -2,15 0,70 2,15 z" Margin="98.75,30.5,155,172.5" RenderTransformOrigin="0.5,1" Stretch="Fill">
    <Path.Fill>
        <LinearGradientBrush EndPoint="1,1" StartPoint="0,0">
            <GradientStop Color="#FFF30E0E" Offset="0"/>
            <GradientStop Color="White" Offset="1"/>
        </LinearGradientBrush>
    </Path.Fill>
    <Path.RenderTransform>
        <RotateTransform x:Name="secondTransform"/>
    </Path.RenderTransform>
    <i:Interaction.Behaviors>
        <me:SameDirectionalLightBehavior/>
    </i:Interaction.Behaviors>
    <i:Interaction.Triggers>
        <me:OclockTrigger>
            <me:TimeSignalAction/>
        </me:OclockTrigger>
    </i:Interaction.Triggers>
</Path>

 

今回はBlendSDKのカスタムトリガ、カスタムアクション、カスタムビヘイビアを使った続編でした。

WPFコントロールテンプレートとデータテンプレートの違い

WPFのコントロールは、

  1. CheckBoxのような単一のアイテムを保有するコンテンツコントロール
  2. ListBoxのような複数のアイテムを保有するアイテムコントロール
  3. Gridのようなレイアウトを制御するパネルコントロール

の3種があります。今回は1, 2のコンテンツコントロール、アイテムコントロールを対象とした『テンプレート』についてのお話。コンテンツコントロール、アイテムコントロールいずれも両テンプレートをそれぞれ以下のプロパティ名で公開しています。

template_property

以下Buttonを例に、簡単に違いを見ておきます。

 

1.コントロールテンプレート

コントロールテンプレートは「コントロール自身の見た目を決定するもの」と言えます。良く言われる話ですがWPFのコントロールはルックレス[LookLess]、つまり基本的には見た目を持ちません。

そのため通常のButtonは、 normal_button_image←こんなですが、

内部的には normal_button←のような構成を取っています。

この一番下部にあるButtonがWPFのButtonです。ButtonChromeやContentPresenterが見た目を制御していると言って良いでしょう。

見た目をいじったものとして、control_template_image←こんなButtonがすぐ作れますが、これは

内部的にはcontent_template←のような構成を取っています。

これを構成するXAMLを見ておくと、

<Button>
    コンテンツ
    <Button.Template>
        <ControlTemplate TargetType="Button">
            <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}">
                <ContentPresenter/>
            </Border>
        </ControlTemplate>
    </Button.Template>
</Button>

こんな感じ。ControlTemplateにターゲットタイプを指定していることに注意、これはContentPresenterが動作するために必要です。というのもContentPresenterは内部でターゲットタイプを必要とするBindingを持っているため。ContentPresenterを持たないボタンなら不要です。TemplateBindingはこのControlTemplateが割り当てられる先(つまりButton)のプロパティと同じ値を取ります、というものです。上の例で見るように、Buttonを例にするとコントロールテンプレートとは、『ボタンとは四角くて、周囲が少し縁取られており、マウスがホバーされると色が変わり、コンテンツをコントロールの中心に持った…』という部分をカスタマイズするものです。

ちなみに上で示したButtonのテンプレート構成図は

var control = Application.Current.FindResource(typeof(Button));
using(XmlTextWriter writer = new XmlTextWriter(@"DefaultButtonTemplate.xml", Encoding.UTF8)){
    writer.Formatting = Formatting.Indented;
    XamlWriter.Save(control, writer);
}
Console.WriteLine(XamlWriter.Save(this));

こんなコードで出力することができます。

 

 

2.データテンプレート

データテンプレートは「コントロールに割り当てられたデータの見た目を定義するもの」と言えます。コントロールテンプレートがボタンの”コントロール”としての部分をカスタマイズするものなのに対し、データテンプレートはそれに表示されるコンテンツの部分をカスタマイズするものです。

私的な印象では、データテンプレートの99% はアイテムコントロールで使われるのですが(リスト形式のデータを表現するのに便利だから)、コンテンツコントロールにもムリヤリ適用するとこんな感じ→ datatemplate_image

コントロールテンプレートはいじってないのでボタンらしい感じながら、表示されているものがカスタマイズされた感じなことに気付くと思います。これを実現するデータテンプレートは参考までにこんな感じです。

<Button x:Name="dataTemplatedButton" Margin="50,50,50,50">
    <Button.ContentTemplate>
        <DataTemplate>
            <StackPanel Orientation="Vertical">
                <TextBlock Text="{Binding}" FontSize="12"/>
                <TextBlock Text="{Binding}" FontSize="8"/>
            </StackPanel>
        </DataTemplate>
    </Button.ContentTemplate>
</Button>

上のような「サイズ違いで2コ同じコンテンツを並べたい」等の場合はデータテンプレートを使います。

 

3.どちらを使うか

コントロールテンプレートは多くの場合TemplateBindingを使い、テンプレート親とのコントロールとしてのデータとのバインドを作成します。一方でデータテンプレートはBindingを使い、割り当てられたデータとのバインドを作成します。カスタマイズしたいものがコントロール自身なのか、割り当てられるデータなのか、でどちらを使うかが決まります。

状況によっては、コントロールテンプレート、データテンプレートどちらを使っても可能なことがあります。この場合これらテンプレートを再利用する時のことを考えると良いと思います。そのカスタマイズした結果できるであろうコントロールに、別のデータを載せることがあるのかどうか、を考えてみてください。別のデータを載せることもある場合はコントロールテンプレート、別のデータを載せない場合はデータテンプレートと判断してよいかと思います。

まぁコンテンツコントロールの場合はコントロールテンプレート、アイテムコントロールの場合はデータテンプレートと言ってもかなりの割合で正しい選択になるとは思います;

WCF”ビヘイビア”なるもの

WPFでのビヘイビアとは、Viewに関連する振る舞い(Behavior)を再利用可能な形で作成したものでした。WCFにもビヘイビアというものがありWCFでのビヘイビアとは、WCFでの通信時の特定のタイミングで何らかの振る舞い(Behavior)を発動させるためのものです。

WPFのビヘイビアはコミュニティの間で「そう呼ばれているもの」 (それがBlendで採用され公にはなっている)ですが、WCFのビヘイビアはそういうクラスがあり「もともと正式なもの」という違いがあります。

 

1.WCFビヘイビアの種類

WCFのビヘイビアとは大きく下記4種あるようです。

①サービスビヘイビア(IServiceBehaviorを実装する)
サーバ側に追加する機能追加モジュールのこと。WSDLファイルによりサーバが提供する機能を公開することができるが、サービスビヘイビアの一種であるserviceMetadataビヘイビアを使うとこのWSDLファイル作成を自動作成&公開できる。(このWSDLファイルを公開するエンドポイントがMEXエンドポイントであり、これがあるとユーザは簡単にプロキシクラスや構成ファイルが作成できるようになる)

②エンドポイントビヘイビア(IEndpointBehaviorを実装する)
サービスビヘイビアがサーバ側に対するビヘイビアだったのに対し、エンドポイントビヘイビアはエンドポイント単位のビヘイビアです。ただし良い実例を見かけません;

③コントラクトビヘイビア(IContractBehaviorを実装する)
コントラクト単位でのビヘイビアです、こちらも良い実例を見かけません;

④操作ビヘイビア(IOperationBehaviorを実装する)
Interfaceとして定義するWCFサービスにはOperationContract属性をつけます。このInterfaceのメソッドに特性を追加するのが操作ビヘイビアです。代表例として、トランザクション操作を実装する操作ビヘイビアがあります(OperationBehaviorにTransactionScopeRequired=trueとして指定)。

これらのスコープや、構成方法についてはこちらを参照ください。

 

2.WCFビヘイビアの利用

上記①に記載したMEXエンドポイントの公開方法を見ておきます。迷子にならないようにまず全体のプロジェクト・ファイル構成を見ておきます(下図)。いずれもコンソールアプリです。

project
サービスを公開するためサーバー側から作成します。まずはコントラクトから。

コントラクトとしての、ISampleService.cs

namespace WcfBehavior.TestServer
{
    using System.ServiceModel;
    [ServiceContract]
    interface ISampleService
    {
        [OperationContract]
        string Get();
    }
}

これを実際に実装する、TestService.cs

namespace WcfBehavior.TestServer
{
    using System;

    class TestService : ISampleService
    {
        public string Get()
        {
            return DateTime.Now.ToString();
        }
    }
}

App.Config<without MEX>

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <system.serviceModel>
    <services>
      <service name="WcfBehavior.TestServer.TestService">
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:8000/WcfBehavior"/>
          </baseAddresses>
        </host>
        <endpoint address ="WcfBehaviorTest"
                  binding="basicHttpBinding"
                  contract="WcfBehavior.TestServer.ISampleService"/>
      </service>
    </services>
  </system.serviceModel>
</configuration>

これにMEXを追加した、App.config<with MEX>

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <system.serviceModel>
    <services>
      <service name="WcfBehavior.TestServer.TestService"
               behaviorConfiguration ="SampleMexBehavior">
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:8000/WcfBehavior"/>
          </baseAddresses>
        </host>
        <endpoint address ="WcfBehaviorTest"
                  binding="basicHttpBinding"
                  contract="WcfBehavior.TestServer.ISampleService"/>
        <endpoint address="WcfBehaviormex"
                  binding="mexHttpBinding"
                  contract="IMetadataExchange"/>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="SampleMexBehavior">
          <serviceMetadata httpGetEnabled="True"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

MEXはWSDLを送信するエンドポイントなのでエンドポイントとして定義が必要です。また公開されるISampleServiceはWSDLをHttp経由で取得できるようにする、というビヘイビアを設定しています(上記、behaviorConfiguration)。最後にサーバ側メイン処理。

 Program.cs(サーバー側)

namespace WcfBehavior.TestServer
{
    using System;
    using System.ServiceModel;
    class Program
    {
        static void Main(string[] args)
        {
            ServiceHost host = new ServiceHost(typeof(TestService));
            host.Open();
            Console.WriteLine("Press Enter to Exit.");
            Console.ReadLine();
        }
    }
}

このサーバーConsoleアプリを管理者として実行した状態で、クライアント側作成に移ります。

クライアント側ではサーバで公開したMEXエンドポイントを利用します。まず、プロジェクトを右クリックして、[追加] – [サービスの追加] を開き、アドレスを指定して [移動] を押すと

add_service

このようにサービスが見つかります。サービスが見つからない場合、

  • サーバーが起動状態か?
  • MEXエンドポイントが公開されているか?

を確認ください。名前空間は今回このままServiceReference1としておきますが、これでOkを押すとApp.configが自動的に更新され、プロキシクラスも作成されます(ぱっと見見えません)。これの使い方は非常に簡単!

Program.cs(クライアント側)

namespace WcfBehavior.TestClient
{
    using System;
    class Program
    {
        static void Main(string[] args)
        {
            var proxy = new ServiceReference1.SampleServiceClient();
            Console.WriteLine(proxy.Get());
            Console.WriteLine("Press Enter to Exit.");
            Console.ReadLine();
        }
    }
}

サービス名+Clientがプロキシクラスの名前になるようです。

MEXの説明がメインになりましたが、WCFビヘイビアを使うことで他にも、同時実行数や同時呼び出し数を制限したり,セキュリティを設けたり,通信時のログをとったり、といった目的で使えます。

 

3.ビヘイビアの自作

MSDNに詳しく記載があります。どのビヘイビアを作るか?によってInterfaceが決まりますが、今回は何もしないサービスビヘイビアを作ってみます。サーバ側に下記2ファイルを追加します。

CustomBehavior.cs

namespace WcfBehavior.TestServer
{
    using System;
    using System.Collections.ObjectModel;
    using System.ServiceModel;
    using System.ServiceModel.Description;
    using System.ServiceModel.Channels;

    public class CustomBehavior : IServiceBehavior
    {
        public void AddBindingParameters(ServiceDescription serviceDescription, 
            ServiceHostBase serviceHostBase, 
            Collection<ServiceEndpoint> endpoints, 
            BindingParameterCollection bindingParameters)
        {
            Console.WriteLine("AddBindingParameters has called");
        }

        public void ApplyDispatchBehavior(ServiceDescription serviceDescription, 
            ServiceHostBase serviceHostBase)
        {
            Console.WriteLine("CustomBehavior has applied.");
        }

        public void Validate(ServiceDescription serviceDescription, 
            ServiceHostBase serviceHostBase)
        {
            Console.WriteLine("Validate has called.");
        }
    }
}

このビヘイビアを構成ファイルから使えるようにするためにもう一つ、下記クラスが必要となります。

CustomBehaviorElement.cs

namespace WcfBehavior.TestServer
{
    using System;
    using System.ServiceModel.Configuration;
    public class CustomBehaviorElement : BehaviorExtensionElement
    {
        public override Type BehaviorType
        {
            get { return typeof(CustomBehavior); }
        }

        protected override object CreateBehavior()
        {
            return new CustomBehavior();
        }
    }
}

このクラスを作る場合、System.Configurationへの参照が必要になります。これを構成ファイルから使うようにするにはサーバー側のApp.configを以下のように変更します。

App.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <system.serviceModel>
    <services>
      <service name="WcfBehavior.TestServer.TestService"
               behaviorConfiguration ="SampleMexBehavior">
        <host>
          <baseAddresses>
            <add baseAddress="http://localhost:8000/WcfBehavior"/>
          </baseAddresses>
        </host>
        <endpoint address ="WcfBehaviorTest"
                  binding="basicHttpBinding"
                  contract="WcfBehavior.TestServer.ISampleService"/>
        <endpoint address="WcfBehaviormex"
                  binding="mexHttpBinding"
                  contract="IMetadataExchange"/>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="SampleMexBehavior">
          <serviceMetadata httpGetEnabled="True"/>
          <customBehavior/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <extensions>
      <behaviorExtensions>
        <add name="customBehavior"
             type="WcfBehavior.TestServer.CustomBehaviorElement, WcfBehavior.TestServer, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
      </behaviorExtensions>
    </extensions>
  </system.serviceModel>
</configuration>

カスタムビヘイビアの場合、extensionsタグを作って、その中にクラスとビヘイビア名を追加しておく必要があります。こうすることで少し上のserviceBehaviorsでカスタムビヘイビアを参照できるようになります。これは一番上でサービスのbehaviorConfigurationとして指定されているのでサービスへと適用される訳です。

最後になりましたが、これを動かしたのは以下。

activate_custombehavior

サーバ側でConsoleへの出力が確認できると思います(これはクライアントを起動させずにサーバ側起動直後に全文出力されます)。

WCFによるP2Pチャットソフト

やってみたかったWCFでP2P。P2Pの利用法としてはベタなチャットソフト作成して方法を確認しておきます。

1.通信内容を決める

WCFで通信するときはコントラクトとして通信内容をInterfaceで実装するんでした、こちら。

 ITocsChat.cs

namespace WcfP2PChat
{
    using System.ServiceModel;
    [ServiceContract(CallbackContract=typeof(ITocsChat))]
    public interface ITocsChat
    {
        [OperationContract(IsOneWay = true)]
        void Join(string userName);

        [OperationContract(IsOneWay = true)]
        void Chat(string userName, string message);

        [OperationContract(IsOneWay = true)]
        void Leave(string userName);
    }
}

このInterfaceとP2Pの送信・応答チャネル定義をするIClientChannelの2つのInterfaceをもつInterfaceを定義しておきます。P2Pではこの2つのInterafaceを持つほうを使います。ただし、このInterfaceクラスを明示的に実装することはありません。

 ITocsChatChannel.cs

namespace WcfP2PChat
{
    using System.ServiceModel;
    public interface ITocsChatChannel : ITocsChat, IClientChannel
    {
    }
}

 

2.P2Pのエンドポイントを構成する

コードからでも問題ないと思いますが、今回はアプリケーション構成ファイルを使います。

App.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup>
      <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
  <system.serviceModel>
    <client>
      <endpoint name="TocsSimpleChatApp"
                address="net.p2p://TocsMesh/TocsChat"
                binding="netPeerTcpBinding"
                bindingConfiguration="TocsChatBinding"
                contract="WcfP2PChat.ITocsChat"/>
    </client>
    <bindings>
      <netPeerTcpBinding>
        <binding name="TocsChatBinding" port="0">
          <resolver mode="Auto"/>
          <security mode="None"/>
        </binding>
      </netPeerTcpBinding>
    </bindings>
  </system.serviceModel>
</configuration>

ChatForm.cs(抜粋)

    public partial class ChatForm : Form, ITocsChat
    {
        private ITocsChatChannel m_participant;

        public ChatForm()
        {
            InitializeComponent();

            var site = new InstanceContext(this);
            var binding = new NetPeerTcpBinding("TocsChatBinding");
            var channelFactory = new DuplexChannelFactory<ITocsChatChannel>(site, "TocsSimpleChatApp");
            m_participant = channelFactory.CreateChannel();

            var statusHandler = m_participant.GetProperty<IOnlineStatus>();
            statusHandler.Online += statusHandler_Online;
            statusHandler.Offline += statusHandler_Offline;
        }
    }

多くの方に理解して頂けるようWindows Forms にしました。FormはITocsChatを実装するようにしていますが、これは他ユーザから送られてきたメッセージをFormで処理します、ということを表しています。またm_participantとしてITocsChannelオブジェクトを取得していますが、これは他ユーザ(群)を表していると考えて良いと思います。

 

3.チャットソフトを作成する

あとはこまごまとした作りこみを少々。本来なら状態に応じてコントロールの有効/無効切り替えや日時表示etc..機能を盛り込むでしょうが、わかりやすさ重視で実装するとこんな感じ。

chat

ChatForm.cs(全体) 

namespace WcfP2PChat
{
    using System;
    using System.Windows.Forms;
    using System.ServiceModel;

    public partial class ChatForm : Form, ITocsChat
    {
        private ITocsChatChannel m_participant;

        public ChatForm()
        {
            InitializeComponent();

            var site = new InstanceContext(this);
            var binding = new NetPeerTcpBinding("TocsChatBinding");
            var channelFactory = new DuplexChannelFactory<ITocsChatChannel>(site, "TocsSimpleChatApp");
            m_participant = channelFactory.CreateChannel();

            var statusHandler = m_participant.GetProperty<IOnlineStatus>();
            statusHandler.Online += statusHandler_Online;
            statusHandler.Offline += statusHandler_Offline;
        }

        void statusHandler_Online(object sender, EventArgs e){
            textBoxChatLog.Text += "接続しました" + Environment.NewLine;
        }
        void statusHandler_Offline(object sender, EventArgs e){
            textBoxChatLog.Text += "切断されました" + Environment.NewLine;
        }

        public void Join(string userName){
            textBoxChatLog.Text += userName + "さんが参加されました" + Environment.NewLine;
        }
        public void Chat(string userName, string message){
            textBoxChatLog.Text += userName + ":" + message + Environment.NewLine;
        }
        public new void Leave(string userName){
            textBoxChatLog.Text += userName + "さんが退出されました" + Environment.NewLine;
        }

        private void buttonJoin_Click(object sender, EventArgs e){
            m_participant.Join(textBoxUserName.Text);
        }
        private void buttonLeave_Click(object sender, EventArgs e){
            m_participant.Leave(textBoxUserName.Text);
        }
        private void buttonSay_Click(object sender, EventArgs e){
            m_participant.Chat(textBoxUserName.Text, textBoxSay.Text);
        }

    }
}

Formが実装するITocsChat Interfaceは他ユーザからの操作に対するcallbackとなるので上のような処理となります。これを動かした模様はこちら。

chatting

こんなに少ないコードながらに結構まともに見えます。

 

補足 – WindowsXPの場合

上記のようにWCFのP2Pでデフォルトリゾルバを使った場合、PNRP Ver2.0となりますがこれはWinXP sp3もしくはKB920342を入れる必要があるようです(こちら)。またPNRPがIPv6の上に構築されるのでIPv6もインストール必要です(こちら)。少なくともこれらが必要なのは間違いありませんが、それで正しく動くかは確認できていません。