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