左边的树(TOC)有根节点(例如'TextEditor'的部分)。每个部分都包含设置类别(例如“格式化”)。右侧(设置视图)的ListView
项目具有组标题,其类别名称与目录名称匹配(例如格式化)。
假设:
- 在一个名为CollectionViewSource的内部存在一个
CollectionViewSource
定义
。ResourceDictionary
- 设置数据项有一个属性
SettingsCategoryName
(例如格式)。
- 的
SettingsCategoryName
of SelectedItem
the
TreeView
绑定到一个属性SelectedSettingsCategoryName
查看.xaml:
<ResourceDictionary>
<CollectionViewSource x:Key="CollectionViewSource" Source="{Binding Settings}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="SettingsCategoryName"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</ResourceDictionary>
<ListView x:Name="ListView" ItemsSource="{Binding Source={StaticResource CollectionViewSource}}">
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock FontWeight="Bold"
FontSize="14"
Text="{Binding Name}" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
View.xaml.cs:
找到选定的类别并将其滚动到视口的顶部。
// Scroll the selected section to top when the selected item has changed
private void ScrollToSection()
{
CollectionViewSource viewSource = FindResource("CollectionViewSource") as CollectionViewSource;
CollectionViewGroup selectedGroupItemData = viewSource
.View
.Groups
.OfType<CollectionViewGroup>()
.FirstOrDefault(group => group.Name.Equals(this.SelectedSettingsCategoryName));
GroupItem selectedroupItemContainer = this.ListView.ItemContainerGenerator.ContainerFromItem(selectedGroupItemData) as GroupItem;
ScrollViewer scrollViewer;
if (!TryFindCildElement(this.ListView, out scrollViewer))
{
return;
}
// Subscribe to scrollChanged event
// because the scroll executed by `BringIntoView` is deferred.
scrollViewer.ScrollChanged += ScrollSelectedGroupToTop;
selectedGroupItemContainer?.BringIntoView();
}
private void ScrollSelectedGroupToTop(object sender, ScrollChangedEventArgs e)
{
ScrollViewer scrollViewer;
if (!TryFindCildElement(this.ListView, out scrollViewer))
{
return;
}
scrollViewer.ScrollChanged -= ScrollGroupToTop;
var viewSource = FindResource("CollectionViewSource") as CollectionViewSource;
CollectionViewGroup selectedGroupItemData = viewSource
.View
.Groups
.OfType<CollectionViewGroup>()
.FirstOrDefault(group => group.Name.Equals(this.SelectedSettingsCategoryName));
var groupIndex = viewSource
.View
.Groups.IndexOf(selectedGroupItemData);
var absoluteVerticalScrollOffset = viewSource
.View
.Groups
.OfType<CollectionViewGroup>()
.TakeWhile((group, index) => index < groupIndex)
.Sum(group =>
(this.ListView.ItemContainerGenerator.ContainerFromItem(group) as GroupItem)?.ActualHeight
?? 0
);
scrollViewer.ScrollToVerticalOffset(absoluteVerticalScrollOffset);
}
// Generic method to find any `DependencyObject` in the visual tree of a parent element
private bool TryFindCildElement<TElement>(DependencyObject parent, out TElement resultElement) where TElement : DependencyObject
{
resultElement = null;
for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
{
DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);
if (childElement is Popup popup)
{
childElement = popup.Child;
}
if (childElement is TElement)
{
resultElement = childElement as TElement;
return true;
}
if (TryFindCildElement(childElement, out resultElement))
{
return true;
}
}
return false;
}
您可以将此方法移动到ListView
派生类型中。然后将 a 添加到处理路由命令CommandBindings
的新自定义中,例如. 将 a模板化为a并让他们发出命令以将部分名称传递给 custom 。ListView
ScrollToSectionRoutedCommand
TreeViewItems
Button
CommandParameter
ListView
备注
由于使用PropertyGroupDescription
导致混合数据类型的项目源(GroupItemData
对于组标头和实际数据项目),因此宿主的 UI 虚拟化ItemsControl
被禁用并且不可能(请参阅 Microsoft Docs:
优化性能:控件)。在这种情况下,附加属性ScrollViewer.CanContentScroll
会自动设置为False
(强制)。对于大列表,这可能是一个巨大的缺点,也是采用替代方法的理由。
2. 替代方案(支持 UI 虚拟化)
当涉及到实际设置结构的设计时,有几种可能的变化。它可以是一棵树,其中每个类别标题节点都有自己的子节点,这些子节点表示类别的设置,也可以是一个平面列表结构,其中类别标题和设置都是同级的。为了示例的简单起见,我选择了第二个选项:平面列表数据结构。
2.1 设置
基本思想:使用具有两个级别
的a 进行TreeView
模板化。HierarchicalDataTemplate
第二级的TreeView
(叶子)和ListView
页眉项目共享相同的实例(IHeaederData
见后文)。因此,选定的标题项目TreeView
引用完全相同的项目标题ListView
- 无需搜索。
实施概述:
- 你需要两个
ItemsControl
元素:
- 一个
TreeView
用于左侧导航窗格,有两个级别
- 带有部分根节点(例如“文本编辑器”)
- 以及该部分的设置类别标题子节点(叶节点)(例如“字体”、“格式”)
- 一个
ListView
用于实际设置及其类别标题。
- 然后设计数据类型来表示一个设置、一个设置头和一个节根节点
- 让他们都实现一个
IData
具有共享属性的公共(例如标题)
- 让设置头数据类型实现一个额外的
IHeaderData
- 让设置数据类型实现一个额外的
ISettingData
- 让实现的父节节点数据类型(根节点)
TreeView
附加ISectionData
一个具有子类型的IHeaderData
- 创建项目源集合(所有类型
IEnumerable<IData>
)
TreeView
一个用于(仅包含类别)的每个父节节点,SectionCollection
类型为ISectionData
- 每个类别一个,一个
CategoryCollection
类型IHeaderData
- 一个用于设置数据和共享类别(标题数据),一个
SettingCollection
类型IData
- 逐节填充排序的源集合
- 将类型的部分数据实例添加
ISectionData
到的源集合SectionCollection
中TreeView
- 将类型的共享类别数据头实例添加
IHeaderData
到源集合CategoryCollection
和SettingCollection
- 添加一个类型的设置实例
ISettingData
,一个用于类别的每个设置,到SettingCollection
唯一的
- 对当前部分的所有类别重复最后两个步骤
- 分配给
CategoryCollection
根节点的ISectionData
子集合
- 对所有部分重复这些步骤(及其类别和相应的设置)
- 绑定
SectionCollection
到 TreeView
- 绑定
SettingsCollection
到LIstView
- 为类型为根
HierarchicalDataTemplate
的TreeView
数据创建一个ISectionData
- 创建
DataTemplate
两个ListView
- 一个针对
IHeaderData
- 一个针对
ISettingData
逻辑:
- When a
IHeaderData
item of the TreeView
is selected then
- 使用获取
ListView
此数据项的项容器var container = ItemsContainerGenerator.GetContainerFromItem(selectedTreeViewCategoryItem)
- 将容器滚动到视图
container.BringIntoView()
中(实现视图外的虚拟化项目)
- 将容器滚动到视图顶部
因为TreeView
和ListView
共享相同的类别标题数据 ( IHeaderData
),所选项目易于跟踪和查找。您不必搜索设置组。您可以使用引用直接跳转到组。这意味着数据的结构是解决方案的关键。