F#でWPF --- ItemsControlのItemのCommand
作成するアプリケーション
複数の、クリック回数を表示するボタンからなるアプリケーションを作成します。
追加ボタンにより、クリックできるボタンを動的に増やすことができます。
本記事では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>