Abstract
This article is Part 2 of the data display performance optimizing series. In Part 1, we addressed displaying a
large list of data using a combination of the Virtual List technique in UI and the paging technique in the backend data store access layer.
In Part 2, we will address the selectable virtual list. The selectable virtual list is a list where you can select
individual items in the list and move it out or in to the list. You can also use a select all checkbox to select all items in the list and move them out, or you
can use a deselect all checkbox to deselect all items in the list.
The code examples are written in WPF and C#, and the Model View ViewModel (MVVM) pattern has been used.
Content
The example application has two lists, an available employee list and a selected list. The employees are named as numbers in
alphabetic order. The user can check the checkbox beside the employee and then click on the arrow button to move employees from the available list to the selected list. The user
can also check the checkall checkbox, which is located on the top of the available list and just on the right side of the letter employee, to select all the employees.
In the article, we refer to the listview in the left part of the window which displays the list of employees which are still available to be
selected/deselected as the Available List. We refer to the listview in the right part of the window which displays the list of employees which displays the list of employees
which have been selected and removed from the Available List as the Selected List.
Picture 1: Selectable Virtual List
In order to make it work, we need to introduce these concepts to the Virtual List: Selectable, RemovedList, and Select All/Deselect All. Selectable
allows users to select items in the virtual list; RemovedList keeps track of items which have been removed from the virtual list; Select All/Deselect All
allows users to select all the available items or deselect all the available items.
Selectable
Selectable allows users to select or deselect single or multiple items in the virtual list. In our example, the user can just check on a CheckBox which resides on
the right side of the employee number to select/deselect the employee. The user can check the checkbox multiple times to select multiple items and then click on the
arrow button to move them out of the available list.
In the UI (View), the available employee list is defined as a ListView
. Its ItemsSource
binds with the AvaialbleEmployeeCollection
in the ViewModel.
I.e., whenever you update data in the AvailableEmployeeCollection
in the ViewModel, the data will automatically be displayed in the 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;}
}
The ListView
contains two columns which are GridViewColumn
s. The first GridViewColumn
's CellTemplate
is a CustomCellTemplate
,
which is defined as a CheckBox
, and it has no ColumnHeader
, and the second GridViewCColumns
's
CellTemplate
is EmployNameTemplate
, which is defined as an employee string and the column header is Employee.
The CustomCellTemplate
contains a checkbox which binds to the IsSelected
property in the Employee
object, i.e., if the checkbox gets checked and the
IsSelected
property is set to true in the Employee
object.
<DataTemplate x:Key="CustomCellTemplate">
<CheckBox Tag="{Binding}"
IsChecked="{Binding IsSelected}"
Style="{DynamicResource AnswerCheckBox}"/>
</DataTemplate>
EmployeeNameTemplate
contains a TextBlock
which binds to the Name
property in the Employee
object. In our example, the name is displayed as a number, e.g., 1,2,3,4,5 ....
<DataTemplate x:Key="EmployeeNameTemplate">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
The Employee
class implements the ISelectable
interface, and ISelectable
has an IsSelected
property. The Employee
class
also implements the INotifyPropertyChanged
interface, and it uses the OnPropertyChanged
event to wire up the UI change with the
Employee
object. I.e., once the user check on the UI for employee A, employee A's IsSelected
value will be set to true automatically.
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");
}
}
Once the user clicks on the arrow button to move the employees from the available list to the selected list, the Add()
method in ViewModel.cs is called. It will
walk through the list of selected employees in the AvaialbleEmployeeCollection
, and move them one by one to the SelectedEmployeeCollection
.
Remember that we are dealing with a large list of items, and not all the items are loaded. So we can not look through all items in the AvailableEmployeeCollection
and find
items which are selected. The SelectedList
property was introduced to get the selected items without looking through all the available items.
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);
}
}
Basically, the SelectedList
property only looks at the cached items and get the items whose IsSelected
value is true. If an item has not been loaded and cached,
then we are pretty sure that the item has not been selected.
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;
}
}
Now the user can click on the arrow button to move the selected items to the Selected List on the left.
What behavior do we expect then? Should the items be displayed in both the available list and the selected list, or should the items be
removed from the the available list after they are moved to the selected list? In the article, these items will be removed from the available list. However in my
next article of this series, these items will remain in the available list, but marked as disabled.
RemovedList
RemovedList
deals with items which have been removed to the Selected List. Once the items have been removed to the Selected List, they need to be removed
from the Available List. The challenge we are facing is that we can not actually remove them from the underlying data access layer nor from the cache in the virtual list.
The data access layer is read only, and the item index/page is fixed, the cache size and item index have to be fixed too in order to map the underlying data access layer properly.
The solution is to have an indirect mapping between SelectableVirtualList
to the internal cache which has a fixed link with the underlying data. For example, if Item 3
has been removed to the Selected List, then Item 3 will be removed from the Available List (SelectableVirtualList
), however Item 3 will still stay in the cache
list. We use mapping to keep track of items that have been removed.
In VirtualList.cs, it has a RemovedLtemList
. This list keeps track of the index of items which have been removed. Once the Insert
and RemoveAt
methods in the virtual list get invoked, RemovedItemIndexList
will be updated as well.
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);
}
The UI displays items that need to be refreshed by using the AdjustIndex
function. AdjustIndex
will adjust the index from the UI display to
the cached index. E.g., if employee 1 has been removed, then employee 2 will be displayed in the first position in the UI, and the index value in
the this[in index]
method will be 1, and AdjustIndex
will look into RemovedItemIndexList
and properly adjust the index to 2 for the cache.
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;
}
Now we have completed the Selectable and RemovedList concepts. What if the user would like to select all items in the list and remove them, or what if the user
would like to deselect all? When the user does a Select All, will all items in the virtual list need to be loaded and selected? This will introduce
a huge performance impact on the virtual list.
Select All / Deselect All
The solution is to introduce a SelectAllFlagOn
flag. When the user selects the SelectAll checkbox, all the items get selected in the Virtual List,
and the SelectAllFlagOn
flag will be set to true. When the user unselects all, instead of all items getting loaded and selected in the virtual list,
the DeSelectAllFlagOn
flag is set to true.
If the item has already been loaded into the cache, then the SelectAllFlagOn
property will set the item's IsSelected
value to true so it can be displayed properly in UI. If the item has not been loaded yet, then nothing will be set.
internal bool SelectAllFlagOn
{
get { return _selectAllFlagOn; }
set
{
_selectAllFlagOn = value;
_deselectAllFlagOn = !value;
foreach (T each in Cache)
{
if (each != null)
each.IsSelected = value;
}
}
}
}
If the user scrolls down the available list in the UI, then the item will be fetched from the data store and cached in the cache. Based on the SelectAllFlagOn
and
DeSelectAllFlagOn
flags, it determines if the recently loaded item is selected or not, and assigns the proper value to it (Cache[index].IsSelected = SelectAllFlagOn &&
!DeSelectAllFlagOn
). Then it will be displayed in UI properly.
protected override T Get(int index)
{
if (!IsItemCached(index))
{
CacheItem(index);
Cache[index].IsSelected = SelectAllFlagOn && !DeSelectAllFlagOn;
}
return Cache[index];
}
After the user selects the CheckAll button and click on the arrow button to move all the items to the Selected List, the program will then ask SelectableVirtualList
to return
the Selected List and then move it out.
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;
}
}
Conclusion
Voila, that is it. Now you have a SelectableVirtualList
to deal with large sets of data items. We reduce the performance overhead from o(n) to o(1). The user can select any item from
the list, remove it from the list, and can also select all items from the list. This concludes Part 2 of the data display performance optimizing series. We will talk about how to
search the virtual list and remove an item from the list in the next part of this series.
Appendix