none
ItemsControl 仮想化時のAlternationIndexについて RRS feed

  • 質問

  • 毎々お世話になっております。

    ItemsControlで仮想化時にすばやくコントロールの大きさを変更したりアイテムをスクロールさせたりすると
    AlternationIndexの順序が変わります(不定)。

    通常、例えばアイテムが50件あるときのindex番号は
    仮想化されていなければ0,1,2,3・・・49のようになるかと思いますが
    仮想化(リサイクル)している場合、48,49,0,1,2・・・47のようにその都度変わると思います。

    以下、サンプル。

    <Window x:Class="Sample1"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:WPFSample"
            mc:Ignorable="d"
            Title="Sample1" Height="300" Width="300">
        <Window.Resources>
            <AlternationConverter x:Key="ItemsRowBackground">
                <SolidColorBrush Color="White"/>
                <SolidColorBrush Color="AliceBlue"/>
            </AlternationConverter>
            <AlternationConverter x:Key="ItemsRowForeground">
                <SolidColorBrush Color="#5B9BD5"/>
                <SolidColorBrush Color="#ED7D31"/>
                <SolidColorBrush Color="#A5A5A5"/>
                <SolidColorBrush Color="#FFC000"/>
                <SolidColorBrush Color="#4472C4"/>
                <SolidColorBrush Color="#70AD47"/>
            </AlternationConverter>
        </Window.Resources>
        <Grid>
            <ListBox ItemsSource="{Binding Mode=OneWay}"
                     AlternationCount="{Binding Count}">
                <ListBox.Resources>
                    <Style TargetType="{x:Type ScrollViewer}">
                        <Setter Property="CanContentScroll" Value="True" />
                        <Setter Property="HorizontalScrollBarVisibility" Value="Disabled" />
                        <Setter Property="VerticalScrollBarVisibility" Value="Visible" />
                        <Setter Property="VirtualizingPanel.IsVirtualizing" Value="True" />
                        <Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling" />
                        <Setter Property="Padding" Value="10" />
                    </Style>
                </ListBox.Resources>
                <ListBox.ItemContainerStyle>
                    <Style TargetType="{x:Type ListBoxItem}">
                        <Setter Property="Template">
                            <Setter.Value>
                                <ControlTemplate TargetType="{x:Type ContentControl}">
                                    <Border Background="{Binding Path=(ItemsControl.AlternationIndex),
                                        RelativeSource={RelativeSource TemplatedParent},
                                        Converter={StaticResource ItemsRowBackground}}">
                                        <ContentPresenter/>
                                    </Border>
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                    </Style>
                </ListBox.ItemContainerStyle>
                <ListBox.ItemsPanel>
                    <ItemsPanelTemplate>
                        <VirtualizingStackPanel />
                    </ItemsPanelTemplate>
                </ListBox.ItemsPanel>
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel Orientation="Horizontal" Margin="5">
                            <TextBlock Text="{Binding (ItemsControl.AlternationIndex),RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}}}"/>
                            <TextBlock Text="■"
                                       Foreground="{Binding Path=(ItemsControl.AlternationIndex),
                                        RelativeSource={RelativeSource AncestorType={x:Type ListBoxItem}},
                                        Converter={StaticResource ItemsRowForeground}}"/>
                            <TextBlock Text="{Binding}"/>
                        </StackPanel>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </Grid>
    
    </Window>
    Imports System.Collections.ObjectModel
    
    Public Class Sample1
        Public Sub New()
    
            ' この呼び出しはデザイナーで必要です。
            InitializeComponent()
    
            ' InitializeComponent() 呼び出しの後で初期化を追加します。
            Dim data As New ObservableCollection(Of String)
            For i As Integer = 1 To 50
                data.Add("アイテム" & i.ToString)
            Next
            Me.DataContext = data
        End Sub
    End Class

    行の背景色など縞々にしたりする場合よくAlternationIndexを使用して対応する旨が

    MSDNやインターネットに記載されていますが仮想化時の対応方法などを見つけることができませんでした。

    https://msdn.microsoft.com/ja-jp/library/system.windows.controls.itemscontrol.alternationcount(v=vs.110).aspx
    https://msdn.microsoft.com/ja-jp/library/system.windows.controls.itemscontrol.alternationindex(v=vs.110).aspx
    https://msdn.microsoft.com/ja-jp/library/system.windows.controls.alternationconverter(v=vs.110).aspx

    index番号が変わってしまうためその都度の見栄えが変わってしまう可能性がありますが触れられていません。
    皆様はどのように対応されているのでしょうか?
    VMにindexプロパティを自前で持つかConverterを作るなどして対応するのが一般的なのでしょうか?

    (データ件数をある程度絞り込んでページャーなどで対応して仮想化パネルは使用しない?)

    それとも、私の理解不足か何か見落としているのでしょうか?


    以上、アドバイスをよろしくお願いいたします。


    <開発環境>
    Visual Studio2015 Community(VB)
    .NET Framework4.6.1
    WPFアプリケーション
    Windows7 Pro x64





    • 編集済み mikupedia 2016年7月11日 4:03 誤字等訂正
    2016年7月11日 2:56

回答

  • どうにもうまい具合に更新ができないので、あきらめて愚直に書き換えてみる

    Class ListBoxEx
        Inherits ListBox
    
        Shared Sub New()
            Dim fi = ItemsControl.AlternationIndexProperty.GetType().GetField("_readOnlyKey", Reflection.BindingFlags.NonPublic Or Reflection.BindingFlags.Instance)
            _AlternationIndexPropertyKey = CType(fi.GetValue(ItemsControl.AlternationIndexProperty), DependencyPropertyKey)
        End Sub
    
        Private Shared _AlternationIndexPropertyKey As DependencyPropertyKey
    
        Private flag As Boolean
        Private panel As Panel
    
        Protected Overrides Sub PrepareContainerForItemOverride(element As DependencyObject, item As Object)
            If (Not flag) Then
                flag = True
                '生成処理が済んだら振り直しさせる
                Dim act As Action = AddressOf RefreshAlternationIndex
                Me.Dispatcher.BeginInvoke(act, Windows.Threading.DispatcherPriority.Loaded, New Object() {})
            End If
            MyBase.PrepareContainerForItemOverride(element, item)
    
            panel = TryCast(VisualTreeHelper.GetParent(element), Panel)
        End Sub
    
        Private Sub RefreshAlternationIndex()
            System.Diagnostics.Debug.WriteLine("Refresh ")
    
            flag = False
    
            If (TypeOf panel Is VirtualizingPanel) Then
    
                If (VirtualizingPanel.GetIsVirtualizing(Me)) Then
                    For Each d As DependencyObject In panel.Children
                        Dim lbi As ListBoxItem = d
                        Dim index As Integer = CInt(lbi.GetValue(ItemsControl.AlternationIndexProperty))
                        Dim indexFix As Integer = Me.ItemContainerGenerator.IndexFromContainer(lbi)
    
                        If (index <> indexFix) Then
                            Dim o As Object = Me.ItemContainerGenerator.ItemFromContainer(lbi)
                            lbi.SetValue(_AlternationIndexPropertyKey, indexFix)
                            System.Diagnostics.Debug.WriteLine(index & " -> " & indexFix)
                        End If
                    Next
                End If
            End If
    
        End Sub
    
    End Class

    これでもスクロール中はズレて見えてしまいます...orz

    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答としてマーク mikupedia 2016年7月19日 2:20
    2016年7月15日 10:19

すべての返信

  • こちらでも再現しました。

    stackoverflowなども探してみましたが解決策らしきものは載ってませんね…。


    かずき Blog:http://d.hatena.ne.jp/okazuki/

    2016年7月11日 7:17
  • むりやり書き換えて修正してみる

    2016/07/12 追記
    PrepareContainerForItemOverrideで設定し直す方法のが簡単でした

    Class ListBoxEx
        Inherits ListBox
    
        Shared Sub New()
            Dim fi = ItemsControl.AlternationIndexProperty.GetType().GetField("_readOnlyKey", Reflection.BindingFlags.NonPublic Or Reflection.BindingFlags.Instance)
            _AlternationIndexPropertyKey = CType(fi.GetValue(ItemsControl.AlternationIndexProperty), DependencyPropertyKey)
        End Sub
    
        Private Shared _AlternationIndexPropertyKey As DependencyPropertyKey
    
        Protected Overrides Sub PrepareContainerForItemOverride(element As DependencyObject, item As Object)
            Dim lbi As ListBoxItem = CType(element, ListBoxItem)
            Dim indexFix = Me.ItemContainerGenerator.IndexFromContainer(element)
            lbi.SetValue(_AlternationIndexPropertyKey, indexFix)
            MyBase.PrepareContainerForItemOverride(element, item)
        End Sub
    End Class

    以下旧コード

    Class ListBoxEx
        Inherits ListBox
    
        Shared Sub New()
            Dim fi = ItemsControl.AlternationIndexProperty.GetType().GetField("_readOnlyKey", Reflection.BindingFlags.NonPublic Or Reflection.BindingFlags.Instance)
            _AlternationIndexPropertyKey = CType(fi.GetValue(ItemsControl.AlternationIndexProperty), DependencyPropertyKey)
        End Sub
    
        Private Shared _AlternationIndexPropertyKey As DependencyPropertyKey
    
        Protected Overrides Function GetContainerForItemOverride() As DependencyObject
            Dim lbi As ListBoxItem = CType(MyBase.GetContainerForItemOverride(), ListBoxItem)
            Dim bnd As Binding = New Binding()
            bnd.Path = New PropertyPath(ContentControl.ContentProperty)
            bnd.Source = lbi
            lbi.SetBinding(ListBoxEx.ContentProperty, bnd)
            Return lbi
        End Function
    
    
        Private Shared ContentProperty As DependencyProperty _
            = DependencyProperty.RegisterAttached("Content", GetType(Object), GetType(ListBoxEx), New PropertyMetadata(Nothing, AddressOf OnContentPropertyChanged))
    
        Private Shared Sub OnContentPropertyChanged(ByVal dpo As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
            Dim lbi As ListBoxItem = CType(dpo, ListBoxItem)
            Dim listBox As ListBox = GetListBox(lbi)
            If (listBox Is Nothing) Then
                Return
            End If
    
            Dim index As Integer = CInt(lbi.GetValue(ItemsControl.AlternationIndexProperty))
            Dim indexFix As Integer = listBox.ItemContainerGenerator.IndexFromContainer(lbi)
    
            If (index <> indexFix) Then
                Dim o As Object = listBox.ItemContainerGenerator.ItemFromContainer(lbi)
                If Not (o Is DependencyProperty.UnsetValue) Then
                    lbi.SetValue(_AlternationIndexPropertyKey, indexFix)
                End If
            End If
        End Sub
    
        Private Shared Function GetListBox(ByVal lbi As ListBoxItem) As ListBox
            Dim d As DependencyObject = lbi
            While d IsNot Nothing
                d = VisualTreeHelper.GetParent(d)
                If (TypeOf d Is ListBox) Then
                    Return CType(d, ListBox)
                End If
            End While
            Return Nothing
        End Function
    End Class

    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 編集済み gekkaMVP 2016年7月12日 3:27 コードを追加
    2016年7月11日 11:48
  • かずき 様
    確認していただきありがとうございました。


    gekka 様
    詳細なコードのご提示ありがとうございました。
    DependencyPropertyKey、PrepareContainerForItemOverrideについて非常に参考になりました。
    まずはお礼申し上げます。

    動作は明日中に確認してみます。

    2016年7月12日 13:53
  • gekka 様

    お世話になります、ご提示頂きましたPrepareContainerForItemOverrideのソースコードで確認してみました。
    以前より現象は出にくくなるものの、マウスでスクロールバーを上下に高速移動させると以前のように順番がおかしくなる時があります。
    あとは番号が重複するときもありました。

    2016年7月13日 10:08
  • あまり発展的でない意見で恐縮なのですが、私個人は仮想化したItemsControlで値の保持が上手くいかない時は(mikupediaさんの書きこみにある内容と同じですが、)ViewModelにシーケンス用のプロパティを持つか、仮想化をしない方向での実装を検討する形で対応してきました。
    よりよい方法があれば、私も後学のためにぜひお聞かせ願いたいですが、上記対処法でも問題ないのではと思いました。
    • 編集済み runcs 2016年7月15日 1:25
    2016年7月14日 23:53
  • どうにもうまい具合に更新ができないので、あきらめて愚直に書き換えてみる

    Class ListBoxEx
        Inherits ListBox
    
        Shared Sub New()
            Dim fi = ItemsControl.AlternationIndexProperty.GetType().GetField("_readOnlyKey", Reflection.BindingFlags.NonPublic Or Reflection.BindingFlags.Instance)
            _AlternationIndexPropertyKey = CType(fi.GetValue(ItemsControl.AlternationIndexProperty), DependencyPropertyKey)
        End Sub
    
        Private Shared _AlternationIndexPropertyKey As DependencyPropertyKey
    
        Private flag As Boolean
        Private panel As Panel
    
        Protected Overrides Sub PrepareContainerForItemOverride(element As DependencyObject, item As Object)
            If (Not flag) Then
                flag = True
                '生成処理が済んだら振り直しさせる
                Dim act As Action = AddressOf RefreshAlternationIndex
                Me.Dispatcher.BeginInvoke(act, Windows.Threading.DispatcherPriority.Loaded, New Object() {})
            End If
            MyBase.PrepareContainerForItemOverride(element, item)
    
            panel = TryCast(VisualTreeHelper.GetParent(element), Panel)
        End Sub
    
        Private Sub RefreshAlternationIndex()
            System.Diagnostics.Debug.WriteLine("Refresh ")
    
            flag = False
    
            If (TypeOf panel Is VirtualizingPanel) Then
    
                If (VirtualizingPanel.GetIsVirtualizing(Me)) Then
                    For Each d As DependencyObject In panel.Children
                        Dim lbi As ListBoxItem = d
                        Dim index As Integer = CInt(lbi.GetValue(ItemsControl.AlternationIndexProperty))
                        Dim indexFix As Integer = Me.ItemContainerGenerator.IndexFromContainer(lbi)
    
                        If (index <> indexFix) Then
                            Dim o As Object = Me.ItemContainerGenerator.ItemFromContainer(lbi)
                            lbi.SetValue(_AlternationIndexPropertyKey, indexFix)
                            System.Diagnostics.Debug.WriteLine(index & " -> " & indexFix)
                        End If
                    Next
                End If
            End If
    
        End Sub
    
    End Class

    これでもスクロール中はズレて見えてしまいます...orz

    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    • 回答としてマーク mikupedia 2016年7月19日 2:20
    2016年7月15日 10:19
  • runcs 様

    ご返答ありがとうございます。
    おっしゃる通り、非仮想化/仮想化に関しては臨機応変に対応すべきですね。


    gekka 様

    ご回答ありがとうございます。ソースの動作を確認してみました。
    おっしゃる通り、スクロール中一瞬だけずれて見えるときもありましたがほとんどわからなかったです。
    スクロール後のIndex番号はずれることもなく、意図した動作となっておりました。
    大変参考になるソースの掲載をありがとうございました。

    2016年7月19日 2:19