F#でWPF --- メッセージボックス

作成するアプリケーション

ボタンを押すと"Hello"のメッセージボックスが表示されるアプリケーションを作成します。
f:id:any-programming:20170206231614p:plain

MVVMを用いない場合

下記の様なシンプルなコードになります。

しかしViewが分離できていません。

button.Click |> Event.add (fun _ -> MessageBox.Show("Hello"))


MVVM化

ViewModelのMessageプロパティが変更されたらViewがMessageBox.Showを呼ぶ実装にします。
f:id:any-programming:20170206233126p:plain

MessageDialogBehavior

上記機能を実現するためにBehaviorを利用します。

BehaviorBaseは下記記事を参照してください。
F#でWPF --- チェックボックスCommand --- Behavior利用 - 何でもプログラミング

open System.Windows
open System.Windows.Input

type MessageDialogBehavior() =
    inherit BehaviorBase<Window>()
    static member val MessageProperty = DependencyProperty.Register("Message", typeof<string option>, typeof<MessageDialogBehavior>)
    member this.Message with get()                = this.GetValue(MessageDialogBehavior.MessageProperty) :?> string option
                        and  set(x:string option) = this.SetValue(MessageDialogBehavior.MessageProperty, x)
    
    static member val ClosedProperty = DependencyProperty.Register("Closed", typeof<ICommand>, typeof<MessageDialogBehavior>)
    member this.Closed with get()           = this.GetValue(MessageDialogBehavior.ClosedProperty) :?> ICommand
                       and  set(x:ICommand) = this.SetValue(MessageDialogBehavior.ClosedProperty, x)

    override this.OnAttached control =        
        [ DependencyProperty.changed<string option> MessageDialogBehavior.MessageProperty this 
          |> Observable.subscribe (Option.iter (fun x -> MessageBox.Show(x) |> ignore
                                                         if this.Closed <> null then
                                                            this.Closed.Execute())) ] 


アプリケーションコード

アプリケーション名はBehaviors.exeです。

MessageDialogBehaviorはnamespace Behaviorsに定義されています。

F#で利用しているDataContextは下記記事を参照してください。
F#でWPF --- Elm Architectureを利用したMVVM - 何でもプログラミング

またF#でWPFプロジェクトを作成する方法は下記記事を参照してください。
F#でWPF --- ウィンドウ表示 - 何でもプログラミング

Xaml

<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">
    <i:Interaction.Behaviors>
        <local:MessageDialogBehavior Message="{Binding Message}" Closed="{Binding ClearMessage}" />
    </i:Interaction.Behaviors>
    <Grid>
        <Button Content="メッセージボックス" Command="{Binding ShowMessage}" HorizontalAlignment="Left" Margin="40,20,0,0" VerticalAlignment="Top" Width="100" />
    </Grid>
</Window>

F#

open System
open System.Windows

type Model = { Message : string option }

type Msg = 
    | ShowMessage
    | ClearMessage

let updateModel model msg =
    match msg with
    | ShowMessage -> 
        { model with Message = Some "Hello" }

    | ClearMessage ->
        { model with Message = None }

[<STAThread>]
[<EntryPoint>]
let main argv = 
    let window = Application.LoadComponent(Uri("MainWindow.xaml", UriKind.Relative)) :?> Window
    window.DataContext <- DataContext({ Message = None }, updateModel, id)
    Application().Run(window) |> ignore   
    0






F#でWPF --- ファイルダイアログCommand

今回はファイルダイアログを開いて選択されたパスをCommandで送る機能を実装します。

この機能を実装するにあたり、Actionを利用します。

Action

Behaviorと同じく、System.Windows.Interactivity.dllを利用します。

Behaviorの詳細は下記記事を参照してください。
F#でWPF --- チェックボックスCommand --- Behavior利用 - 何でもプログラミング

ActionはTriggerと共に用いられ、例えばボタンがクリックされた時にCommandを実行する記述は下記の様になります。(挙動はButtonのCommandを利用するのと同じです。)

<Button>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <i:InvokeCommandAction Command="{Binding DoSomething}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>


OpenFileDialogAction

独自のActionを作成するには、TriggerActionを継承しInvoke関数をオーバーライドします。

open System.Windows
open System.Windows.Input
open System.Windows.Interactivity
open Microsoft.FSharp.Linq.NullableOperators
open Microsoft.Win32

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

    static member val FilterProperty = DependencyProperty.Register("Filter", typeof<string>, typeof<OpenFileDialogAction>)
    member this.Filter with get()         = this.GetValue(OpenFileDialogAction.FilterProperty) :?> string
                        and set(x:string) = this.SetValue(OpenFileDialogAction.FilterProperty, x)

    override this.Invoke parameter = 
        let dialog = OpenFileDialog(Filter = this.Filter)
        if dialog.ShowDialog() ?= true && this.Command <> null then 
            this.Command.Execute dialog.FileName


作成するアプリケーション

ボタンを押してファイルダイアログを開き、パスを選択するとテキストボックスにパスが表示されるアプリケーションを作成します。
f:id:any-programming:20170205225741p:plain

アプリケーションコード

アプリケーション名はActions.exeです。

OpenFileDialogActionはnamespace Actionsに定義されています。

F#で利用しているDataContextは下記記事を参照してください。
F#でWPF --- Elm Architectureを利用したMVVM - 何でもプログラミング

またF#でWPFプロジェクトを作成する方法は下記記事を参照してください。
F#でWPF --- ウィンドウ表示 - 何でもプログラミング

Xaml

<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:Actions;assembly=Actions"
        Title="MainWindow" Height="80" Width="250">
    <Grid>
        <Button Content="ファイルダイアログ" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="100">
            <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <local:OpenFileDialogAction Command="{Binding SetPath}" Filter="F#|*.fs" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
        <TextBlock Text="{Binding Path}" HorizontalAlignment="Left" Margin="115,10,0,0" TextWrapping="Wrap" VerticalAlignment="Top"/>
    </Grid>
</Window>

F#

open System
open System.Windows

type Model = { Path : string }

type Msg = SetPath of string

let updateModel model msg =
    match msg with
    | SetPath x -> 
        { model with Path = x }

[<STAThread>]
[<EntryPoint>]
let main argv = 
    let window = Application.LoadComponent(Uri("MainWindow.xaml", UriKind.Relative)) :?> Window
    window.DataContext <- DataContext({ Path = "" }, updateModel, id)
    Application().Run(window) |> ignore   
    0






F#でWPF --- スライダーCommand

今回はdouble値をCommandで送るSliderを作成します。
SliderにはCommandプロパティが用意されていないため、Behaviorを作成します。

Behaviorの基本は下記記事を参照してください。
F#でWPF --- チェックボックスCommand --- Behavior利用 - 何でもプログラミング

ValueChangedイベント

SliderにはValueChangedイベントがありますが、ViewModelからのValueの変更でも発行されてしまいます。
ループ構造は予期せぬ挙動を招く可能性がありますので、今回はViewModel側からのValueの変更とユーザーからのValueの変更を分離するよう実装します。
f:id:any-programming:20170204214004p:plain

SliderBehavior

ViewModelからのValueの変更の場合、一時変数に入力をセットしておきます。
ValueChangedイベントが発行されたときに一時変数の値と同じならViewModelからの変更、異なる場合はユーザーからの変更と考えられます。

ViewModelからのValueの値がMinimum~Maximunに収まっていない場合、収まった値がSlider.Valueに設定されてしまう(ValueChangedが発行されてしまう)ので、事前にMinimum~Maximumに収めています。

BehaviorBaseは下記記事を参照してください。
F#でWPF --- チェックボックスCommand --- Behavior利用 - 何でもプログラミング

DependencyProperty.changedは下記記事を参照してください。
F#でWPF --- テキストボックスCommand --- 1文字変更毎に発行 - 何でもプログラミング

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

    static member val ValueProperty = DependencyProperty.Register("Value", typeof<double>, typeof<SliderBehavior>)
    member this.Value with get()         = this.GetValue(SliderBehavior.ValueProperty) :?> double
                      and  set(x:double) = this.SetValue(SliderBehavior.ValueProperty, x)

    override this.OnAttached control =        
        let mutable value = control.Value
        [ control.ValueChanged.Subscribe (fun x -> if value <> x.NewValue then
                                                      value <- x.NewValue
                                                      if this.Command <> null then
                                                         this.Command.Execute(value)) 
          DependencyProperty.changed<double> SliderBehavior.ValueProperty this 
          |> Observable.subscribe (fun x -> let clamp min max = function
                                                                | x when x < min -> min
                                                                | x when max < x -> max
                                                                | x              -> x                                            
                                            value <- x |> clamp control.Minimum control.Maximum
                                            control.Value <- value) ]


作成するアプリケーション

スライダーとスライダーの値を表示するテキストボックスからなります。
f:id:any-programming:20170204232747p:plain

アプリケーションコード

SliderのIsSnapToTickEnabledをtrueにすると、TickFrequencyの値でスライダーを刻むことができるようになります。(TickFrequencyのデフォルトは1です。)

F#で利用しているDataContextは下記記事を参照してください。
F#でWPF --- Elm Architectureを利用したMVVM - 何でもプログラミング

またF#でWPFプロジェクトを作成する方法は下記記事を参照してください。
F#でWPF --- ウィンドウ表示 - 何でもプログラミング

Xaml

<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="80" Width="220">
    <Grid>
        <Slider IsSnapToTickEnabled="True" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="150">
            <i:Interaction.Behaviors>
                <local:SliderBehavior Command="{Binding SetValue}" Value="{Binding Value}" />
            </i:Interaction.Behaviors>
        </Slider>
        <TextBlock Text="{Binding Value}" HorizontalAlignment="Left" Margin="165,10,0,0" TextWrapping="Wrap" VerticalAlignment="Top"/>
    </Grid>
</Window>

F#

open System
open System.Windows

type Model = { Value : double }

type Msg = SetValue of double

let updateModel model msg =
    match msg with
    | SetValue x -> 
        { model with Value = x }

[<STAThread>]
[<EntryPoint>]
let main argv = 
    let window = Application.LoadComponent(Uri("MainWindow.xaml", UriKind.Relative)) :?> Window
    window.DataContext <- DataContext({ Value = 0.0 }, updateModel, id)
    Application().Run(window) |> ignore   
    0






F#でWPF --- テキストボックスCommand --- 1文字変更毎に発行

下記記事ではキーボードのフォーカスが外れた時にCommandを実行するよう実装しました。
F#でWPF --- テキストボックスCommand - 何でもプログラミング

今回は1文字変更する度にCommandを実行するよう実装します。

TextChangedイベント

TextBoxにはTextChangedイベントがあり、1文字変更される度に発行されます。
正確にはTextの値が変更されると発行されます。

ここで問題となるのが、ViewModel側でTextを変更した場合もTextChangedが発行されてしまうことです。
ループ構造は予期せぬ挙動を招く可能性がありますので、今回はViewModel側からのTextの変更とユーザーからのTextの変更を分離するよう実装します。
f:id:any-programming:20170204100709p:plain

TextBoxBehavior

BehaviorBaseは下記記事を参照してください。
F#でWPF --- チェックボックスCommand --- Behavior利用 - 何でもプログラミング

ViewModelからのTextの変更の場合、一時変数に入力をセットしておきます。
TextChangedイベントが発行されたときに一時変数の値と同じならViewModelからの変更、異なる場合はユーザーからの変更と考えられます。

DependencyProperty.changedは後程説明します。

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

    static member val TextProperty = DependencyProperty.Register("Text", typeof<string>, typeof<TextBoxBehavior>)
    member this.Text with get()         = this.GetValue(TextBoxBehavior.TextProperty) :?> string
                     and  set(x:string) = this.SetValue(TextBoxBehavior.TextProperty, x)

    override this.OnAttached control =
        let mutable text = control.Text
        [ control.TextChanged.Subscribe(fun _ -> if text <> control.Text then
                                                    text <- control.Text
                                                    if this.Command <> null then 
                                                          this.Command.Execute(control.Text))
          DependencyProperty.changed<string> TextBoxBehavior.TextProperty this
          |> Observable.subscribe (fun x -> text <- x
                                            control.Text <- x) ]


DependencyProperty.changed

ViewModelからの変更を検知するため、DependencyPropertyのIObservableを作成します。

変更の検知にはDependencyPropertyDescriptorのAddValueChangedを利用します。

IObservableでイベントを発行するにはobserverのOnNextを呼びます。
またSubscribeの返り値としてRemoveValueChangedを呼ぶIDisposableを作成します。

RemoveValueChangedが呼ばれない限りコントロールがメモリに残り続けてしまうため、利用側はDisposeを忘れないようにしてください。

open System
open System.Windows
open System.ComponentModel

module DependencyProperty =
    let changed<'a> property (obj : DependencyObject) = 
        { new IObservable<'a> with
            member this.Subscribe observer = 
                let handler = EventHandler(fun _ _ -> observer.OnNext(obj.GetValue(property) :?> 'a))
                let descriptor = DependencyPropertyDescriptor.FromProperty(property, property.OwnerType)
                descriptor.AddValueChanged(obj, handler)
                { new IDisposable with
                    member this.Dispose() = 
                        descriptor.RemoveValueChanged(obj, handler) } }


アプリケーションコード

下記記事と同じアプリケーションを作成します。
ただし今回は文字変更の度にテキストブロックに反映されます。
F#でWPF --- テキストボックスCommand - 何でもプログラミング

Xaml

<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="250">
    <Grid>
        <TextBox HorizontalAlignment="Left" Height="23" Margin="10,10,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120">
            <i:Interaction.Behaviors>
                <local:TextBoxBehavior Command="{Binding SetText}" Text="{Binding Text}" />
            </i:Interaction.Behaviors>
        </TextBox>
        <TextBlock Text="{Binding Text}" HorizontalAlignment="Left" Margin="13,37,0,0" TextWrapping="Wrap" VerticalAlignment="Top"/>
    </Grid>
</Window>

F#

open System
open System.Windows

type Model = { Text : string }

type Msg = SetText of string

let updateModel model msg =
    match msg with
    | SetText x -> 
        { model with Text = x }

[<STAThread>]
[<EntryPoint>]
let main argv = 
    let window = Application.LoadComponent(Uri("MainWindow.xaml", UriKind.Relative)) :?> Window
    window.DataContext <- DataContext({ Text = "" }, updateModel, id)
    Application().Run(window) |> ignore   
    0






F#でWPF --- テキストボックスCommand

今回はstringを送るCommandをTextBoxに追加します。
TextBoxにはCommandプロパティが用意されていないため、Behaviorを作成します。

Behaviorの基本は下記記事を参照してください。
F#でWPF --- チェックボックスCommand --- Behavior利用 - 何でもプログラミング

作成するアプリケーション

キーボードのフォーカスが外れた時に、テキストボックスの文字列がテキストブロックに反映されるアプリケーションです。
f:id:any-programming:20170203075044p:plain

TextBoxBehavior

LostKeyboradFocusのイベントでCommandを実行するようにします。

ついでにReturnでキーボードのフォーカスが外れるようにします。

BehaviorBaseは下記記事を参照してください。
F#でWPF --- チェックボックスCommand --- Behavior利用 - 何でもプログラミング

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

    override this.OnAttached control =
        [ control.LostKeyboardFocus.Subscribe(fun _ -> if this.Command <> null then 
                                                          this.Command.Execute(control.Text))
          // Returnでキーボードフォーカスを外す
          control.KeyDown.Subscribe(fun x -> if x.Key = Key.Return then
                                                Keyboard.ClearFocus()) ]


アプリケーションコード

アプリケーション名はBehaviors.exeでTextBoxBehaviorはnamespace Behaviorsに定義されています。

TextBoxのTextはデフォルトでTwoWayなので、setterにアクセスしないようOneWayにします。

F#で利用しているDataContextは下記記事を参照してください。
F#でWPF --- Elm Architectureを利用したMVVM - 何でもプログラミング

またF#でWPFプロジェクトを作成する方法は下記記事を参照してください。
F#でWPF --- ウィンドウ表示 - 何でもプログラミング

Xaml

<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>
        <TextBox Text="{Binding Text, Mode=OneWay}" HorizontalAlignment="Left" Height="23" Margin="10,10,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120">
            <i:Interaction.Behaviors>
                <local:TextBoxBehavior Command="{Binding SetText}" />
            </i:Interaction.Behaviors>
        </TextBox>
        <TextBlock Text="{Binding Text}" HorizontalAlignment="Left" Margin="13,37,0,0" TextWrapping="Wrap" VerticalAlignment="Top"/>
    </Grid>
</Window>

F#

type Model = { Text : string }

type Msg = SetText of string

let updateModel model msg =
    match msg with
    | SetText x -> 
        { model with Text = x }

[<STAThread>]
[<EntryPoint>]
let main argv = 
    let window = Application.LoadComponent(Uri("MainWindow.xaml", UriKind.Relative)) :?> Window
    window.DataContext <- DataContext({ Text = "" }, updateModel, id)
    Application().Run(window) |> ignore   
    0


EventTriggerとInvokeCommandActionで実装

System.Windows.InteractivityのEventTriggerとInvokeCommandActionを利用してLostKeycoardFocus時にCommandを送ることができます。

またRelativeSourceのFindAncestorモードを利用することにより、TextBoxのTextプロパティをCommandParameterにバインドすることが可能です。

この例ではReturn入力時にキーボードフォーカスが外れることはありません。

<TextBox Text="{Binding Text, Mode=OneWay}" HorizontalAlignment="Left" Height="23" Margin="10,10,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="120">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="LostKeyboardFocus">
            <i:InvokeCommandAction Command="{Binding SetText}" CommandParameter="{Binding Text, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TextBox}}}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</TextBox>






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

下記記事では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で作成した方が使いまわしが可能となります。





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

今回はチェックボックスのCommandを利用したアプリケーションを作成します。

作成するアプリケーション

チェックを入れるとダイアログの背景がグレーに、外すと白になるアプリケーションを作成します。
f:id:any-programming:20170131235550p:plain

CommandParameter

チェックの状態をCommandと共に送るために、CommandParameterにIsCheckedの値を設定するようにします。

CommandParameter="{Binding IsChecked, RelativeSource={RelativeSource Self}}"


IsChecked

IsCheckedはデフォルトでTwoWayです。今回はsetterを利用しないのでModeをOneWayにします。

冗長ではありますが一貫した実装ができるため、入力は全てCommandにバインドし、その他は全てOneWayのバインドにすることを推奨します。

IsChecked="{Binding IsDark, Mode=OneWay}"


コード全体

F#で利用しているDataContextは下記記事を参照してください。
F#でWPF --- Elm Architectureを利用したMVVM - 何でもプログラミング

またF#でWPFプロジェクトを作成する方法は下記記事を参照してください。
F#でWPF --- ウィンドウ表示 - 何でもプログラミング

Xaml

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        Title="MainWindow" Height="100" Width="200">
    <Grid Background="{Binding Background}">
        <CheckBox Content="背景を暗く" IsChecked="{Binding IsDark, Mode=OneWay}" Command="{Binding SetIsDark}" CommandParameter="{Binding IsChecked, RelativeSource={RelativeSource Self}}" HorizontalAlignment="Left" Margin="15,25,0,0" VerticalAlignment="Top"/>
    </Grid>
</Window>

F#

open System
open System.Windows
open System.Windows.Media

type Model = 
    { IsDark : bool }
    member this.Background = 
        match this.IsDark with
        | true  -> Brushes.LightGray
        | false -> Brushes.White

type Msg = SetIsDark of bool

let updateModel model msg =
    match msg with
    | SetIsDark x -> 
        { model with IsDark = x }

[<STAThread>]
[<EntryPoint>]
let main argv = 
    let window = Application.LoadComponent(Uri("MainWindow.xaml", UriKind.Relative)) :?> Window
    window.DataContext <- DataContext({ IsDark = false }, updateModel, id)
    Application().Run(window) |> ignore   
    0