Implementing a VirtualizingPanel part 2: IItemContainerGenerator

In part 1 of this series of posts, I gave an overview of how to write a VirtualizingPanel. One of the keys to the implementation is understanding IItemContainerGenerator. I personally found it a bit nonintuitive to begin with. IItemContainerGenerator will not only help us generate (realize) and destroy (virtualize) the items, but we'll also use it to track the mapping of child indices to item indices through the GeneratorPosition.

The easiest way to see how this works is to step through an example of using it. For this example, I created a simple Avalon application that had an ItemsControl bound to 5 integers and used a custom VirtualizingPanel subclass as the ItemsPanelTemplate for the ItemsControl. Then, I inserted some test code into MeasureOverride.

Before we can use the generator, we must access the InternalChildren property as follows. Until you do, the generator is null. This is a bug, that will hopefully be fixed before long.

UIElementCollection children = base.InternalChildren;

Next, let’s look at the state of the generator using a utility function. The function demonstrates the use of GeneratorPositionFromIndex. This function takes a item index, not a child index. So, in our test case, we always expect there to be 5 iterations through this loop, no matter how many children we have created.

private void DumpGeneratorContent()

{

    IItemContainerGenerator generator = this.ItemContainerGenerator;

    ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);

    Console.WriteLine("Generator positions:");

    for (int i = 0; i < itemsControl.Items.Count; i++)

    {

        GeneratorPosition position = generator.GeneratorPositionFromIndex(i);

        Console.WriteLine("Item index=" + i + ", Generator position: index=" + position.Index + ", offset=" + position.Offset);

    }

    Console.WriteLine();

}

The result in the initial state is as follows. There are no realized items yet – they are all virtualized.

Generator positions:

Item index=0, Generator position: index=-1, offset=1

Item index=1, Generator position: index=-1, offset=2

Item index=2, Generator position: index=-1, offset=3

Item index=3, Generator position: index=-1, offset=4

Item index=4, Generator position: index=-1, offset=5

Next, let’s realize the first item:

Console.WriteLine("Realizing the first item...");

IItemContainerGenerator generator = this.ItemContainerGenerator;

bool isNewlyRealized;

GeneratorPosition position = generator.GeneratorPositionFromIndex(0);

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

{

    DependencyObject child = generator.GenerateNext(out isNewlyRealized);

    Console.WriteLine("isNewlyRealized = " + isNewlyRealized);

    generator.PrepareItemContainer(child);

}

DumpGeneratorContent();

First, we map the item index to a GeneratorPosition. Before you can use it, you need to use the StartAt function to tell it where to start generating. StartAt is an IDisposable, so putting it inside a using is the most convenient way to set up a range of code that does the iteration. We tell it to start at our generator position and to go forward. The last parameter is allowStartAtRealizedItem and says whether StartAt will throw an exception if the item at the position has already been realized. In this case, none of the items are realized, so it doesn’t matter.

GenerateNext() is what actually creates the child. It has an output parameter which will tell you if the child is new, or if it had been previously realized. Finally, if the child is new, you must call PrepareItemContainer() to set up the UI. To be honest, I’m not sure why this is necessary. It seems like GenerateNext could do this for you.

The results are:

Realizing the first item...

isNewlyRealized = True

Generator positions:

Item index=0, Generator position: index=0, offset=0

Item index=1, Generator position: index=0, offset=1

Item index=2, Generator position: index=0, offset=2

Item index=3, Generator position: index=0, offset=3

Item index=4, Generator position: index=0, offset=4

Note that the one we just realized has index 0 and offset 0, the items that are still virtualized have non-zero offsets.

If you were to repeat this same code, you’d get isNewlyRealized = true. If you had passed false in as the final parameter to StartAt, it would throw an exception when you called GenerateNext because is has been previously realized.

If you were to then realize the 3rd item using the following code. Remember that GeneratorPositionFromIndex is an item index, so index=2 is the 3rd item.

position = generator.GeneratorPositionFromIndex(2);

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

{

    DependencyObject child = generator.GenerateNext(out isNewlyRealized);

    Console.WriteLine("isNewlyRealized = " + isNewlyRealized);

    generator.PrepareItemContainer(child);

}

DumpGeneratorContent();

The results are as follows:

Realizing the third item...

isNewlyRealized = True

Generator positions:

Item index=0, Generator position: index=0, offset=0

Item index=1, Generator position: index=0, offset=1

Item index=2, Generator position: index=1, offset=0

Item index=3, Generator position: index=1, offset=1

Item index=4, Generator position: index=1, offset=2

You can see that the GeneratorPosition for item 2 now has index=1 and offset=0. Conveniently, if we keep the children sorted by item index, the generator positions correspond to the child indices.

You can revirtualize an item with code like the following:

Console.WriteLine("Virtualizng the first item...");

position = generator.GeneratorPositionFromIndex(0);

generator.Remove(position, 1);

DumpGeneratorContent();

We map the item index to a GeneratorPosition and then tell it to remove 1 item starting at that position. If you tell it to remove multiple items, they must be contiguous realized items that you remove. The results are as follows:

Virtualizng the first item...

Generator positions:

Item index=0, Generator position: index=-1, offset=1

Item index=1, Generator position: index=-1, offset=2

Item index=2, Generator position: index=0, offset=0

Item index=3, Generator position: index=0, offset=1

Item index=4, Generator position: index=0, offset=2

The GeneratorPosition for item index 2 has now shifted down to 1.

In summary:

· Realized items will have a GeneratorPosition of (index = child index, offset = 0)

· Virtualized items will have a GeneratorPosition with a non-zero offset

In the next post I’ll walk through the implementation of a VirtualizingPanel’s MeasureOverride to realize and virtualize the child UI. I’ll also talk about what happens when the item collection changes.