实现可选择的虚拟列表
本文是数据展示性能优化系列文章的第二部分。可选择的虚拟列表是一个列表,您可以从中选择单个项目,并将其移出或移入列表。您还可以使用全选复选框来选择列表中的所有项目。
摘要
本文是数据展示性能优化系列文章的第二部分。在第一部分中,我们解决了使用 UI 中的虚拟列表技术和后端数据存储访问层中的分页技术来显示大量数据列表的问题。
在第二部分中,我们将讨论可选择的虚拟列表。可选择的虚拟列表是一个列表,您可以从中选择单个项目,并将其移出或移入列表。您还可以使用全选复选框来选择列表中的所有项目并将其移出,或者使用取消全选复选框来取消选择列表中的所有项目。
代码示例是用 WPF 和 C# 编写的,并使用了 Model View ViewModel (MVVM) 模式。
Content
示例应用程序有两个列表:可用员工列表和已选员工列表。员工姓名以字母顺序编号。用户可以选中员工旁边的复选框,然后单击箭头按钮将员工从可用列表中移到已选列表中。用户还可以选中可用列表顶部、员工字母右侧的“全选”复选框,以选择所有员工。
在文章中,我们将窗口左侧显示仍可供选择/取消选择的员工列表的 listview 称为“可用列表”。我们将窗口右侧显示已选择并已从可用列表中移除的员工列表的 listview 称为“已选列表”。
为了实现这一功能,我们需要将以下概念引入虚拟列表:可选择 (Selectable)、移除列表 (RemovedList) 和全选/取消全选 (Select All/Deselect All)。可选择允许用户选择虚拟列表中的项目;移除列表用于跟踪已从虚拟列表中移除的项目;全选/取消全选允许用户选择所有可用项目或取消选择所有可用项目。
可选择 (Selectable)
可选择允许用户选择或取消选择虚拟列表中的单个或多个项目。在我们的示例中,用户只需选中员工编号右侧的复选框即可选择/取消选择员工。用户可以多次选中复选框以选择多个项目,然后单击箭头按钮将它们从可用列表中移出。
在 UI (View) 中,可用员工列表被定义为 ListView
。其 ItemsSource
绑定到 ViewModel 中的 AvaialbleEmployeeCollection
。也就是说,无论何时您在 ViewModel 的 AvailableEmployeeCollection
中更新数据,数据都会自动显示在 UI 中。
<ListView x:Name="availableListView"
ItemsSource="{Binding AvailableEmployeeCollection}">
<ListView.View>
<GridView>
<GridViewColumn CellTemplate="{StaticResource CustomCellTemplate}"/>
<GridViewColumn Header="Employee"
CellTemplate="{StaticResource EmployeeNameTemplate}" />&
</GridView>
</ListView.View>
<ListView>
public class ViewModel : INotifyPropertyChanged
{
public SelectableVirtualList<Employee> AvailableEmployeeCollection
{
get { return _AvailableEmployeeCollection; }
set
{
_AvailableEmployeeCollection = value;
OnPropertyChanged("AvailableEmployeeCollection");
}
}
public ObservableCollection<Employee> SelectedEmployeeCollection {get; set;}
}
ListView
包含两列,即 GridViewColumn
。第一列 GridViewColumn
的 CellTemplate
是一个 CustomCellTemplate
,它被定义为一个 CheckBox
,并且没有 ColumnHeader
;第二列 GridViewCColumns
的 CellTemplate
是 EmployNameTemplate
,它被定义为一个员工字符串,列标题为“Employee”。
CustomCellTemplate
包含一个复选框,该复选框绑定到 Employee
对象中的 IsSelected
属性,即如果复选框被选中,则 Employee
对象中的 IsSelected
属性将被设置为 true。
<DataTemplate x:Key="CustomCellTemplate">
<CheckBox Tag="{Binding}"
IsChecked="{Binding IsSelected}"
Style="{DynamicResource AnswerCheckBox}"/>
</DataTemplate>
EmployeeNameTemplate
包含一个 TextBlock
,该 TextBlock
绑定到 Employee
对象中的 Name
属性。在我们的示例中,名称显示为数字,例如 1、2、3、4、5……
<DataTemplate x:Key="EmployeeNameTemplate">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
Employee
类实现了 ISelectable
接口,ISelectable
有一个 IsSelected
属性。Employee
类还实现了 INotifyPropertyChanged
接口,并使用 OnPropertyChanged
事件将 UI 更改与 Employee
对象关联起来。也就是说,一旦用户在 UI 上选中了员工 A,员工 A 的 IsSelected
值就会自动设置为 true。
public interface ISelectable
{
bool IsSelected { get; set; }
}
public class Employee : ISelectable, INotifyPropertyChanged
{
public string Name {get;set}
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
_isSelected = value;
OnPropertyChanged("IsSelected");
}
}
一旦用户单击箭头按钮将员工从可用列表移到已选列表,就会调用 ViewModel.cs 中的 Add()
方法。它将遍历 AvaialbleEmployeeCollection
中已选员工列表,并将它们逐个移到 SelectedEmployeeCollection
。
请记住,我们处理的是一个大型项目列表,并且并非所有项目都已加载。因此,我们无法遍历 AvailableEmployeeCollection
中的所有项目并找到已选中的项目。引入 SelectedList
属性是为了在不遍历所有可用项目的情况下获取已选中的项目。
public void Add()
{
IList<Employee> employees = AvailableEmployeeCollection.SelectedList;
foreach (var employee in employees)
{
employee.IsSelected = false;
if (!SelectedEmployeeCollection.Contains(employee))
{
SelectedEmployeeCollection.Add(employee);
}
AvailableEmployeeCollection.Remove(employee);
}
}
基本上,SelectedList
属性只查看缓存的项目,并获取 IsSelected
值为 true 的项目。如果一个项目尚未加载和缓存,那么我们可以确定该项目尚未被选中。
public IList<T> SelectedList
{
get {
IList<T> selectedlList = new List<T>();
for (int i = 0; i < Cache.Length; i++) {
if (Cache[i] != null && Cache[i].IsSelected)
selectedList.Add(Cache[i]);
return selectedVirtualList;
}
}
现在用户可以单击箭头按钮将选中的项目移到左侧的“已选列表”。
我们期望的行为是什么?项目应该显示在可用列表和已选列表吗?还是在移动到已选列表后,项目应该从可用列表中移除?在本文中,这些项目将从可用列表中移除。但是,在我接下来的系列文章中,这些项目将保留在可用列表中,但标记为禁用。
移除列表 (RemovedList)
RemovedList
处理已移至“已选列表”的项目。一旦项目被移至“已选列表”,就需要从“可用列表”中移除它们。我们面临的挑战是,我们实际上无法从底层数据访问层或虚拟列表中的缓存中移除它们。数据访问层是只读的,并且项目索引/页固定,缓存大小和项目索引也必须固定,以便正确映射底层数据访问层。
解决方案是建立 SelectableVirtualList
与内部缓存之间的间接映射,该缓存与底层数据有固定链接。例如,如果项目 3 已被移至“已选列表”,那么项目 3 将从“可用列表”(SelectableVirtualList
)中移除,但项目 3 仍将保留在缓存列表中。我们使用映射来跟踪已移除的项目。
在 VirtualList.cs 中,有一个 RemovedLtemList
。这个列表跟踪已移除项目的索引。一旦虚拟列表中的 Insert
和 RemoveAt
方法被调用,RemovedItemIndexList
也会被更新。
private readonly List<int> _removedItemIndexList = new List<int>();
protected List<int> RemovedItemIndexList
{
get { return _removedItemIndexList; }
}
public void Insert(int index, T item)
{
if (_removedItemIndexList.Contains(index))
_removedItemIndexList.Remove(index);
}
public void RemoveAt(int index)
{
_removedItemIndexList.Add(index);
}
UI 使用 AdjustIndex
函数显示需要刷新的项目。AdjustIndex
将 UI 显示的索引调整为缓存索引。例如,如果员工 1 已被移除,那么员工 2 将显示在 UI 的第一个位置,并且 this[in index]
方法中的索引值将是 1,AdjustIndex
将查找 RemovedItemIndexList
并将索引正确地调整为缓存的 2。
public T this[int index]
{
get { return Get(AdjustIndex(index)); }
set { Insert(index, value); }
}
private int AdjustIndex(int index)
{
int adjustedIndex = index;
List<int> orderedRemovedItemList =
(from each in _removedItemIndexList orderby each ascending select each).ToList();
for (int i = 0; i < orderedRemovedItemList.Count; i++)
{
int removedItemPosition = orderedRemovedItemList[i];
if (removedItemPosition <= adjustedIndex)
{
adjustedIndex++;
}
}
return adjustedIndex;
}
现在我们已经完成了可选择和移除列表的概念。如果用户想选择列表中的所有项目并移除它们,或者用户想取消选择所有项目怎么办?当用户选择“全选”时,虚拟列表中的所有项目都需要加载并选中吗?这将对虚拟列表产生巨大的性能影响。
全选/取消全选 (Select All / Deselect All)
解决方案是引入一个 SelectAllFlagOn
标志。当用户选中“全选”复选框时,虚拟列表中的所有项目都会被选中,并且 SelectAllFlagOn
标志将被设置为 true。当用户取消全选时,而不是加载和选中虚拟列表中的所有项目,而是将 DeSelectAllFlagOn
标志设置为 true。
如果项目已加载到缓存中,那么 SelectAllFlagOn
属性将设置项目的 IsSelected
值为 true,以便在 UI 中正确显示。如果项目尚未加载,则不会设置任何内容。
internal bool SelectAllFlagOn
{
get { return _selectAllFlagOn; }
set
{
_selectAllFlagOn = value;
_deselectAllFlagOn = !value;
foreach (T each in Cache)
{
if (each != null)
each.IsSelected = value;
}
}
}
}
如果用户向下滚动 UI 中的可用列表,则项目将从数据存储中获取并缓存。根据 SelectAllFlagOn
和 DeSelectAllFlagOn
标志,它确定最近加载的项目是否被选中,并为其分配适当的值(Cache[index].IsSelected = SelectAllFlagOn && !DeSelectAllFlagOn
)。然后它将在 UI 中正确显示。
protected override T Get(int index)
{
if (!IsItemCached(index))
{
CacheItem(index);
Cache[index].IsSelected = SelectAllFlagOn && !DeSelectAllFlagOn;
}
return Cache[index];
}
在用户选中“全选”按钮并单击箭头按钮将所有项目移至“已选列表”后,程序将要求 SelectableVirtualList
返回“已选列表”然后将其移出。
public IList<T> SelectedList
{
get
{
IList<T> selectedList = new List<T>();
for (int i = 0; i < Cache.Length; i++)
{
if (!RemovedItemIndexList.Contains(i))
{
if (Cache[i] != null && Cache[i].IsSelected)
{
selectedList.Add(Cache[i]);
}
else if (SelectAllFlagOn)
{
CacheItem(i);
Cache[i].IsSelected = SelectAllFlagOn && !DeSelectAllFlagOn;
selectedList.Add(Cache[i]);
}
}
}
return selectedList;
}
}
结论
搞定,就是这样。现在您拥有了一个 SelectableVirtualList
来处理大量数据项。我们将性能开销从 O(n) 降低到 O(1)。用户可以从列表中选择任何项目,将其从列表中移除,还可以选择列表中的所有项目。这标志着数据展示性能优化系列文章第二部分的结束。在下一篇文章中,我们将讨论如何搜索虚拟列表和从列表中移除项目。