Menu

FastObjectListView doesn't preserve selection

Vu N.
2014-03-31
2018-06-21
  • Vu N.

    Vu N. - 2014-03-31

    Hi,

    We recently switched from using the ObjectListView to FastObjectListView for our solution due to the large number of objects that need to be displayed. FastObjectListView lives up to its name and provides a significant increase in performance. Since our solution provides network device monitoring, object data can be quite dynamic so the object collection updates fairly often.

    When using the ObjectListView, row selection preservation worked as expected; however, with the FastObjectListView selection preservation seems to be broken. The number of the selected rows is correct, but the selected row(s) changes to some other random row(s) on update.

    I have used both the SetObjects() method and the Objects property to update the listview collection with the same result. I have also scoured and stepped through the FastObjectListView source to determine the issue without success.

    Has anyone encountered this and would you have any suggestions to remedy the issue?

    Thanks,

    -Vu

     
  • Dialecticus

    Dialecticus - 2018-06-06

    I can confirm this problem. It can be observed in demo, with some code tweaking. In TabFastList.buttonRemove_Click method replace this line:

    this.ListView.RemoveObjects(this.ListView.SelectedObjects);
    

    with this line:

    this.ListView.RemoveObjects(this.ListView.CheckedObjects);
    

    Check the first row (Wilhelm Frat) and select the second and the last row (Alana Roderick and Mister Null) and click Remove. We expect the first row to be removed and selection to persist. First row will be removed, but Frank Price will be the only selected row, instead of Alana nad Null.

    I'm guessing the problem is that SelectedIndices property is not updated on call to RemoveObjects. Plain old ObjectListView does not have this problem.

    The same problem happens when new objects are inserted. Change the code in TabFastList.buttonAdd_Click to call InsertObjects(0, ...) instead of AddObjects, and also to insert only one instead of 1000. SelectedIndices will be preserved, instead of updated.

     

    Last edit: Dialecticus 2018-06-06
  • Dialecticus

    Dialecticus - 2018-06-17

    This is the code that apparently fixes the problem. The code should replace the existing methods in FastObjectListDataSource. Originally I fixed the issue by using SelectedObjects instead of SelectedIndices, but that fix requires changes in FastObjectListView as well, and likely a new method in IVirtualListDataSource. This fix is more elegant, if it works.

    public override void InsertObjects(int index, ICollection modelObjects)
    {
        this.fullObjectList.InsertRange(index, modelObjects);
        this.FilterObjects();
        this.RebuildIndexMap();
    
        var newSelectedIndices = new List<int>();
    
        foreach (int selectedIndex in listView.SelectedIndices)
            if (selectedIndex >= index)
            {
                listView.SelectedIndices.Remove(selectedIndex);
                newSelectedIndices.Add(selectedIndex + modelObjects.Count);
            }
    
        listView.UpdateVirtualListSize();
        foreach (var newIndex in newSelectedIndices)
            listView.SelectedIndices.Add(newIndex);
    }
    
    public override void RemoveObjects(ICollection modelObjects)
    {
        List<int> indicesToRemove = new List<int>();
        foreach (object modelObject in modelObjects) {
            int i = this.GetObjectIndex(modelObject);
            if (i >= 0)
                indicesToRemove.Add(i);
        }
    
        indicesToRemove.Sort();
    
        int offset = 0;
        var newSelectedIndices = new List<int>();
    
        foreach (int index in listView.SelectedIndices)
        {
            while (offset < indicesToRemove.Count && indicesToRemove[offset] < index)
                offset++;
    
            if (offset >= indicesToRemove.Count || indicesToRemove[offset] != index)
                newSelectedIndices.Add(index - offset);
        }
    
        listView.SelectedIndices.Clear();
        foreach (var index in newSelectedIndices)
            listView.SelectedIndices.Add(index);
    
        foreach (object modelObject in modelObjects)
            this.fullObjectList.Remove(modelObject);
    
        this.FilterObjects();
        this.RebuildIndexMap();
    }
    
     
  • Vu N.

    Vu N. - 2018-06-21

    Hi Dialecticus. Thanks for looking into this issue, but I was using the SetObjects method to update the contents of the FastObjectListView en masse since I have a very large number of model objects and, potentially, a large number of model object property changes.

    I found the issue stems from sorting the updated VirtualListDataSource. Apparently when the ListViewItem index map gets rebuilt, the associated indices change and therefore the selected objects change.

    To resolve the issue, I ended up implementing a new method within the VirtualObjectListView class, UpdateObjects(IEnumerable collection), which assumes the selection state must be preserved. This method behaves as SetObjects does with the exception that it also saves the existing SelectedObjects collection and then re-selects the previously selected objects after updating the VirtualListDataSource. I'm uncertain if this is the most efficient fix, but it resolves my issue, and the impact on performance is imperceptible.

    There is a caveat. For this to work, my model object class has a unique identifier property and implements IEquatable to override the default equality comparison, and the override only compares the unique identifier for equality.

    public void UpdateObjects(IEnumerable collection)
    {
        if (this.InvokeRequired)
        {
            this.Invoke((MethodInvoker)delegate { this.UpdateObjects(collection); });
            return;
        }
    
        if (this.VirtualListDataSource == null)
            return;
    
        // Give the world a chance to cancel or change the assigned collection
        ItemsChangingEventArgs args = new ItemsChangingEventArgs(null, collection);
        this.OnItemsChanging(args);
        if (args.Canceled)
            return;
    
        this.BeginUpdate();
        try
        {
            // Save a copy of currently selected objects.
            var selectedObjects = this.SelectedObjects;
    
            this.VirtualListDataSource.SetObjects(args.NewObjects);
            this.BuildList();
            this.UpdateNotificationSubscriptions(args.NewObjects);
    
            // If there were previously selected objects, re-select them.
            if (selectedObjects != null && selectedObjects.Count > 0)
                this.SelectObjects(selectedObjects);
        }
        finally
        {
            this.EndUpdate();
        }
    }
    
    /// <summary>
    /// Set the collection of objects that this control will show.
    /// </summary>
    /// <param name="collection"></param>
    /// <param name="preserveState">Should the state of the list be preserved as far as is possible.</param>
    public override void SetObjects(IEnumerable collection, bool preserveState)
    {
        if (preserveState)
        {
            UpdateObjects(collection);
            return;
        }
    
        if (this.InvokeRequired)
        {
            this.Invoke((MethodInvoker)delegate { this.SetObjects(collection, preserveState); });
            return;
        }
    
        if (this.VirtualListDataSource == null)
            return;
    
        // Give the world a chance to cancel or change the assigned collection
        ItemsChangingEventArgs args = new ItemsChangingEventArgs(null, collection);
        this.OnItemsChanging(args);
        if (args.Canceled)
            return;
    
        this.BeginUpdate();
        try
        {
            // Apparently a side effect of the object "re-selection", the selected indices
            // remain selected regardless of preserveState value and new object list content
            // so we to have to explicitly clear the selected indices if we do not wish to
            // preserve the selection state.
            this.SelectedIndices.Clear();
            this.VirtualListDataSource.SetObjects(args.NewObjects);
            this.BuildList();
            this.UpdateNotificationSubscriptions(args.NewObjects);
        }
        finally
        {
            this.EndUpdate();
        }
    }
    
     

    Last edit: Vu N. 2018-06-21

Log in to post a comment.

MongoDB Logo MongoDB