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

F#でWPF --- リストボックスCommand

今回はListBoxに、選択されたindexを送るCommandを実装します。

実装はBehaviorを利用して行います。

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

SelectionChanged

ListBoxにあるSelectionChangedイベントは、ユーザーからの入力以外でも発火することがあります。

今回はこれを回避するよう実装していきます。
f:id:any-programming:20170218171652p:plain

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:id:any-programming:20170218173250p:plain

アプリケーションコード

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

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=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