18

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));
    }
HaemEternal
  • 2,229
  • 6
  • 31
  • 50
Hussein Khalil
  • 1,585
  • 2
  • 25
  • 47
  • Are you recycling container? – paparazzo Jan 11 '13 at 17:56
  • @Blam: I don't think I am, what do you mean by recycling the container ? – Hussein Khalil Jan 11 '13 at 18:05
  • Just search msdn for recycle container http://msdn.microsoft.com/en-us/library/system.windows.controls.virtualizationmode.aspx Just a reach but also just a comment – paparazzo Jan 11 '13 at 21:18
  • @Blam: Thanks, I tried using a VirtualizingStackPanel and turning container Recycling on. I unfortunately still have the same problem, i.e.: DisconnectedItems are being generated when items are virtualized. – Hussein Khalil Jan 11 '13 at 21:47

2 Answers2

11

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.

Ádám Bozzay
  • 529
  • 7
  • 19
  • You beauty! Fixed my problem related to triggers and switching templates. – david.pfx Sep 08 '19 at 05:15
  • 3
    This didn't work in my case, but led me to another solution: I set the element's `Tag` to the binding, as a backup, so I can still access the real item even when the data context is disconnected. E.g. in the ListBoxItem's style I have this: `` – Paul Dec 04 '20 at 01:50
  • No you're not late at all!! A few years early, impressive. My issue was invoking `NotifyCollectionChanged` for Move operations used in sorting which I don't think work out of the box but my workaround is manually doing a remove/insert and was a getting `{{DisconnectedItem}}` in the re-insertion – tkefauver Mar 21 '21 at 21:32
  • The link is dead unfortunately – Nik Jun 01 '22 at 22:47
  • It's available on the [web archive](https://web.archive.org/web/20180317025036/https://www.programering.com/a/MDO5ADMwATQ.html). But the main takeaway is in my answer. – Ádám Bozzay Jun 07 '22 at 08:09
8

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

StayOnTarget
  • 11,743
  • 10
  • 52
  • 81
Paul Zahra
  • 9,522
  • 8
  • 54
  • 76