F#でWPF --- リストボックスCommand
今回はListBoxに、選択されたindexを送るCommandを実装します。
実装はBehaviorを利用して行います。
Behaviorの詳しい内容は下記記事を参照してください。
F#でWPF --- チェックボックスCommand --- Behavior利用 - 何でもプログラミング
SelectionChanged
ListBoxにあるSelectionChangedイベントは、ユーザーからの入力以外でも発火することがあります。
今回はこれを回避するよう実装していきます。
SelectedItem、SelectedIndex、SelectedValue
ListBoxには現在の選択状態を示すのに、いくつかのプロパティが存在します。
その中で、SelectedItemをベースに作成してすれば不可解な挙動をしなくなると思います。
ListBoxBehavior
Item毎にインスタンスを変えるため(SelectedItemで判別するため)、独自のItemクラスでラップします。
ViewModelからの変更でSelectionChangedを発行しないよう、isUpdatingFromVMをフラグとして利用しています。
BehaviorBaseは下記記事を参照してください。
F#でWPF --- チェックボックスCommand --- Behavior利用 - 何でもプログラミング
DependencyProperty.changedは下記記事を参照してください。
F#でWPF --- テキストボックスCommand --- 1文字変更毎に発行 - 何でもプログラミング
open System.Collections open System.Windows.Input type Item(name : obj) = override this.ToString() = name.ToString() type ListBoxBehavior() = inherit BehaviorBase<ListBox>() static member val ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof<IEnumerable>, typeof<ListBoxBehavior>) member this.ItemsSource with get() = this.GetValue(ListBoxBehavior.ItemsSourceProperty) :?> IEnumerable and set(x:IEnumerable) = this.SetValue(ListBoxBehavior.ItemsSourceProperty, x) static member val SelectedIndexProperty = DependencyProperty.Register("SelectedIndex", typeof<int>, typeof<ListBoxBehavior>) member this.SelectedIndex with get() = this.GetValue(ListBoxBehavior.SelectedIndexProperty) :?> int and set(x:int) = this.SetValue(ListBoxBehavior.SelectedIndexProperty, x) static member val CommandProperty = DependencyProperty.Register("Command", typeof<ICommand>, typeof<ListBoxBehavior>) member this.Command with get() = this.GetValue(ListBoxBehavior.CommandProperty) :?> ICommand and set(x:ICommand) = this.SetValue(ListBoxBehavior.CommandProperty, x) override this.OnAttached control = let mutable items = [] let mutable isUpdatingFromVM = false let setSelectedItem index = if 0 <= index && index < items.Length then control.SelectedItem <- items.[index] else control.SelectedItem <- null [ DependencyProperty.changed<IEnumerable> ListBoxBehavior.ItemsSourceProperty this |> Observable.subscribe (fun x -> isUpdatingFromVM <- true let index = control.SelectedIndex items <- x |> Seq.cast<obj> |> Seq.toList |> List.map Item control.ItemsSource <- items setSelectedItem index isUpdatingFromVM <- false) DependencyProperty.changed<int> ListBoxBehavior.SelectedIndexProperty this |> Observable.subscribe (fun x -> isUpdatingFromVM <- true setSelectedItem x isUpdatingFromVM <- false) control.SelectionChanged.Subscribe(fun _ -> if (not isUpdatingFromVM) && (this.Command <> null) then this.Command.Execute(control.SelectedIndex)) ]
作成するアプリケーション
追加、削除ボタンでリストを管理するアプリケーションです。
アプリケーションコード
F#で利用しているDataContextは下記記事を参照してください。
F#でWPF --- Elm Architectureを利用したMVVM - 何でもプログラミング
<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=ListBoxBehavior" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="150" Width="200"> <Grid> <ListBox Margin="10,10,0,10" HorizontalAlignment="Left" Width="80"> <i:Interaction.Behaviors> <local:ListBoxBehavior ItemsSource="{Binding Items}" SelectedIndex="{Binding SelectedIndex}" Command="{Binding SetSelectedIndex}" /> </i:Interaction.Behaviors> </ListBox> <Button Command="{Binding AddItem}" Content="追加" HorizontalAlignment="Left" Margin="95,10,0,0" VerticalAlignment="Top" Width="75"/> <Button Command="{Binding RemoveSelectedItem}" Content="削除" HorizontalAlignment="Left" Margin="95,35,0,0" VerticalAlignment="Top" Width="75"/> </Grid> </Window>
F#
open System open System.Windows type Msg = | AddItem | RemoveSelectedItem | SetSelectedIndex of int type Model = { Items : string list SelectedIndex : int } let initialModel = { Items = [] SelectedIndex = -1 } let updateModel model msg = match msg with | AddItem -> { model with Items = "item" :: model.Items } | RemoveSelectedItem -> match model.SelectedIndex with | -1 -> model | x -> let items = (model.Items |> List.take x) @ (model.Items |> List.skip (x + 1)) { model with Items = items SelectedIndex = min x (items.Length - 1) } | SetSelectedIndex x -> { model with SelectedIndex = x } [<STAThread>] [<EntryPoint>] let main argv = let window = Application.LoadComponent(Uri("MainWindow.xaml", UriKind.Relative)) :?> Window window.DataContext <- DataContext(initialModel, updateModel, id) Application().Run(window) |> ignore 0