У меня есть иерархическая структура данных, которую я пытаюсь визуализировать с помощью WPF TreeView и иерархических шаблонов данных. Количество элементов может достигать миллионов, поэтому я решил попробовать функции виртуализации, которые работают легко и хорошо, кроме той проблемы, которая возникает, когда я использую «Переработка» вместо «Стандартный» для VirtualizationMode. Давным-давно у кого-то еще была похожая проблема:
«Ошибка пути BindingExpression» с использованием ItemsControl и VirtualizingStackPanel
Тем не менее, у меня проблемы с реализацией работающей и производительной фильтрации.
Я попробовал два разных подхода, основанные на подсказках, которые я мог найти в интернете, например
Первый - использовать конвертер для установки TreeViewItem.Visibility на основе фильтра, второй - создавать ICollectionView ad hoc для всех элементов и назначать каждому из них один и тот же предикат фильтра.
Мне нравится первый подход, потому что он требует меньше кода, кажется более понятным и требует меньше хаков, и я чувствую, что мне пришлось использовать этот один хак (HackToForceVisibilityUpdate) только потому, что я не знаю лучше, но он значительно замедляет интерфейс, и я не знаю, как это исправить.
Проблема со вторым подходом состоит в том, что он сворачивает все узлы при смене фильтра (что, я думаю, можно исправить, отслеживая состояния до смены фильтра и восстанавливая их впоследствии), и что он включает в себя много дополнительного кода ( в качестве примера используется взлом одиночного кода, чтобы не слишком сильно взорвать код, а добавление / удаление элементов работать не будет).
Мне кажется, что оба подхода могут быть исправлены, но я не могу понять, как, и я действительно хотел бы исправить проблемы с производительностью первого подхода.
В посте много кода для показа, и если вам не нравятся приведенные ниже блоки кода, вот файлы в виде pastebin
и в виде архива, содержащего решение VS и
- проект WpfVirtualizedTreeViewPerItemVisibility для 1-го и
- проект WpfVirtualizedTreeViewPerItemVisibility3 для 2-го подхода
которые можно найти здесь
Первый подход:
Структуры данных:
public class TvItemBase {}
public class TvItemType1 : TvItemBase
{
public string Name1 { get; set; }
public List<TvItemBase> Entries { get; } = new List<TvItemBase>();
}
public class TvItemType2 : TvItemBase
{
public string Name2 { get; set; }
public int i { get; set; }
}
Конвертер выглядит так (он соответствует «filterFunc» во 2-м подходе)
public class TvItemType2VisibleConverter : IMultiValueConverter
{
public TvItemType2VisibleConverter() { }
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (values.Length < 2)
return Visibility.Visible;
var tvItem = values[0] as TreeViewItem;
if (tvItem == null)
return Visibility.Visible;
var entry = tvItem.DataContext as TvItemType2;
if (entry == null)
return Visibility.Visible;
var model = values[1] as IFilterProvider;
if (model == null)
return Visibility.Visible;
if (!model.ShowA)
return Visibility.Collapsed;
else if (entry.i % 2 == 0)
return Visibility.Collapsed;
else
return Visibility.Visible;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture) { throw new System.NotImplementedException(); }
}
и окно impl, которое также является моделью вида, выглядит следующим образом
public interface IFilterProvider
{
bool ShowA { get; }
}
public partial class MainWindow : Window, INotifyPropertyChanged, IFilterProvider
{
public event PropertyChangedEventHandler PropertyChanged;
void NotifyChanged(string name) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); }
bool ShowA_ = true;
public bool ShowA
{
get { return ShowA_; }
set { ShowA_ = value; NotifyChanged(nameof(ShowA)); NotifyChanged(nameof(HackToForceVisibilityUpdate)); }
}
public bool HackToForceVisibilityUpdate { get { return true; } }
void generateTestItems(TvItemType1 parent, int nof1, int nof2, int levels)
{
for (int i = 0; i < nof1; i++)
{
var i1 = new TvItemType1 { Name1 = string.Format("F_{0}.{1}.{2}.{3}", levels, i, nof1, nof2) };
parent.Entries.Add(i1);
if (levels > 0)
generateTestItems(i1, nof1, nof2, levels - 1);
}
for (int i = 0; i < nof2; i++)
parent.Entries.Add(new TvItemType2 { Name2 = string.Format("{0}.{1}.{2}.{3}", levels, nof1 + i, nof1, nof2), i = nof1 + i });
}
public MainWindow()
{
InitializeComponent();
DataContext = this;
var i1 = new TvItemType1 { Name1 = "root" };
generateTestItems(i1, 10, 1000, 3);
tv.ItemsSource = new List<TvItemBase> { i1 };
}
}
Наконец, вот xaml:
<Window.Resources>
<local:TvItemType2VisibleConverter x:Key="TvItemType2VisibleConverter"/>
<HierarchicalDataTemplate DataType="{x:Type local:TvItemType1}" ItemsSource="{Binding Entries}">
<TextBlock Text="{Binding Name1}" />
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="Visibility">
<Setter.Value>
<MultiBinding Converter="{StaticResource TvItemType2VisibleConverter}">
<Binding RelativeSource="{RelativeSource Self}" />
<!-- todo: how to specify the filter provider through a view model property (using Path and Source?) -->
<Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=local:IFilterProvider}" />
<!-- todo: how to enforce filter reevaluation without this hack -->
<Binding Path="HackToForceVisibilityUpdate" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=local:IFilterProvider}" />
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="IsExpanded" Value="True" />
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:TvItemType2}">
<TextBlock Text="{Binding Name2}" />
</HierarchicalDataTemplate>
</Window.Resources>
<Grid>
<ToggleButton Content="A" IsChecked="{Binding ShowA, Mode=TwoWay}" Width="20" Height="20" HorizontalAlignment="Left" VerticalAlignment="Top" />
<TreeView x:Name="tv"
ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.CanContentScroll="True"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Standard" Margin="0,24,0,0" />
<!-- todo: when using VirtualizingStackPanel.VirtualizationMode="Recycling", a lot of
System.Windows.Data Error: 40 : BindingExpression path error: 'Entries' property not found on 'object' ''TvItemType2' (HashCode=...)'. BindingExpression:Path=Entries; DataItem='TvItemType2' (HashCode=...); target element is 'TreeViewItem' (Name=''); target property is 'ItemsSource' (type 'IEnumerable')
are flooding the output window.
See f.e. /3197634/oshibka-puti-bindingexpression-s-ispolzovaniem-itemscontrol-i-virtualizingstackpanel -->
</Grid>
Код для второго подхода:
Структуры данных:
public class TvItemBase {}
public class TvItemType1 : TvItemBase
{
public string Name1 { get; set; }
public List<TvItemBase> Entries { get; } = new List<TvItemBase>();
public ICollectionView FilteredEntries
{
get
{
var dv = CollectionViewSource.GetDefaultView(Entries);
dv.Filter = MainWindow.singleton.filterFunc; // todo:hack
return dv;
}
}
}
public class TvItemType2 : TvItemBase
{
public string Name2 { get; set; }
public int i { get; set; }
}
и окно impl, которое также является моделью вида, выглядит следующим образом
public interface IFilterProvider
{
bool ShowA { get; }
}
public partial class MainWindow : Window, INotifyPropertyChanged, IFilterProvider
{
public event PropertyChangedEventHandler PropertyChanged;
void NotifyChanged(string name) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); }
bool ShowA_ = true;
public bool ShowA
{
get { return ShowA_; }
set
{
ShowA_ = value;
// todo:hack
// todo:why does this update the whole tree
(tv.ItemsSource as ICollectionView).Refresh();
NotifyChanged(nameof(ShowA));
}
}
void generateTestItems(TvItemType1 parent, int nof1, int nof2, int levels)
{
for (int i = 0; i < nof1; i++)
{
var i1 = new TvItemType1 { Name1 = string.Format("F_{0}.{1}.{2}.{3}", levels, i, nof1, nof2) };
parent.Entries.Add(i1);
if (levels > 0)
generateTestItems(i1, nof1, nof2, levels - 1);
}
for (int i = 0; i < nof2; i++)
parent.Entries.Add(new TvItemType2 { Name2 = string.Format("{0}.{1}.{2}.{3}", levels, nof1 + i, nof1, nof2), i = nof1 + i });
}
public bool filterFunc(object obj)
{
var entry = obj as TvItemType2;
if (entry == null)
return true;
var model = this;
if (!model.ShowA)
return false;
else if (entry.i % 2 == 0)
return false;
else
return true;
}
public MainWindow()
{
InitializeComponent();
DataContext = this;
singleton = this; // todo:hack
var i1 = new TvItemType1 { Name1 = "root" };
generateTestItems(i1, 10, 1000, 3);
//generateTestItems(i1, 3, 10, 3);
var l = new List<TvItemBase> { i1 };
var dv = CollectionViewSource.GetDefaultView(l);
dv.Filter = filterFunc;
tv.ItemsSource = dv;
}
public static MainWindow singleton = null; // todo:[really big]hack
}
Наконец, вот xaml:
<Window.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:TvItemType1}" ItemsSource="{Binding FilteredEntries}">
<TextBlock Text="{Binding Name1}" />
<HierarchicalDataTemplate.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="True" />
</Style>
</HierarchicalDataTemplate.ItemContainerStyle>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:TvItemType2}">
<TextBlock Text="{Binding Name2}" />
</HierarchicalDataTemplate>
</Window.Resources>
<Grid>
<ToggleButton Content="A" IsChecked="{Binding ShowA, Mode=TwoWay}" Width="20" Height="20" HorizontalAlignment="Left" VerticalAlignment="Top" />
<TreeView x:Name="tv"
ScrollViewer.VerticalScrollBarVisibility="Visible"
ScrollViewer.CanContentScroll="True"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Standard" Margin="0,24,0,0" />
</Grid>