Recycling指定でも正常に動作する版の、完全なサンプルコードは最後に載せています。
確認環境
- Visual Studio: Community 2013
- プロジェクトの対象のフレームワーク: .NET Framework 4.5
- PCにインストールしているフレームワーク: .NET Framework 4.5.51209
概要
- WinFormsのTreeView.ShowLinesプロパティをtrueにした時と同じような表示を、WPFでも行いたい
- stackoverflowでコード片を見つけた
- 紹介されている内容で一見上手く動くけれど、VirtualizingPanel.VirtualizationModeをRecyclingに指定すると、表示がおかしくなる場合がある
- Headerプロパティ更新時にも表示を更新するようにして解決した
表示がおかしくなるコード(要所のみ)
現象を分かりやすくするために、TreeViewItemの表示内容を"index / count"にしています。
Converter
public class TreeViewItemToIsLastItemConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { var item = (TreeViewItem)value; var ic = ItemsControl.ItemsControlFromItemContainer(item); if (ic == null) { return DependencyProperty.UnsetValue; } int index = ic.ItemContainerGenerator.IndexFromContainer(item); return index == ic.Items.Count - 1; } // ConverterBackはthrowするだけ } public class TreeViewItemToTitleConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { var item = (TreeViewItem)value; var ic = ItemsControl.ItemsControlFromItemContainer(item); if (ic == null) { return DependencyProperty.UnsetValue; } int index = ic.ItemContainerGenerator.IndexFromContainer(item); return string.Format("{0}/{1}", index + 1, ic.Items.Count); } // ConverterBackはthrowするだけ }
Viewで使用するTreeViewItemのControlTemplate
<ControlTemplate x:Key="TreeViewItemTemplate" TargetType="{x:Type TreeViewItem}"> <Grid> <Grid.ColumnDefinitions ... /> <Grid.RowDefinitions ... /> <!--ShowLines用の線--> <Rectangle Margin="9,0,0,0" Height="1" Stroke="#DCDCDC" /> <Rectangle x:Name="VerticalLine" Width="1" Stroke="#DCDCDC" Grid.RowSpan="2" /> <ToggleButton x:Name="Expander" ... /> <TextBlock Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type TreeViewItem}}, Converter={StaticResource TreeViewItemToTitleConverter}}" Grid.Column="1" /> <ItemsPresenter x:Name="ItemsHost" Grid.Row="1" Grid.Column="1" /> </Grid> <ControlTemplate.Triggers> <!--ToggleButtonの表示非表示や、IsExpandedの制御用のTriggerがここに入る--> <!--兄弟間で最後の要素は縦線を半分までにする--> <DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Converter={StaticResource TreeViewItemToIsLastItemConverter}}" Value="true"> <Setter TargetName="VerticalLine" Property="Height" Value="9" /> <Setter TargetName="VerticalLine" Property="VerticalAlignment" Value="Top" /> </DataTrigger> </ControlTemplate.Triggers> </ControlTemplate> <!--TreeViewに VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling" を指定している-->
どのように表示がおかしくなるか
しかし適当にスクロールしていると、実際の兄弟位置とTextBlockの表示内容が食い違うようになります。
Trigger条件が同じであるShowLinesの表示の制御も、同様におかしくなります。
原因および対処方法
・TextBlockのBindingも、DataTriggerのBindingも、binding target objectsはTreeViewItem
・VirtualizingPanel.VirtualizationModeにRecyclingを指定しているため、TreeViewItemインスタンスが使い回される
→TreeViewItemの位置が移動し、新しい内容を表示するようになっても、Bindingが更新されない
というわけで、TreeViewItemの内容が変化した場合でもBindingが更新されるようにすれば、この現象は解決できます。
具体的には Headerプロパティ が変化した場合でも更新されるようにします。
Converterの処理でTreeViewItemにアクセスする必要があるため、MultiBindingにTreeViewItemそのものとHeaderプロパティを指定してやります。
正常にShowLinesされるコード(抜粋)
Conterver
values[1]にHeaderプロパティの内容が渡されていますが、Bindingの更新タイミングの制御だけに必要であり、Converter内部では使用しません。
public class TreeViewItemToHeightConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { var item = (TreeViewItem)values[0]; var ic = ItemsControl.ItemsControlFromItemContainer(item); if (ic == null) { return DependencyProperty.UnsetValue; } int index = ic.ItemContainerGenerator.IndexFromContainer(item); return index == ic.Items.Count - 1 ? 9.0 : double.NaN; } // ConverterBackはthrowするだけ } public class TreeViewItemToVerticalAlignmentConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { var item = (TreeViewItem)values[0]; var ic = ItemsControl.ItemsControlFromItemContainer(item); if (ic == null) { return DependencyProperty.UnsetValue; } int index = ic.ItemContainerGenerator.IndexFromContainer(item); return index == ic.Items.Count - 1 ? VerticalAlignment.Top : VerticalAlignment.Stretch; } // ConverterBackはthrowするだけ } public class TreeViewItemToTitleConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { var item = (TreeViewItem)values[0]; var ic = ItemsControl.ItemsControlFromItemContainer(item); if (ic == null) { return DependencyProperty.UnsetValue; } int index = ic.ItemContainerGenerator.IndexFromContainer(item); return string.Format("{0}/{1}", index + 1, ic.Items.Count); } // ConverterBackはthrowするだけ }
Viewで使用するTreeViewItemのControlTemplate
MultiBindingを使用し、TreeViewItemそのものと、TreeViewItem.Headerの両方を指定します。
Bindingで全てを済ませるようになったため、縦線用Rectangleのx:Nameは不要になります。
<ControlTemplate x:Key="TreeViewItemTemplate" TargetType="{x:Type TreeViewItem}"> <Grid> <Grid.ColumnDefinitions ... /> <Grid.RowDefinitions ... /> <!--ShowLines用の線--> <Rectangle Margin="9,0,0,0" Height="1" Stroke="#DCDCDC" /> <Rectangle Width="1" Stroke="#DCDCDC" Grid.RowSpan="2"> <Rectangle.Height> <MultiBinding Converter="{StaticResource TreeViewItemToHeightConverter}"> <Binding RelativeSource="{RelativeSource AncestorType={x:Type TreeViewItem}}" Path="." /> <Binding RelativeSource="{RelativeSource AncestorType={x:Type TreeViewItem}}" Path="Header" /> </MultiBinding> </Rectangle.Height> <Rectangle.VerticalAlignment> <MultiBinding Converter="{StaticResource TreeViewItemToVerticalAlignmentConverter}"> <Binding RelativeSource="{RelativeSource AncestorType={x:Type TreeViewItem}}" Path="." /> <Binding RelativeSource="{RelativeSource AncestorType={x:Type TreeViewItem}}" Path="Header" /> </MultiBinding> </Rectangle.VerticalAlignment> </Rectangle> <ToggleButton x:Name="Expander" ... /> <TextBlock Grid.Column="1"> <TextBlock.Text> <MultiBinding Converter="{StaticResource TreeViewItemToTitleConverter}"> <Binding RelativeSource="{RelativeSource AncestorType={x:Type TreeViewItem}}" Path="." /> <Binding RelativeSource="{RelativeSource AncestorType={x:Type TreeViewItem}}" Path="Header" /> </MultiBinding> </TextBlock.Text> </TextBlock> <ItemsPresenter x:Name="ItemsHost" Grid.Row="1" Grid.Column="1" /> </Grid> <ControlTemplate.Triggers> <!--ToggleButtonの表示非表示や、IsExpandedの制御用のTriggerがここに入る--> </ControlTemplate.Triggers> </ControlTemplate> <!--TreeViewに VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling" を指定している-->
Recycling指定でも正常に動作する版の、完全なサンプルコード
ViewModel
using System.Collections.Generic; using System.ComponentModel; using System.Linq; namespace WpfTreeViewShowLines { public class ViewModel { public ViewModel() { var root = new Item { Children = Enumerable.Range(0, 100) .Select(i => new Item { Children = Enumerable.Range(0, 200) .Select(j => new Item()) .ToList(), }).ToList() }; this.Items = new[] { root }.ToList(); } public List<Item> Items { get; private set; } } public class Item : INotifyPropertyChanged { public List<Item> Children { get; set; } private bool _isExpanded = true; public bool IsExpanded { get { return this._isExpanded; } set { if (this._isExpanded == value) { return; } this._isExpanded = value; var temp = this.PropertyChanged; if (temp != null) { temp(this, new PropertyChangedEventArgs("IsExpanded")); } } } public event PropertyChangedEventHandler PropertyChanged; } }
Converter
using System; using System.Globalization; using System.Windows; using System.Windows.Controls; using System.Windows.Data; namespace WpfTreeViewShowLines { public class TreeViewItemToHeightConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { var item = (TreeViewItem)values[0]; var ic = ItemsControl.ItemsControlFromItemContainer(item); if (ic == null) { return DependencyProperty.UnsetValue; } int index = ic.ItemContainerGenerator.IndexFromContainer(item); return index == ic.Items.Count - 1 ? 9.0 : double.NaN; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { throw new NotSupportedException(); } } public class TreeViewItemToVerticalAlignmentConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { var item = (TreeViewItem)values[0]; var ic = ItemsControl.ItemsControlFromItemContainer(item); if (ic == null) { return DependencyProperty.UnsetValue; } int index = ic.ItemContainerGenerator.IndexFromContainer(item); return index == ic.Items.Count - 1 ? VerticalAlignment.Top : VerticalAlignment.Stretch; } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { throw new NotSupportedException(); } } public class TreeViewItemToTitleConverter : IMultiValueConverter { public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) { var item = (TreeViewItem)values[0]; var ic = ItemsControl.ItemsControlFromItemContainer(item); if (ic == null) { return DependencyProperty.UnsetValue; } int index = ic.ItemContainerGenerator.IndexFromContainer(item); return string.Format("{0}/{1}", index + 1, ic.Items.Count); } public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) { throw new NotSupportedException(); } } }
View
<Window x:Class="WpfTreeViewShowLines.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfTreeViewShowLines" Title="MainWindow" Height="300" Width="300"> <Window.DataContext> <local:ViewModel /> </Window.DataContext> <Window.Resources> <local:TreeViewItemToHeightConverter x:Key="TreeViewItemToHeightConverter" /> <local:TreeViewItemToVerticalAlignmentConverter x:Key="TreeViewItemToVerticalAlignmentConverter" /> <local:TreeViewItemToTitleConverter x:Key="TreeViewItemToTitleConverter" /> <Style x:Key="ExpandCollapseToggleStyle" TargetType="{x:Type ToggleButton}"> <Setter Property="Focusable" Value="False" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ToggleButton}"> <Grid Width="15" Height="13"> <Rectangle Width="9" Height="9" Stroke="#919191"> <Rectangle.Fill> <LinearGradientBrush EndPoint="0.5,2" StartPoint="0.5,0"> <GradientStop Color="White" Offset="0" /> <GradientStop Color="Silver" Offset="0.5" /> <GradientStop Color="LightGray" Offset="1" /> </LinearGradientBrush> </Rectangle.Fill> </Rectangle> <Rectangle x:Name="ExpandPath" Width="1" Height="5" Stroke="Black" /> <Rectangle Width="5" Height="1" Stroke="Black" /> </Grid> <ControlTemplate.Triggers> <Trigger Property="IsChecked" Value="True"> <Setter TargetName="ExpandPath" Property="Visibility" Value="Collapsed" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> <ControlTemplate x:Key="TreeViewItemTemplate" TargetType="{x:Type TreeViewItem}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition MinWidth="19" Width="Auto" /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <!--ShowLines用の線--> <Rectangle Margin="9,0,0,0" Height="1" Stroke="#DCDCDC" /> <Rectangle Width="1" Stroke="#DCDCDC" Grid.RowSpan="2"> <Rectangle.Height> <MultiBinding Converter="{StaticResource TreeViewItemToHeightConverter}"> <Binding RelativeSource="{RelativeSource AncestorType={x:Type TreeViewItem}}" Path="." /> <Binding RelativeSource="{RelativeSource AncestorType={x:Type TreeViewItem}}" Path="Header" /> </MultiBinding> </Rectangle.Height> <Rectangle.VerticalAlignment> <MultiBinding Converter="{StaticResource TreeViewItemToVerticalAlignmentConverter}"> <Binding RelativeSource="{RelativeSource AncestorType={x:Type TreeViewItem}}" Path="." /> <Binding RelativeSource="{RelativeSource AncestorType={x:Type TreeViewItem}}" Path="Header" /> </MultiBinding> </Rectangle.VerticalAlignment> </Rectangle> <ToggleButton x:Name="Expander" Style="{StaticResource ExpandCollapseToggleStyle}" IsChecked="{Binding Path=IsExpanded, RelativeSource={RelativeSource AncestorType={x:Type TreeViewItem}}}" ClickMode="Press" /> <TextBlock Grid.Column="1"> <TextBlock.Text> <MultiBinding Converter="{StaticResource TreeViewItemToTitleConverter}"> <Binding RelativeSource="{RelativeSource AncestorType={x:Type TreeViewItem}}" Path="." /> <Binding RelativeSource="{RelativeSource AncestorType={x:Type TreeViewItem}}" Path="Header" /> </MultiBinding> </TextBlock.Text> </TextBlock> <ItemsPresenter x:Name="ItemsHost" Grid.Row="1" Grid.Column="1" /> </Grid> <ControlTemplate.Triggers> <Trigger Property="HasItems" Value="False"> <Setter TargetName="Expander" Property="Visibility" Value="Collapsed" /> </Trigger> <Trigger Property="IsExpanded" Value="False"> <Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> <Style x:Key="TreeViewItemStyle" TargetType="{x:Type TreeViewItem}"> <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" /> <Setter Property="Template" Value="{StaticResource TreeViewItemTemplate}" /> </Style> </Window.Resources> <TreeView ItemsSource="{Binding Items}" ItemContainerStyle="{StaticResource TreeViewItemStyle}" VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling"> <TreeView.ItemTemplate> <HierarchicalDataTemplate ItemsSource="{Binding Children}" /> </TreeView.ItemTemplate> </TreeView> </Window>