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)