F#でWPF --- テキストボックスCommand --- 1文字変更毎に発行
下記記事ではキーボードのフォーカスが外れた時にCommandを実行するよう実装しました。
F#でWPF --- テキストボックスCommand - 何でもプログラミング
今回は1文字変更する度にCommandを実行するよう実装します。
TextChangedイベント
TextBoxにはTextChangedイベントがあり、1文字変更される度に発行されます。
正確にはTextの値が変更されると発行されます。
ここで問題となるのが、ViewModel側でTextを変更した場合もTextChangedが発行されてしまうことです。
ループ構造は予期せぬ挙動を招く可能性がありますので、今回はViewModel側からのTextの変更とユーザーからのTextの変更を分離するよう実装します。
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 - 何でもプログラミング
<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