Implementing a VirtualizingPanel part 3: MeasureCore

Now that we understand how IItemContainerGenerator works, I’m going to walk through some of the details of how your virtualizing panel’s MeasureOverride should work. I think the easiest way to see how it all works is to just look at some code. In the code below, I’ve excluded the code in MeasureOverride that implements the IScrollInfo stuff. I’m also excluding all of the layout specific code. Basically, it will have to figure out the range of items that are visible based on the scroll position.

protected override Size MeasureOverride(Size constraint)

{

    // Do work for IScrollInfo implementation

    // Figure out range that's visible based on layout algorithm

    int firstVisibleItemIndex = 0;

    int lastVisibleItemIndex = 0;

    IItemContainerGenerator generator = this.ItemContainerGenerator;

    // Get the generator position of the first visible data item

    GeneratorPosition startPos = generator.GeneratorPositionFromIndex(firstVisibleItemIndex);

    // Get index where we'd insert the child for this position. If the item is realized

    // (position.Offset == 0), it's just position.Index, otherwise we have to add one to

    // insert after the corresponding child

    childIndex = (position.Offset == 0) ? position.Index : position.Index + 1;

    using (generator.StartAt(startPos, GeneratorDirection.Forward, true))

    {

        for (int itemIndex = firstVisibleItemIndex; itemIndex <= lastVisibleItemIndex; ++itemIndex, ++childIndex)

        {

            bool newlyRealized;

            // Get or create the child

            UIElement child = generator.GenerateNext(out newlyRealized) as UIElement;

            if (newlyRealized)

            {

                // Figure out if we need to insert the child at the end or somewhere in the middle

                if (childIndex >= children.Count)

                {

                    base.AddInternalChild(child);

                }

                else

                {

                    base.InsertInternalChild(childIndex, child);

                }

                generator.PrepareItemContainer(child);

            }

            else

            {

                // The child has already been created, let's be sure it's in the right spot

                Debug.Assert(child == children[childIndex], "Wrong child was generated");

            }

            // Measurements will depend on layout algorithm

            child.Measure(new Size(itemWidth, itemHeight));

        }

    }

    // Note: this could be deferred to idle time for efficiency

    CleanUpItems(firstVisibleIndex, lastVisibleIndex);

    return desiredSize;

}

private void CleanUpItems(int firstVisibleItemIndex, int lastVisibleItemIndex)

{

    UIElementCollection children = this.InternalChildren;

    IItemContainerGenerator generator = this.ItemContainerGenerator;

    for (int i = children.Count-1; i >= 0; i--)

    {

        // Map a child index to an item index by going through a generator position

        GeneratorPosition childGeneratorPos = new GeneratorPosition(i, 0);

        int itemIndex = generator.IndexFromGeneratorPosition(childGeneratorPos);

        if (itemIndex < firstVisibleItemIndex || itemIndex > lastVisibleItemIndex)

        {

            generator.Remove(childGeneratorPos, 1);

            RemoveInternalChildRange(i, 1);

        }

    }

}

That’s it! There’s not actually that much virtualization specific code. The loop in MeasureCore will generate any missing children, and then measure children whether they are new or were already there. The cleanup code removes all UI that’s no longer visible. There are lots of opportunities for optimization here. You can defer the cleanup to idle time to keep it from slowing down layout. You can also keep extra items around in case they may come into view again. Also, it’s more efficient to remove children in batches, so the code could be improved to batch them up.

Let’s walk through a hypothetical case. Say you have a view that can display 10 items at a time. I hope these aren’t too hard to follow. It might be easier to step through the complete code I post in a future post if this is too hard to follow.

When you first display the panel, firstVisibleItemIndex will be 0 and lastVisibleItemIndex will be 9. The childIndex will be 0. It will loop through creating and measuring 10 children, inserting each one at the end. The cleanup code will walk through the children and see their item indices are in the range 0-9, so it won’t remove them.

Now, say you scroll it down a bit so items 2-11 are visible. The loop in measure core will find startPosition to be index=2, offset=0. This will be mapped to child index 2. Newly realized will be false for the first 7 items, but then it will start creating new items and adding them at the end. In cleanup items, it loops through the children backwards. The last 10 children will all be in the visible range, but then it will hit children that are out of range and remove them one at a time.

If the item collection was modified so that an item was added at index 8, this would cause a remeasure to happen. The IItemContainerGenerator is going to know about the new item, so when we iterate through the positions, it will create a new item and we’ll insert it at the right location.

But, what happens when you remove an item from the collection? We’ll get measure called again and the IItemContainerGenerator will be updated, but we’ll have this extra child that’s no longer in the generator. This is a problem! The solution to this is to override OnItemsChanged and update things immediately. I’ll include the full code for this in my next post.

Coming next is an implementation of a VirtualizingTilePanel that puts this all together.