F#でWPF --- ItemsControlのItemのCommand

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

複数の、クリック回数を表示するボタンからなるアプリケーションを作成します。

追加ボタンにより、クリックできるボタンを動的に増やすことができます。
f:id:any-programming:20170525140454p:plain

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

ItemsControl第一歩

ItemsControlの実装は、何となく下記のようなものになるかと思います。

ただしこの記述だと、CountUpコマンドがCountsの各要素に存在する必要があります。

Elm Architectureを利用した場合、Commandは親に集約させる必要があるため、親要素へBindingする必要があります。

<ItemsControl ItemsSource="{Binding Counts}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Button Content="{Binding}" Command="{Bindin CountUp}" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>


F#側のコード

CountUpで指定インデックスの要素をインクリメントし、AddCountでCountsの要素を増やしています。

type Model = { Counts : int list }

let initialModel = { Counts = [] }

type Msg = 
    | CountUp of int
    | AddCount

let update model msg =
    match msg with
    | CountUp index -> 
        { Counts = model.Counts |> List.mapi (fun i x -> if i = index then x + 1 else x) }        
    | AddCount ->
        { Counts = 0 :: model.Counts }


AlternationCountとRelativeSourceを駆使して実装

RelativeSourceを利用することにより、親要素のCommandにBindingすることが可能になります。

また、何番目の要素からのCommandかを伝えるため、AlternationCountを利用しています。(最大値は現状適当な値を入れています。)

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        Title="MainWindow" Height="200" Width="200">
    <Grid>
        <ItemsControl ItemsSource="{Binding Counts}" AlternationCount="100" Margin="0,0,0,35">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <StackPanel />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <Button Content="{Binding}" 
                            Command="{Binding DataContext.CountUp, RelativeSource={RelativeSource AncestorType=ItemsControl}}" 
                            CommandParameter="{Binding Path=(ItemsControl.AlternationIndex), RelativeSource={RelativeSource TemplatedParent}}" />
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
        <Button Command="{Binding AddCount}" Content="追加" HorizontalAlignment="Right" Margin="0,0,10,10" VerticalAlignment="Bottom" Width="75"/>
    </Grid>
</Window>


独自のMarkup作成

上記でも所望の動作をしますが、毎回この記述をするのも面倒ですし、CommandParameterが定義されていない(独自プロパティ)可能性もあります。

そこで独自のマークアップを定義してみます。

type ItemCommandExtension(path : string) =
    inherit MarkupExtension()
    override this.ProvideValue(serviceProvider : IServiceProvider) = 
        let target = serviceProvider :?> IProvideValueTarget
        match target.TargetObject with
        | :? FrameworkElement as control -> 
            let property = target.TargetProperty :?> DependencyProperty
            assert (typeof<ICommand>.IsAssignableFrom(property.PropertyType))

            // 親要素をたどる
            let ancestors = 
                control :> DependencyObject
                |> List.unfold (Option.ofObj >> Option.map(fun x -> x, VisualTreeHelper.GetParent(x)))

            let itemsControl = ancestors |> Seq.ofType<ItemsControl> |> Seq.head

            // コンテナがContentPresenterでない場合は変更してください。
            let container = ancestors |> Seq.ofType<ContentPresenter> |> Seq.head 

            // アイテムのインデックスを取得
            let index = itemsControl.ItemContainerGenerator.IndexFromContainer(container)
 
            // parameterにindexを付与したCommandに変換
            let converter = Wpf.createConverter (fun (command : ICommand) -> 
                Wpf.createCommand (function                        
                   | null -> command.Execute(index)
                   | x    -> command.Execute(index, x)))

            // BindingのProvideValueを利用
            Binding(path, Source = itemsControl.DataContext, Converter = converter).ProvideValue(serviceProvider)

        // DataTemplateの場合、TargetObjectがShareDPで呼ばれることがあり、その場合はthisを返す約束となっています。
        | _ -> this :> obj 
module Seq =
    let ofType<'a> source = System.Linq.Enumerable.OfType<'a>(source)

module Wpf =
    let createCommand<'a> f = 
        { new ICommand with 
            member this.CanExecute _ = true
            [<CLIEvent>]
            member this.CanExecuteChanged = Event<_, _>().Publish 
            member this.Execute parameter = parameter :?> 'a |> f }

    let createConverter<'a, 'b> (f : 'a -> 'b) = 
        { new IValueConverter with
            override this.Convert(value, targetType, parameter, culture) = value :?> 'a |> f :> obj                
            override this.ConvertBack(value, targetType, parameter, culture) = raise (NotImplementedException()) }       

ItemsControlを下記のように書き換えると、所望の動作をします。

<ItemsControl ItemsSource="{Binding Counts}" Margin="0,0,0,35">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel />
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Button Content="{Binding}" Command="{local:ItemCommand CountUp}" />
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>