Implementing a VirtualizingPanel part 4: the goods!

Ok, we finally get to a full implementation with this post. I’ll be showing the implementation of a VirtualizingTilePanel. This is a layout is very similar to the one I used for the layout animation sample. For the sample, I’ve created a small test harness that lets you play with it. Here’s a screenshot of what it looks like:

 

This harness lets you insert and delete items so you can make sure that works and step through the code to see how it works.

When implementing a panel like this, you have a couple of options on the scrolling behavior. You can scroll by pixels, or scroll by items. VirtualizingStackPanel (the default for ListBox) scrolls by item so the top of the view will always be at top of an item. The views in Max do pixel based scrolling. One advantage to item based scrolling is that you can support variable sized items without having to do really complex approximations to handle scroll positions. Pixel based scrolling is only easy if you have fixed sized children. For this panel, I’m using pixel based scrolling. If there is interest, I can write about how item based scrolling will work. The panel chooses the children per row based on the width and grows vertically.

I’m not going to write much on the test harness implementation, unless there are questions. Basically it just creates an ObservableCollection<string> and sets it as the ItemSource for an ItemsControl that uses VirtualizingTilePanel. It uses the ObservableCollection APIs to insert and remove items. When creating new items, it uses increasing numbers so you can tell where the new ones are.

I divided the VirtualizingTilePanel implementation into 3 parts:

First is the IScrollInfo implementation. I basically just grabbed the code from Ben’s latest posts on IScrollInfo. He hasn’t done his last post yet, so it doesn’t preserve the position properly on a resize, and it doesn’t implement MakeVisible. One addition I had to make was to call InvalidateMeasure when scrolling to force new items to be realized. I only support vertical scrolling.

Second is the layout specific information. I tried to encapsulate this so that if you are doing a panel that’s similar enough, you can just replace this logic with yours. Creating a base class with the common logic and abstract classes for the layout stuff would also be a natural next step.

Finally, there’s the VirtualizingPanel specific stuff. I went over the MeasureCore and CleanUpItems logic in the previous post, but there are a couple of new pieces here. Here’s the ArrangeOverride implementation:

protected override Size ArrangeOverride(Size finalSize)

{

    IItemContainerGenerator generator = this.ItemContainerGenerator;

    UpdateScrollInfo(finalSize);

    for (int i = 0; i < this.Children.Count; i++)

    {

        UIElement child = this.Children[i];

        // Map the child offset to an item offset

        int itemIndex = generator.IndexFromGeneratorPosition(new GeneratorPosition(i, 0));

  ArrangeChild(itemIndex, child, finalSize);

    }

    return finalSize;

}

The only interesting thing here is the line that converts a child index to an item index. We need this to figure out where to put a child.

And, there’s the code that deals with items being removed:

protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args)

{

    switch (args.Action)

    {

        case NotifyCollectionChangedAction.Remove:

        case NotifyCollectionChangedAction.Replace:

        case NotifyCollectionChangedAction.Move:

            RemoveInternalChildRange(args.Position.Index, args.ItemUICount);

            break;

    }

}

This removes realized children when the corresponding data items are removed. I have to be honest that I’m not sure this implementation works in all cases. ObservableCollection<> only supports removing single items, so I haven’t tested he more complex cases that can happen.

The code can be found at https://www.boingo.org/samples/VirtualizingTilePanelSample.zip. It works with the December/January CTP.

I’m hoping to hear some good feedback on this! Let me know what you think and if you have questions.