読者です 読者をやめる 読者になる 読者になる

F#でWPF --- チェックボックスCommand --- Behavior利用

F# MVVM WPF

下記記事ではCommandParameterを利用してboolを渡すCommandを実装しました。
F#でWPF --- チェックボックスCommand - 何でもプログラミング

今回はBehaviorという機能を利用して同じCommandを実現します。

参照追加

Behaviorを利用するには、System.Windows.Interactivityを参照に追加する必要があります。

もしXamlエディタで
The type .... is built with an older version of the Blend SDK
のようなエラーが表示される場合は、ver4.0のSystem.Windows.Interactivity.dllを参照してみてください。
f:id:any-programming:20170202002141p:plain

Xaml

BehaviorはInteraction.Behaviorsに追加することにより利用できます。

CheckBoxBehaviorはこれから実装するクラスになります。

CheckBoxBehaviorは作成するアプリケーション(Behaviors.exe)のnamespace Behaviors内に定義されています。

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" 
        xmlns:local="clr-namespace:Behaviors;assembly=Behaviors"
        Title="MainWindow" Height="100" Width="200">
    <Grid Background="{Binding Background}">
        <CheckBox Content="背景を暗く" IsChecked="{Binding IsDark, Mode=OneWay}" HorizontalAlignment="Left" Margin="15,25,0,0" VerticalAlignment="Top">
            <i:Interaction.Behaviors>
                <local:CheckBoxBehavior Command="{Binding SetIsDark}" />
            </i:Interaction.Behaviors>
        </CheckBox>
    </Grid>
</Window>


CheckBoxBehavior

System.Windows.Interactivity.Behaviorを継承して作成します。

OnAttachedをオーバーライドして独自の処理を追加していきます。

AssociatedObjectプロパティからターゲットを取得できます。

Bindingできるようにするため、CommandのプロパティはDependencyPropertyとして定義しています。
定義の記述は冗長なので、スニペットや自動生成を利用した方がよいです。

namespace Behaviors

open System.Windows
open System.Windows.Interactivity
open System.Windows.Controls
open System.Windows.Input

type CheckBoxBehavior() =
    inherit Behavior<CheckBox>()
    static member val CommandProperty = DependencyProperty.Register("Command", typeof<ICommand>, typeof<CheckBoxBehavior>)
    member this.Command with get()           = this.GetValue(CheckBoxBehavior.CommandProperty) :?> ICommand
                        and  set(x:ICommand) = this.SetValue(CheckBoxBehavior.CommandProperty, x)

    override this.OnAttached() =        
        let control = this.AssociatedObject
        control.Click
        |> Event.add (fun _ -> if this.Command <> null then 
                                  this.Command.Execute(control.IsChecked.Value))


動作確認

F#側は下記記事のものを変更しないで利用できます。
F#でWPF --- チェックボックスCommand - 何でもプログラミング

より安全なBehavior

OnAttachedの中ではイベントをSubscribeすることが多く、依存関係を考えておかないとメモリリークの原因になることがあります。

これを回避するため、BehaviorのOnDetachとAssociatedObjectのUnloadedでUnsubscribeするベースクラスを用意します。

継承先はコントロールを入力とし、IDisposable listを返り値とするOnAttachedを実装します。

[<AbstractClass>]
type BehaviorBase<'a when 'a :> FrameworkElement>() =
    inherit Behavior<'a>()
    let mutable subscriptions : IDisposable list = []
    let unsubscribe() = 
        subscriptions |> List.iter (fun x -> x.Dispose())
        subscriptions <- []

    abstract OnAttached : 'a -> IDisposable list

    override this.OnAttached() = 
        base.OnAttached()
        subscriptions <- this.AssociatedObject.Unloaded.Subscribe(fun _ -> unsubscribe())
                         :: this.OnAttached this.AssociatedObject

    override this.OnDetaching() =     
        unsubscribe()
        base.OnDetaching()


BehaviorBaseを利用したCheckBoxBehavior

type CheckBoxBehavior() =
    inherit BehaviorBase<CheckBox>()
    static member val CommandProperty = DependencyProperty.Register("Command", typeof<ICommand>, typeof<CheckBoxBehavior>)
    member this.Command with get()           = this.GetValue(CheckBoxBehavior.CommandProperty) :?> ICommand
                        and  set(x:ICommand) = this.SetValue(CheckBoxBehavior.CommandProperty, x)

    override this.OnAttached control =        
        [ control.Click.Subscribe(fun _ -> if this.Command <> null then 
                                              this.Command.Execute(control.IsChecked.Value)) ]


継承してMyCheckBoxを作成することとの違い

今回の利用ではCheckBoxを継承して新たにCommandプロパティを追加することと動作自体は変わりません。

ただし、コントロールのベースクラスであるFrameworkElementを拡張したい時などはBehaviorで作成した方が使いまわしが可能となります。