Question

I created SelectionHelper for DataGrid which allows binding for selected items in both directions.
In this helper I call ScrollIntoView for the first selected item, when selection changes from viewmodel. The call successfully returns. But later somewhere in UI's message queue something happens and IndexOf for my collection is called.
I suspect it to be async because of the UI virtualization. DataGrid is definitely whants to know item's index. But what I cannot understand why it puts ItemsControl.ItemInfo instead of item.
Is this a bug or undocumented feature?

My collection implements these interfaces: IList<T>, IList, INotifyCollectionChanged

and here is the code of IndexOf:

public int IndexOf(object value)
        {
            if ((value != null && !(value is T))
                || (value == null && typeof(T).IsValueType))
                throw new ArgumentException(WrongTypeMessage, "value");

            return IndexOf((T)value);
        }

And it throws exception as expected =)

Update

Yes, my guess was right. Here's the code for DataGrid's ScrollIntoView

public void ScrollIntoView(object item)
    {
      if (item == null)
        throw new ArgumentNullException("item");
      this.ScrollIntoView(this.NewItemInfo(item, (DependencyObject) null, -1));
    }

    internal void ScrollIntoView(ItemsControl.ItemInfo info)
    {
      if (this.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
        this.OnBringItemIntoView(info);
      else
        this.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, (Delegate) new DispatcherOperationCallback(((ItemsControl) this).OnBringItemIntoView), (object) info);
    }

Update Problem is fixed in this update

Was it helpful?

Solution

Yep, probably I've found the reason:

Here's the code of DataGrid.ScrollIntoView (decompiled by Resharper)

internal void ScrollIntoView(ItemsControl.ItemInfo info)
    {
     if (this.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
        this.OnBringItemIntoView(info);
     else
        this.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, (Delegate) new DispatcherOperationCallback(((ItemsControl) this).OnBringItemIntoView), (object) info);
    }

Here they cast info to the object type. Whell, the problem is really that DispatcherOperationCallback expects object.
But ItemsControl has two overloads for OnBringItemIntoView: one for ItemsControl.ItemInfo and the second for object type.

internal object OnBringItemIntoView(ItemsControl.ItemInfo info)
    {
      FrameworkElement frameworkElement = info.Container as FrameworkElement;
      if (frameworkElement != null)
        frameworkElement.BringIntoView();
      else if ((info = this.LeaseItemInfo(info, true)).Index >= 0)
      {
        VirtualizingPanel virtualizingPanel = this.ItemsHost as VirtualizingPanel;
        if (virtualizingPanel != null)
          virtualizingPanel.BringIndexIntoView(info.Index);
      }
      return (object) null;
    }

internal object OnBringItemIntoView(object arg)
    {
     return this.OnBringItemIntoView(this.NewItemInfo(arg, (DependencyObject) null, -1));
    }

Guess, which one is selected? ;-)
So they get ItemInfo wrapped in ItemInfo. That's why this.LeaseItemInfo(info, true) iside

internal object OnBringItemIntoView(ItemsControl.ItemInfo info)
    {
      FrameworkElement frameworkElement = info.Container as FrameworkElement;
      if (frameworkElement != null)
        frameworkElement.BringIntoView();
      else if ((info = this.LeaseItemInfo(info, true)).Index >= 0)
      {
        VirtualizingPanel virtualizingPanel = this.ItemsHost as VirtualizingPanel;
        if (virtualizingPanel != null)
          virtualizingPanel.BringIndexIntoView(info.Index);
      }
      return (object) null;
    }

recieves incorrect item and calls IndexOf with wrong value:

 internal ItemsControl.ItemInfo LeaseItemInfo(ItemsControl.ItemInfo info, bool ensureIndex = false)
    {
      if (info.Index < 0)
      {
        info = this.NewItemInfo(info.Item, (DependencyObject) null, -1);
        if (ensureIndex && info.Index < 0)
          info.Index = this.Items.IndexOf(info.Item);
      }
      return info;
    }

But this should break ScrollIntoView for all the cases when containers were not generated. The simple workaround is to call ScrollIntoView via Dispatcher.BeginInvoke

I'll wait for the answer of MS.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top