views:

146

answers:

3

There are not very many options for a virtualizing wrap panel for use in WPF. For one reason or another MS decided to not ship one in the standard library.

If anyone could be so bold as to provide a crowd source answer (and explaination) to the first work item on the following codeplex project, I would greatly appreciate it:

http://virtualwrappanel.codeplex.com/workitem/1

Thanks!


Summary of issue:

I've recently tried using the virtualizing wrappanel from this project and have encountered a bug.

Steps to reproduce:

  1. Create listbox.
  2. Set the virtualizing wrappanel as the itemhost in a listboxpanel template.
  3. Bind the itemsource of the listbox to an observable collection.
  4. Remove an item from the backing observable collection.

The Debug.Assert fails (Debug.Assert(child == _children[childIndex], "Wrong child was generated");) in MeasureOverride, and continued execution results in a null exception in the Cleanup method [see attached screenshot].

Please let me know if you are able to correct this.

Thanks,

AO


Code:

http://virtualwrappanel.codeplex.com/SourceControl/list/changesets#

alt text

A: 

First, beware that in general, if you are removing an object from a collection and you don't have it's reference, that object is dead at the point of removal. So at the very least RemoveInternalChildRange call is illegal after removal but that's not the core issue.

Second, you might be having a little race condition, even if it's not strictly multi-threaded. Have to check (with breakpoint) if that event handler is reacting too eagerly - you don't want event handler running while you are still in the middle of a removal even if it's a single item.

Third, check for null after:

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

and for the first trial change the code to have a graceful exit, which in this case means gracefull continue - have to use for loop and increments in the loop to be able to do continue at all.

Also check InternalChildren whne you see that null to see if that access path gives the same result as your _children (as in size, internal data, null in the same place).

If just skipping a null survives (renders without exceptions) stop it in debugger right after that and check if these arrays/collections got settled (no nulls inside).

Also, post the fully compilable sample project that gives the repro (as a zip file) somewhere - reduces random assumprions and allows ppl to just build/run and see.

Speaking of assumptions - check what's your "observable collection" doing. If you are removing an item from a collection, any and every iterator/enumerator from a prior state of that collection has the right to throw or give nulls and in a UI that tries to be too smart, having a stale iterator can happen easily.

ZXX
+2  A: 

The OnItemsChanged method needs to properly handle the args parameters. Please see this question for more information. Copying the code from that question, you would need to update OnItemsChanged like so:

protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args) {
    base.OnItemsChanged(sender, args);
    _abstractPanel = null;
    ResetScrollInfo();

    // ...ADD THIS...
    switch (args.Action) {
        case NotifyCollectionChangedAction.Remove:
        case NotifyCollectionChangedAction.Replace:
            RemoveInternalChildRange(args.Position.Index, args.ItemUICount);
            break;
        case NotifyCollectionChangedAction.Move:
            RemoveInternalChildRange(args.OldPosition.Index, args.ItemUICount);
            break;
    }
}
Tom Goff
+2  A: 

Explanation of the problem

You asked for an explanation of what is going wrong as well as instructions how to fix it. So far nobody has explained the problem. I will do so.

In ListBox with a VirtualizingWrapPanel there are five separate data structures that track items, each in different ways:

  1. ItemsSource: The original collection (in this case ObservableCollection)
  2. CollectionView: Keeps a separate list of sorted/filtered/grouped items (only if any of these features are in use)
  3. ItemContainerGenerator: Tracks the mapping between items and containers
  4. InternalChildren: Tracks containers that are currently visible
  5. WrapPanelAbstraction: Tracks which containers appear on which line

When an item is removed from ItemsSource, this removal must be propagated through all data structures. Here is how it works:

  1. You call Remove() on the ItemsSource
  2. ItemsSource removes the item and fires its CollectionChanged which is handled by the CollectionView
  3. CollectionView removes the item (if sorting/filtering/grouping is in use) and fires its CollectionChanged which is handled by the ItemContainerGenerator
  4. ItemContainerGenerator updates its mapping, fires its ItemsChanged which is handled by VirtualizingPanel
  5. VirtualizingPanel calls its virtual OnItemsChanged method which is implemented by VirtualizingWrapPanel
  6. VirtualizingWrapPanel discards its WrapPanelAbstraction so it will be built, but it never updates InternalChildren

Because of this, the InternalChildren collection is out of sync with the other four collections, leading to the errors that were experienced.

Solution to the problem

To fix the problem, add the following code anywhere within VirtualizingWrapPanel's OnItemsChanged method:

switch(args.Action)
{ 
    case NotifyCollectionChangedAction.Remove: 
    case NotifyCollectionChangedAction.Replace: 
        RemoveInternalChildRange(args.Position.Index, args.ItemUICount); 
        break; 
    case NotifyCollectionChangedAction.Move: 
        RemoveInternalChildRange(args.OldPosition.Index, args.ItemUICount); 
        break; 
} 

This keeps the InternalChildren collection in sync with the other data structures.

Why AddInternalChild/InsertInternalChild is not called here

You may wonder why there are no calls to InsertInternalChild or AddInternalChild in the above code, and especially why handling Replace and Move don't require us to add a new item during OnItemsChanged.

The key to understanding this is in the way ItemContainerGenerator works.

When ItemContainerGenerator receives a remove event it handles everything immediately:

  1. ItemContainerGenerator immediately removes the item from its own data structures
  2. ItemContainerGenerator fires the ItemChanged event. The panel is expected to immediately remove the container.
  3. ItemContainerGenerator "unprepares" the container by removing its DataContext

On the other hand, ItemContainerGenerator learns that an item is added everything is typically deferred:

  1. ItemContainerGenerator immediately adds a "slot" for the item in its data structure but does not create a container
  2. ItemContainerGenerator fires the ItemChanged event. The panel calls InvalidateMeasure() [this is done by the base class - you do not have to do it]
  3. Later when MeasureOverride is called, Generator.StartAt/MoveNext is used to generate the item containers. Any newly-generated containers are added to InternalChildren at that time.

Thus, all removals from the InternalChildren collection (including ones that are part of a Move or Replace) must be done inside OnItemsChanged, but additions can (and should) be deferred until the next MeasureOverride.

Ray Burns
Looks like Tom Goff gave the necessary code while I was typing my answer. His answer is correct as well, and is essentially the same as mine without the detailed explanation.
Ray Burns
Hi Ray - Nice summary, you got my vote. One problem with your answer is that the issue isn't really that the "InternalChildren collection is out of sync with the other four collections", but I'm sure it doesn't help.The underlying issue is that realized children are not "cleaned up". If you remove the item at index 10, then the item at index 11 will be moved to index 10. When you go to realize the item at index 10 (which was previously at 11), you will end up with the assertion "Wrong child was generated", since the other child was never unrealized.
Tom Goff