Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF Listbox Virtualization creates DisconnectedItems

I'm attempting to create a Graph control using a WPF ListBox. I created my own Canvas which derives from a VirtualizingPanel and I handle the realization and virtualization of items myself.

The listbox' item panel is then set to be my custom virtualized canvas.

The problem I am encountering occurs in the following scenario:

  • ListBox Item A is created first.
  • ListBox Item B is created to the right of Item A on the canvas.
  • ListBox Item A is virtualized first (by panning it out of view).
  • ListBox Item B is virtualized second (again by panning it out of view).
  • Bring ListBox Item A and B in view (i.e: realize them)
  • Using Snoop, I detect that the ListBox has now 3 items, one of them being a "DisconnectedItem" located directly underneath ListBox Item B.

What causes the creation of this "DisconnectedItem" ? If I were to virtualize B first, followed by A, this item would not be created. My theory is that virtualizing items that precedes other items in a ListBox causes children to be disconnected.

The problem is even more apparent using a graph with hundreds of nodes, as I end up with hundreds of disconnected items as I pan around.

Here is a portion of the code for the canvas:

/// <summary>
/// Arranges and virtualizes child element positionned explicitly.
/// </summary>
public class VirtualizingCanvas : VirtualizingPanel
{
   (...)

    protected override Size MeasureOverride(Size constraint)
    {
        ItemsControl itemsOwner = ItemsControl.GetItemsOwner(this);

        // For some reason you have to "touch" the children collection in 
        // order for the ItemContainerGenerator to initialize properly.
        var necessaryChidrenTouch = Children;

        IItemContainerGenerator generator = ItemContainerGenerator;

        IDisposable generationAction = null;

        int index = 0;
        Rect visibilityRect = new Rect(
            -HorizontalOffset / ZoomFactor,
            -VerticalOffset / ZoomFactor,
            ActualWidth / ZoomFactor,
            ActualHeight / ZoomFactor);

        // Loop thru the list of items and generate their container
        // if they are included in the current visible view.
        foreach (object item in itemsOwner.Items)
        {
            var virtualizedItem = item as IVirtualizingCanvasItem;

            if (virtualizedItem == null || 
                visibilityRect.IntersectsWith(GetBounds(virtualizedItem)))
            {
                if (generationAction == null)
                {
                    GeneratorPosition startPosition = 
                                 generator.GeneratorPositionFromIndex(index);
                    generationAction = generator.StartAt(startPosition, 
                                           GeneratorDirection.Forward, true);
                }

                GenerateItem(index);
            }
            else
            {
                GeneratorPosition itemPosition = 
                               generator.GeneratorPositionFromIndex(index);

                if (itemPosition.Index != -1 && itemPosition.Offset == 0)
                {
                    RemoveInternalChildRange(index, 1);
                    generator.Remove(itemPosition, 1);
                }

                // The generator needs to be "reseted" when we skip some items
                // in the sequence...
                if (generationAction != null)
                {
                    generationAction.Dispose();
                    generationAction = null;
                }
            }

            ++index;
        }

        if (generationAction != null)
        {
            generationAction.Dispose();
        }

        return default(Size);
    }

   (...)

    private void GenerateItem(int index)
    {
        bool newlyRealized;
        var element = 
          ItemContainerGenerator.GenerateNext(out newlyRealized) as UIElement;

        if (newlyRealized)
        {
            if (index >= InternalChildren.Count)
            {
                AddInternalChild(element);
            }
            else
            {
                InsertInternalChild(index, element);
            }

            ItemContainerGenerator.PrepareItemContainer(element);

            element.RenderTransform = _scaleTransform;
        }

        element.Measure(new Size(double.PositiveInfinity,
                                 double.PositiveInfinity));
    }
like image 204
Hussein Khalil Avatar asked Jan 11 '13 16:01

Hussein Khalil


2 Answers

I'm 6 years late, but the problem is still not fixed in WPF. Here is the solution (workaround).

Make a self-binding to the DataContext, eg.:

<Image DataContext="{Binding}" />

This worked for me, even for a very complex xaml.

like image 156
Ádám Bozzay Avatar answered Sep 29 '22 01:09

Ádám Bozzay


It's used whenever a container is removed from the visual tree, either because the corresponding item was deleted, or the collection was refreshed, or the container was scrolled off the screen and re-virtualized.

This is a known bug in WPF 4

See this link for known bug, it also has a workaround you may be able to apply.

"You can make your solution a little more robust by saving a reference to the sentinel object {DisconnectedItem} the first time you see it, then comparing against the saved value after that.

We should have made a public way to test for {DisconnectedItem}, but it slipped through the cracks. We'll fix that in a future release, but for now you can count on the fact that there's a unique {DisconnectedItem} object."

like image 34
Paul Zahra Avatar answered Sep 28 '22 23:09

Paul Zahra