AttachedProperty定義の記述量削減(F#)

下記記事にて、C#でAttachedPropertyの記述を削減してみました。
AttachedProperty定義の記述量削減 - 何でもプログラミング

今回はF#でAttachedPropertyの記述を減らしてみたいと思います。

C#版同様、プリプロセスやポストプロセスは利用しないで実装してみます。

AttachedPropertyManager

Register、Get、Set、Changedを公開するクラスになります。

C#の時と異なり、プロパティの変更通知はEventクラスを介して行います。

現状、member valで宣言されたものからのCallerMemberNameには、末尾に@が付与されているようです。

open System.Runtime.CompilerServices

type AttachedProperty<'owner, 'target, 'value when 'target :> DependencyObject>() =
    let changed = Event<'target>()
    let mutable property : DependencyProperty = null
    member this.Changed = changed.Publish
    member this.Register(?defaultValue : 'value, [<CallerMemberName>]?propertyName : string) =
        assert (property = null)
        let name = propertyName.Value.Substring(0, propertyName.Value.Length - "Property@".Length)
        let metadata = PropertyMetadata(defaultValue |> Option.defaultValue(Unchecked.defaultof<'value>), 
                                        PropertyChangedCallback(fun obj _ -> changed.Trigger(obj :?> 'target)))
        property <- DependencyProperty.RegisterAttached(name, typeof<'value>, typeof<'owner>, metadata)
        property
    member this.Get(obj : 'target) = obj.GetValue(property) :?> 'value
    member this.Set(obj : 'target, value : 'value) = obj.SetValue(property, value)


distinct

初回通知の時のみに処理を行う用に、distinctオペレータを定義します。

弱参照になるようConditionalWeakTableを利用しています。

open System.Runtime.CompilerServices

module Event =
    let distinct (source : IEvent<'a>) = 
        let ev = Event<'a>()
        let table = ConditionalWeakTable<'a, unit>()
        source.Add(fun x -> if table.TryGetValue(x) |> fst |> not then
                                table.GetValue(x, (fun _ -> ()))
                                ev.Trigger(x))
        ev.Publish


動作確認

C#版同様、Rectangle上でマウスをDownするとコンソール出力されるアプリケーションを作成してみます。

OwnerTypeを毎回指定するのが面倒な場合は、さらに一段クラスか関数をかまして利用してください。
f:id:any-programming:20170517233730p:plain

Xaml

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:local="clr-namespace:FsApp;assembly=FsApp"
        Title="MainWindow" Height="200" Width="200">
    <Grid>
        <Rectangle Fill="White" local:Commands.MouseLeftButtonDown="{Binding Down}" />
    </Grid>
</Window>

F#

type ViewModel() =
    member this.Down = command (fun _ -> printfn "down")

let main argv = 
    let window = loadEmbeddedResource<Window> "MainWindow.xaml"

    window.DataContext <- ViewModel()

    Application().Run(window) |> ignore
    0
namespace FsApp

type Commands() =
    static let mouseLeftButtonDown = AttachedProperty<Commands, FrameworkElement, ICommand>()
    static do 
        mouseLeftButtonDown.Changed
        |> Event.distinct
        |> Event.add(fun x -> x.MouseLeftButtonDown.Add(fun _ -> mouseLeftButtonDown.Get(x).Execute(null)))

    static member val MouseLeftButtonDownProperty = mouseLeftButtonDown.Register()
    static member GetMouseLeftButtonDown(x) = mouseLeftButtonDown.Get(x)
    static member SetMouseLeftButtonDown(x, y) = mouseLeftButtonDown.Set(x, y)






AttachedProperty定義の記述量削減

下記記事にて、依存プロパティの定義の記述量を削減しました。
DependencyProperty定義の記述量削減 - 何でもプログラミング

今回は同様に、添付プロパティの記述量を削減してみたいと思います。

素のままでは下記のような記述になり、エディタ上で"propa"とタイプするとスニペットが利用できます。

public static readonly DependencyProperty ValueProperty =
    DependencyProperty.RegisterAttached("Value", typeof(int), typeof(OwnerClass), new PropertyMetadata(0, (obj, args) =>
    {
        // GUIの更新など
    }));
public static int GetValue(DependencyObject obj)
{
    return (int)obj.GetValue(ValueProperty);
}
public static void SetValue(DependencyObject obj, int value)
{
    obj.SetValue(ValueProperty, value);
}


AttachedPropertyManager

Register、GetValue、SetValueを担うクラスになります。

基本的にCallerMemberNameを活用して記述量を減らします。

また値の変化は、On***Changedが定義されていれば呼び出す仕組みにしています。

さらに初回のOn***Changedの時には、On***ChangedFirstTimeも呼び出すようにしています。(対象コントロールのeventにhandlerを追加する時などに利用)

初回かどうかの判定は、ConditionalWeakTableに状態を保存することにより実現しています。

public class AttachedPropertyManager<TClass>
{
    class Dummy { }
    public DependencyProperty Register<TValue>(TValue defaultValue = default(TValue), [CallerMemberName]string name = "")
    {
        name = name.Substring(0, name.Length - "Property".Length);
        var bindingFlag = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
        var callback = typeof(TClass).GetMethod($"On{name}Changed", bindingFlag);
        var callbackFirstTime = typeof(TClass).GetMethod($"On{name}ChangedFirstTime", bindingFlag);
        var metadata = new PropertyMetadata(defaultValue);
        if (callback != null || callbackFirstTime != null)
        {
            var called = new ConditionalWeakTable<DependencyObject, Dummy>();
            metadata.PropertyChangedCallback = (obj, args) =>
            {
                if (called.TryGetValue(obj, out Dummy dummy) == false)
                {
                    callbackFirstTime?.Invoke(null, new object[] { obj });
                    called.GetOrCreateValue(obj);
                }
                callback?.Invoke(null, new object[] { obj });
            };
        }
        return DependencyProperty.RegisterAttached(name, typeof(TValue), typeof(TClass), metadata);
    }
    public object Get(DependencyObject obj, [CallerMemberName]string name = "") =>
        obj.GetValue(GetProperty(name));
    public void Set(DependencyObject obj, object value, [CallerMemberName]string name = "") =>
        obj.SetValue(GetProperty(name), value);
    DependencyProperty GetProperty(string name) =>
        (DependencyProperty)typeof(TClass).GetField(name.Substring(3) + "Property").GetValue(null);
}


RectangleにMouseLeftButtonDownコマンドを追加してみる

Rectangle上でマウスをDownすると、コンソール出力されるアプリケーションを作成してみます。

ICommand型の添付プロパティを定義し、OnChangeFirstTimeにて、ICommand.Executeを実行するhandlerをFrameworkElement.MouseLeftButtonDownに登録しています。

※添付プロパティでeventにhandlerを登録する際はリークに注意してください。(今回の利用方法では問題ありません。)
f:id:any-programming:20170517233730p:plain

Xaml

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApp"
        Title="MainWindow" Height="200" Width="200">
    <Grid>
        <Rectangle Fill="White" local:Commands.MouseLeftButtonDown="{Binding Down}" />
    </Grid>
</Window>

C#

public class ViewModel
{
    public Command<object> Down { get; } = new Command<object>(x => Console.WriteLine("down"));
}
public class Commands
{
    static AttachedPropertyManager<Commands> _ap = new AttachedPropertyManager<Commands>();

    public static readonly DependencyProperty MouseLeftButtonDownProperty = _ap.Register<ICommand>();
    public static ICommand GetMouseLeftButtonDown(FrameworkElement obj) => (ICommand)_ap.Get(obj);
    public static void SetMouseLeftButtonDown(FrameworkElement obj, ICommand value) => _ap.Set(obj, value);

    static void OnMouseLeftButtonDownChangedFirstTime(FrameworkElement obj) =>
        obj.MouseLeftButtonDown += (s, e) => GetMouseLeftButtonDown(obj)?.Execute(null);
}






LINQ独自オペレータ メモ

C#のIEnumerableには、SelectやAggregateなど様々なオペレータが用意されています。

本記事では、あったら便利な独自のオペレータを定義してメモしたいと思います。

随時更新予定です。

ForEach

なんだかんだで、やっぱりあると便利です。

public static void ForEach<T>(this IEnumerable<T> source, Action<T> f)
{
    foreach (T item in source)
        f(item);
}


ForEach

引数にインデックスを渡すForEachです。

public static void ForEach<T>(this IEnumerable<T> source, Action<int, T> f)
{
    int i = 0;
    foreach (T item in source)
    {
        f(i, item);
        i++;
    }
}


ZipForEach

ZipとForEachを同時に行います。

public static void ZipForEach<T1, T2>(this IEnumerable<T1> source1, IEnumerable<T2> source2, Action<T1, T2> f)
{
    var s1 = source1.GetEnumerator();
    var s2 = source2.GetEnumerator();
    while (s1.MoveNext() && s2.MoveNext())
        f(s1.Current, s2.Current);
}


ReplaceAt

あるインデックスの要素を置き換えます。

public static IEnumerable<T> ReplaceAt<T>(this IEnumerable<T> source, int index, T value) =>
    source.Select((x, i) => i == index ? value : x);


UpdateAt

あるインデックスの要素を更新します。

public static IEnumerable<T> UpdateAt<T>(this IEnumerable<T> source, int index, Func<T, T> f) =>
    source.Select((x, i) => i == index ? f(x) : x);


RemoveAt

あるインデックスの要素を削除します。

public static IEnumerable<T> RemoveAt<T>(this IEnumerable<T> source, int index) =>
    source.Where((x, i) => i != index);


InsertAt

指定のインデックスに要素を追加します。

先頭は0、末尾はsource.Countを指定し、範囲外の場合はsourceそのままが出力されます。

public static IEnumerable<T> InsertAt<T>(this IEnumerable<T> source, int index, T value)
{
    if (index == 0)
        yield return value;

    int i = 0;
    foreach (T item in source)
    {
        yield return item;
        i++;
        if (i == index)
            yield return value;
    }
}


RemoveLast

最後の要素を削除します。(UpdateLastやInsertLastも同様に定義できます。)

public static IEnumerable<T> RemoveLast<T>(this IEnumerable<T> source) =>
    source.RemoveAt(source.Count() - 1);


Scan

Aggregateの全過程を出力します。

public static IEnumerable<TAccumulate> Scan<T, TAccumulate>(this IEnumerable<T> source, TAccumulate seed, Func<TAccumulate, T, TAccumulate> func)
{
    yield return seed;
    foreach (var item in source)
    {
        seed = func(seed, item);
        yield return seed;
    }
}


Create

初期値と更新関数からIEnumerableを作成します。

public static IEnumerable<T> Create<T>(T seed, Func<T, T> func)
{
    while (true)
    {
        yield return seed;
        seed = func(seed);
    }
}


Flatten

木構造を配列に変換します。

public static IEnumerable<T> Flatten<T>(this IEnumerable<T> seed, Func<T, IEnumerable<T>> func)
{
    var queue = new Queue<T>(seed);
    while (queue.Any())
    {
        T item = queue.Dequeue();
        foreach (T subItem in func(item))
            queue.Enqueue(subItem);
        yield return item;
    }
}


Buffer

指定の要素数ごとにまとめたListに変換します。

public static IEnumerable<List<T>> Buffer<T>(this IEnumerable<T> source, int count)
{
    var buffer = new List<T>(count);
    foreach (T item in source)
    {
        buffer.Add(item);
        if (buffer.Count == count)
        {
            yield return buffer;
            buffer = new List<T>(count);
        }
    }
    if (buffer.Any())
        yield return buffer;
}


Distinct(セレクタ関数で)

DistinctにはIEqualityComparerを取るものしか用意されていないので、セレクタ関数を取るように実装します。

class CompareSelector<T, TKey> : IEqualityComparer<T>
{
    private Func<T, TKey> _selector;
    public CompareSelector(Func<T, TKey> selector) => _selector = selector;
    public bool Equals(T x, T y) => _selector(x).Equals(_selector(y));
    public int GetHashCode(T obj) => _selector(obj).GetHashCode();
}
public static IEnumerable<T> Distinct<T, TKey>(this IEnumerable<T> source, Func<T, TKey> selector) =>
    source.Distinct(new CompareSelector<T, TKey>(selector));


Lines

stringを行ごとに分解

public static IEnumerable<string> Lines(this string source)
{
    using (var reader = new StringReader(source))
    {
        while (true)
        {
            var line = reader.ReadLine();
            if (line == null)
                break;
            yield return line;
        }
    }
}






T4でImmutableクラスコンストラクタ作成(C#)

C#(7.0)でImmutableクラスを作成する際、下記のような記述が必要になります。

public class Person
{
    public string Name { get; }
    public int Age { get; }
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

F#のレコードと異なり、コンストラクタを定義しないといけません。

今回はこのコンストラクタをT4で自動生成してみたいと思います。

Tangible T4

tangible T4 Editor for VS2010 / VS2012 / VS2013 / VS2015 / VS2017

素のVisual Studioでは、T4はハイライトや補間などが一切サポートされていません。

そこでこれらを有効にするためにTangible T4をインストールします。

有償版ではさらに、DTE関連のインテリセンスの制限が解除されたり、デバッグができるようになるようです。

C#側準備

ターゲットのクラス判別のため、Record属性を定義しています。

またターゲットのクラスはpartialにします。

public class RecordAttribute : Attribute { }

[Record]
public partial class Person
{
    public string Name { get; }
    public int Age { get; }
}


T4ファイル

DTEを用いて、csファイル内のクラスを解析していきます。

this.Hostを利用するため、hostspecificはtrueにします。

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="EnvDTE" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="EnvDTE" #>
<#@ output extension=".cs" #>
<#
    DTE dte = (this.Host as IServiceProvider).GetService(typeof(DTE)) as DTE;
    Project project = dte.Solution.FindProjectItem(this.Host.TemplateFile).ContainingProject; // T4ファイルが所属するプロジェクト
#>



<# 
    foreach (CodeClass cls in GetClasses(project, "Record")) { 
        string parameters = string.Join(", ", 
            GetProperties(cls).Select(x => string.Format("{0} {1}", x.Type.AsString, x.Name.ToLower()))); 
#>
namespace <#= cls.Namespace.FullName #>
{
    public partial class <#= cls.Name #>
    {
        public <#= cls.Name #> (<#= parameters #>)
        {
<# foreach (CodeProperty p in GetProperties(cls)) { #>
            <#= p.Name #> = <#= p.Name.ToLower() #>;
<# } #>
        }
    }
}
<# } #>



<#+ 
IEnumerable<T> Flatten<T>(IEnumerable<T> root, Func<T, IEnumerable<T>> getChildren)
{
    // 木構造を配列に展開
    return root.Concat(root.SelectMany(x => Flatten(getChildren(x), getChildren)));
}
IEnumerable<ProjectItem> GetCSFiles(Project project)
{
    return Flatten(project.ProjectItems.Cast<ProjectItem>(), x => x.ProjectItems.Cast<ProjectItem>())
        .Where(x => Path.GetExtension(x.Name) == ".cs");
}
IEnumerable<CodeClass> GetClasses(ProjectItem item)
{
    // 今回はnamespace直下のクラスのみ取得
    return Flatten(item.FileCodeModel.CodeElements.Cast<CodeElement>(),
        x => x is CodeNamespace
            ? (x as CodeNamespace).Members.Cast<CodeElement>()
            : Enumerable.Empty<CodeElement>())
        .OfType<CodeClass>();
}
IEnumerable<CodeClass> GetClasses(Project project, string attribute)
{
    return GetCSFiles(project)
        .SelectMany(GetClasses)
        .Where(c => c.Attributes.Cast<CodeAttribute>().Any(x => x.Name == attribute));
}
IEnumerable<CodeProperty> GetProperties(CodeClass cls)
{
    // { get; }なのか、{ get { return ユーザー定義 } }なのかの違いがCodePropertyから判別できないため、今回は文字列解析で判別
    return cls.Members
        .OfType<CodeProperty>()
        .Where(x => x.StartPoint.CreateEditPoint().GetText(x.EndPoint).TrimEnd().EndsWith("{ get; }"));
}
#>


自動生成結果

上記T4により、下記のクラスが自動生成されます。

public partial class Person
{
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}






DependencyProperty定義の記述量削減

独自のDependencyPropertyを定義する際、コールバックも含めると下記のような記述になります。

public static readonly DependencyProperty ValueProperty =
    DependencyProperty.Register("Value", typeof(int), typeof(Counter), new PropertyMetadata(0, (obj, args) =>
    {
        var counter = (Counter)obj;
        // GUI更新など
    }));
public int Value
{
    get { return (int)GetValue(ValueProperty); }
    set { SetValue(ValueProperty, value); }
}

エディタ上で"propdp"と打ち込むとスニペットが利用できるのでタイピング量自体は少ないですが、コード量自体は多くなります。

今回はDependencyPropertyのコード量を減らしてみたいと思います。

なお、プリプロセス(T4など)や、ポストプロセス(PostSharpやMono.Cecilなど)は利用しないで実装してみます。(T4を利用すれば下記サイトのように最低限の記述で済みますので、実用上はT4のほうが良いと思います。)
Declarative Dependency Property Definition with T4 + DTE

DependencyPropertyManager

Register、GetValue、SetValueを担うクラスになります。

基本的にCallerMemberNameを活用して記述量を減らします。

また値の変化は、On***Changedが定義されていれば呼び出す仕組みにしています。

public class DependencyPropertyManager<TClass>
{
    public DependencyProperty Register<TValue>(TValue defaultValue = default(TValue), [CallerMemberName]string name = "")
    {
        name = name.Substring(0, name.Length - "Property".Length);
        var callback = typeof(TClass).GetMethod($"On{name}Changed", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
        var metadata = callback == null
            ? new PropertyMetadata(defaultValue)
            : new PropertyMetadata(defaultValue, (obj, args) => { callback.Invoke(obj, null); });
        return DependencyProperty.Register(name, typeof(TValue), typeof(TClass), metadata);
    }
    public object Get(DependencyObject obj, [CallerMemberName]string name = "") =>
        obj.GetValue(GetProperty(name));
    public void Set(DependencyObject obj, object value, [CallerMemberName]string name = "") =>
        obj.SetValue(GetProperty(name), value);
    DependencyProperty GetProperty(string name) => 
        (DependencyProperty)typeof(TClass).GetField(name + "Property").GetValue(null);
}


Counterコントロール

ボタンでカウントアップ/ダウンが行えるコントロールを作成してみます。

カウンタの値(Value)と、カウントアップ/ダウン(Command)をDependencyPropertyとして定義しています。

CounterにGet/Setメンバを定義すれば、さらにプロパティ内の記述量を減らすことが可能です。
f:id:any-programming:20170515181632p:plain
C#

public partial class Counter : UserControl
{
    static DependencyPropertyManager<Counter> _dp = new DependencyPropertyManager<Counter>();

    public static readonly DependencyProperty ValueProperty = _dp.Register<int>();
    public static readonly DependencyProperty CommandProperty = _dp.Register<Command<int>>();

    public int Value { get => (int)_dp.Get(this); set => _dp.Set(this, value); }
    public Command<int> Command { get => (Command<int>)_dp.Get(this); set => _dp.Set(this, value); }

    void OnValueChanged() => textbox.Text = Value.ToString();

    public Counter()
    {
        InitializeComponent();

        textbox.Text = Value.ToString();
        buttonUp.Click += (s, e) => Command?.Execute(1);
        buttonDown.Click += (s, e) => Command?.Execute(-1);
    }
}

Xaml

<UserControl x:Class="WpfApp.Counter"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="23" d:DesignWidth="200">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="23"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="23"/>
        </Grid.ColumnDefinitions>
        <Button x:Name="buttonDown" Content="◀" Grid.Column="0" />
        <TextBox x:Name="textbox" Grid.Column="1" IsReadOnly="True" TextAlignment="Center" VerticalContentAlignment="Center" />
        <Button x:Name="buttonUp" Content="▶" Grid.Column="2" />
    </Grid>
</UserControl>






WPFでマウスドラッグイベント

WPFにはデフォルトでドラッグのイベントは用意されていません。

今回はドラッグ用のイベントを実装してみたいと思います。

記事内で利用しているObservableクラス、Disposeクラス、Subscribe関数は下記記事を参照してください。
C# eventをIObservableに変換 - 何でもプログラミング

MouseDragArgs

ドラッグイベント時に渡されるクラスを作成します。

public enum MouseDragPhase
{
    Begin,
    Move,
    End
}
public class MouseDragArgs
{
    public Point StartPoint { get; }
    public ModifierKeys StartModifier { get; }
    public Point Point { get; }
    public MouseDragPhase Phase { get; }
    public MouseDragArgs(Point startPoint, ModifierKeys startModifier, Point point, MouseDragPhase phase)
    {
        StartPoint = startPoint;
        StartModifier = startModifier;
        Point = point;
        Phase = phase;
    }
}


MouseLeftButtonDragObs

IObservable<MouseDragArgs>を返す拡張メソッドを作成します。

Down、Up等のIObservableとオペレータの組み合わせで記述できなくもないですが、副作用(CaptureMouse等)や状態(isDragging等)を伴うため、手続き的に記述したほうがわかりやすいです。

マウスDown時にMouseCaptureし、要素から外れてもドラッグが継続されるように実装しています。

LostFocusを監視することにより、Up以外(Alt + Tabや右クリックメニュー等)でドラッグが終了しても通知するようにしてあります。

イベントが並走するのを避けるため、他のボタンがDownの状態では開始しないよう実装してあります。

この辺の仕様は適宜カスタマイズしてください。

public static IObservable<MouseDragArgs> MouseLeftButtonDragObs(this FrameworkElement element) =>
    new Observable<MouseDragArgs>(observer =>
    {
        bool isDragging = false;
        Point startPoint = new Point(0, 0);
        ModifierKeys startModifier = ModifierKeys.None;

        // Down
        MouseButtonEventHandler down = (s, e) =>
        {
            if (e.RightButton == MouseButtonState.Pressed || e.MiddleButton == MouseButtonState.Pressed)
                return;
            element.CaptureMouse();
            isDragging = true;
            startPoint = e.GetPosition(element);
            startModifier = Keyboard.Modifiers;
            observer.OnNext(new MouseDragArgs(startPoint, startModifier, startPoint, MouseDragPhase.Begin));
        };

        // Move
        MouseEventHandler move = (s, e) =>
        {
            if (isDragging)
                observer.OnNext(new MouseDragArgs(startPoint, startModifier, e.GetPosition(element), MouseDragPhase.Move));
        };

        // Up
        MouseButtonEventHandler up = (s, e) => element.ReleaseMouseCapture();

        // LostFocus
        MouseEventHandler lostFocus = (s, e) => 
        {
            if (isDragging == false)
                return;
            isDragging = false;
            observer.OnNext(new MouseDragArgs(startPoint, startModifier, e.GetPosition(element), MouseDragPhase.End));
        };

        // イベント登録
        element.MouseLeftButtonDown += down;
        element.MouseMove += move;
        element.MouseLeftButtonUp += up;
        element.LostMouseCapture += lostFocus;

        return new Disposable(() =>
        {
            // イベント解除
            element.MouseLeftButtonDown -= down;
            element.MouseMove -= move;
            element.MouseLeftButtonUp -= up;
            element.LostMouseCapture -= lostFocus;
        });
    });


動作確認

長方形上でのLeftDrag情報をコンソールに表示するアプリケーションになります。

Disposeボタンを押すと、一切出力されなくなります。
f:id:any-programming:20170512172531p:plain
Xaml

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="175" Width="135">
    <Grid>
        <Rectangle x:Name="rect" Fill="#FFF4F4F5" Stroke="Black" Margin="10,10,10,35"/>
        <Button x:Name="buttonDispose" Content="Dispose" Margin="10,115,10,10" />
    </Grid>
</Window>

C#

public MainWindow()
{
    InitializeComponent();

    var subscription = rect.MouseLeftButtonDragObs().Subscribe(args =>
    {
        if (args.Phase == MouseDragPhase.Begin)
            Console.WriteLine($"begin {args.StartModifier} {args.Point}");
        else if (args.Phase == MouseDragPhase.Move)
            Console.WriteLine($"move {args.StartModifier} {args.StartPoint} {args.Point}");
        else
            Console.WriteLine($"end {args.StartModifier} {args.StartPoint} {args.Point}");
    });
    buttonDispose.Click += (s, e) => subscription.Dispose();
}






C# eventをIObservableに変換

eventからIObservableへの変換は、下記のRxのFromEvent関数にて行えます。
NuGet Gallery | Reactive Extensions (Rx) - Main Library 3.1.1

今回は、Rxを導入するほどでもないとき用に、シンプルなものを自前で実装してみたいと思います。

Observer、Observable、Disposable

用途に応じて継承クラスを作成するのは面倒なので、ラムダで作成できるクラスを作成します。

eventではOnCompletedとOnErrorを利用することがないので、実装していません。

ついでにラムダでSubscribe出来る拡張メソッドも定義しておきます。

public class Observer<T> : IObserver<T>
{
    Action<T> _onNext;
    public Observer(Action<T> onNext) => _onNext = onNext;
    public void OnNext(T value) => _onNext(value);
    public void OnCompleted() => throw new NotImplementedException();
    public void OnError(Exception error) => throw new NotImplementedException();
}
public class Observable<T> : IObservable<T>
{
    Func<IObserver<T>, IDisposable> _subscribe;
    public Observable(Func<IObserver<T>, IDisposable> subscribe) => _subscribe = subscribe;
    public IDisposable Subscribe(IObserver<T> observer) => _subscribe(observer);
}
public class Disposable : IDisposable
{
    Action _dispose;
    public Disposable(Action dispose) => _dispose = dispose;
    public void Dispose() => _dispose();
}
public static class EventExtensions
{
    public static IDisposable Subscribe<T>(this IObservable<T> observable, Action<T> f) =>
        observable.Subscribe(new Observer<T>(f));
}


MouseLeftButtonDownを実装してみる

FrameworkElementの拡張メソッドとして定義しています。

Subscribe時にhandlerを追加し、Dispose時にhandlerを解除するObservableを作成しています。

public static IObservable<MouseButtonEventArgs> MouseLeftButtonDownObs(this FrameworkElement element) =>
    new Observable<T>(observer =>
    {
        MouseButtonEventHandler handler = (sender, e) => observer.OnNext(e);
        element.MouseLeftButtonDown += handler;
        return new Disposable(() => element.MouseLeftButtonDown -= handler);
    });


FromEvent

少し汎用化して、オブジェクトとイベント名からIObservableを作成できるようにしてみます。

命名の仕方が固定であるなら、CallerMemberNameを利用してもいいと思います。

public static IObservable<T> FromEvent<T>(object obj, string name) =>
    new Observable<T>(observer =>
    {
        var ev = obj.GetType().GetEvent(name);
        Action<object, T> action = (sender, e) => observer.OnNext(e);
        Delegate handler = Delegate.CreateDelegate(ev.EventHandlerType, action.Target, action.Method);
        ev.AddEventHandler(obj, handler);
        return new Disposable(() => ev.RemoveEventHandler(obj, handler));
    });
public static IObservable<MouseButtonEventArgs> MouseLeftButtonDownObs(this FrameworkElement element) =>
    FromEvent<MouseButtonEventArgs>(element, nameof(element.MouseLeftButtonDown));
public static IObservable<MouseButtonEventArgs> MouseLeftButtonUpObs(this FrameworkElement element) =>
    FromEvent<MouseButtonEventArgs>(element, nameof(element.MouseLeftButtonUp));


オペレータ

ついでにSelect、Where、Mergeオペレータも作成してみます。

public static IObservable<TDst> Select<TSrc, TDst>(this IObservable<TSrc> observable, Func<TSrc, TDst> f) =>
    new Observable<TDst>(observer =>
    {
        var disposable = observable.Subscribe(x => observer.OnNext(f(x)));
        return new Disposable(disposable.Dispose);
    });
public static IObservable<T> Where<T>(this IObservable<T> observable, Func<T, bool> f) =>
    new Observable<T>(observer =>
    {
        var disposable = observable.Subscribe(x =>
        {
            if (f(x))
                observer.OnNext(x);
        });
        return new Disposable(disposable.Dispose);
    });
public static IObservable<T> Merge<T>(this IObservable<T> observable1, IObservable<T> observable2) =>
    new Observable<T>(observer =>
    {
        var disposable1 = observable1.Subscribe(observer.OnNext);
        var disposable2 = observable2.Subscribe(observer.OnNext);
        return new Disposable(() =>
        {
            disposable1.Dispose();
            disposable2.Dispose();
        });
    });


動作確認

長方形上でLeftDown、LeftUp時に座標をコンソールに表示するアプリケーションになります。

左Shift押下時はDownが、左Ctrl押下時はUpが無視されるようになっています。

Disposeボタンを押すと、一切出力されなくなります。

f:id:any-programming:20170511155239p:plain
Xaml

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="175" Width="135">
    <Grid>
        <Rectangle x:Name="rect" Fill="#FFF4F4F5" Stroke="Black" Margin="10,10,10,35"/>
        <Button x:Name="buttonDispose" Content="Dispose" Margin="10,115,10,10" />
    </Grid>
</Window>

C#

public MainWindow()
{
    InitializeComponent();

    var down = rect.MouseLeftButtonDownObs()
       .Where(x => Keyboard.IsKeyUp(Key.LeftShift))
       .Select(x => x.GetPosition(rect));
    var up = rect.MouseLeftButtonUpObs()
       .Where(x => Keyboard.IsKeyUp(Key.LeftCtrl))
       .Select(x => x.GetPosition(rect));
    var subscription = down.Merge(up).Subscribe(Console.WriteLine);
    
    buttonDispose.Click += (s, e) => subscription.Dispose();
}