Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Implement Selectable Virtual List

0.00/5 (No votes)
3 Jan 2013 1  
This article is Part 2 of the data display performance optimizing series. 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 the select all checkbox to select all items in the list.

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

Selectable Virtual List Picture

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 GridViewColumns. 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.

Removed List

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.

Select All

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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here