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 http://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.


 


 

Comments (11)

  1. Manny says:

    Thank you for posting the series on VirtualizingPanel.  

    How would I get the VirtualizingTilePanel object from _itemsControl if I want to do something like changing the ChildSize property of the panel?  

  2. Manny says:

    If I wire up the MouseDown event on the Border element in the DataTemplate, how would I identify which

    item it represents in the ObservableCollection.  I am looking to see if I could determine the Border’s index in the panel.

    Thanks,

    Manny

  3. dancre says:

    On the child size question, I have to admit that I tried to get some binding working, but failed. You should be able to get to it through the ItemsPanel property, although you might have to dig into the visual tree. Any suggestions from others would be great.

    For the mouse down, you could look at the DataContext to see which item it represnts. Another option is to handle this at the itemscontrol level. You can use a ListBox if you need all of that functionality, or just Selector if you want selection handled for you.

  4. Manny says:

    I tried the following in the Window Loaded event implementation:

    FrameworkElementFactory frameworkElementFactory= itemsPanelTemplate.VisualTree;



    binding.ElementName = "_slider";

    binding.Path = new PropertyPath((object)Slider.ValueProperty);

               

    //this causes the following exception:

    // "InvalidOperationException"

    //"After a ‘FrameworkElementFactory’ is in use (sealed), it cannot be modified."

    frameworkElementFactory.SetBinding(VirtualizingTilePanel.ChildSizeProperty, binding);

    //this causes the same exception:

    // "InvalidOperationException"

    //"After a ‘FrameworkElementFactory’ is in use (sealed), it cannot be modified."

    frameworkElementFactory.SetValue(VirtualizingTilePanel.ChildSizeProperty, 50);

    Any ideas on how to get around this?

  5. dancre says:

    I think there may be an bug preventing this binding from working. Keep an eye out for a newer build…

  6. mlaroche says:

    Hi,

       First, thanks for those enligthening posts…

       I’d like to challenge you with 1 or 2 questions:

       First Question: How would you deal with child UIElements of different size (in your example, everybody has the same size)?

       Let’s take an example where a text control wraps long text around, making the control taller…

       Wouldn’t it be troublesome since you wouldn’t be able to calculate the size of the panel efficiently. Wouldn’t it also make the scrolling gestion a bit troublesome?

       Second Question: Take the example of a ‘virtualizing’ treeview,  how would you handle the Expansion/Collapsing of the nodes?

      If you could help me out a little on those topics, I would appreciate greatly…

  7. dancre says:

    First question: Yes, this is difficult if you support pixel-based scrolling for the reasons you mention. It might be doable if you could calculate the heights based on the data, but if you need to do something like wrap text, thata wouldn’t be possible. Another possibility is to use approximate values. And, another solution (what VirtualizingStackPanel does) is scroll based by element, not by pixel. That way, you just need to know how many elements there are.

    Second question: I think you could do this if you kept track of the expanded/collapsed state outside of the UI. I haven’t done much with the tree view stuff though. so I don’t know the data structures involved very well. If you can provide a bit more info on what you have in mind, I could try to be more help.

  8. mlaroche says:

    Regarding the first item:

    Well, I had already identified that Item Scrolling would solve all the scrolling issues… as I said, my primary goal was to challenge your design a little… Based on that, after giving some toughts into it over the weekend… We could emulate the "nice looking" per pixel scrolling by using proportional scrolling…  By proportional, I mean to give a fixed value to each item and to scroll by proportions of an item…

    If we give a proportion of 100 (100%) for each item, then independently of the item’s size, we could scroll trough the item and have a "pixel" scrolling feel… Only noticeable side effect would be the "scrolling acceleration/decelerations" feeling  for much "larger or smaller" items when scrolling…  For almost similar item size, I don’t think it would be noticeable at all.

    Since this solution is based on the number of items and not the particularities of items themselves, it would be pretty doable…

    Regarding the second point:

      I provided the tree view example because it has a well known behavior (expansion/collapsing)… In fact, I’m looking into virtualization to create custom data controls which items could have child items.. I’m not directly interested in the tree view itself.

      In any way, I suppose that if the "child" items are nested under the "items" of the virtualizing control, the measure and arrange of the control will ensure that Expansion/Collapsing of the items is done correctly.

      Or alternativelly, I suppose using HierarchicalDataTemplates would do the job,  except for the collapsing/expansion.

     

  9. Control Licensing in Cider (WPF designer for VS)
    James provides some
    great
    information on supporting…

  10. Ask Dr. WPF says:

    Let’s continue our exploration of WPF through the medium of the ItemsControl class. I know I promised to write ‘G’ is for Generator next, but after giving it more consideration, I’ve decided t …