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
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.