Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 60 additions & 29 deletions src/Controls/src/Core/Handlers/Items/ItemsViewHandler.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -130,45 +130,76 @@ void OnItemsVectorChanged(global::Windows.Foundation.Collections.IObservableVect
// Use the nullable IViewHandler.VirtualView (not the typed property) for the null
// check. The typed VirtualView throws InvalidOperationException if base.VirtualView
// is null, which can happen if the handler was disconnected before this event fires.
if (((IViewHandler)this).VirtualView is null)
if (((IViewHandler)this).VirtualView is null || ListViewBase is null)
return;

if (sender is not ItemCollection items)
var mode = VirtualView.ItemsUpdatingScrollMode;

if (mode != ItemsUpdatingScrollMode.KeepItemsInView && mode != ItemsUpdatingScrollMode.KeepLastItemInView)
{
return;
}

ListViewBase.DispatcherQueue.TryEnqueue(() =>
// Reset notifications are unsafe to process because WinUI may still be rebuilding
// the internal projection. Accessing indexes during Reset can trigger
// E_CHANGED_STATE / COMException.
if (@event?.CollectionChange == global::Windows.Foundation.Collections.CollectionChange.Reset)
{
// The lambda is dispatched asynchronously. By the time it runs, the handler may
// have been disconnected (e.g. by element.DisconnectHandlers() in
// CleanUpCollectionViewSource), setting base.VirtualView to null. The typed
// VirtualView property throws in that case, so use the nullable interface accessor
// here to safely bail out instead of crashing (issue #9075).
if (((IViewHandler)this).VirtualView is null || ListViewBase is null)
{
return;
}
return;
}

var itemsCount = items.Count;
// Grouped CollectionViews are backed by a flattened projection that may still be
// mutating while VectorChanged is firing. Accessing indexes synchronously can
// trigger STATUS_STOWED_EXCEPTION / E_CHANGED_STATE.
//
// Non-grouped views (e.g. CarouselView) should remain synchronous to avoid
// regressions where deferred scrolling changes the intended position.
bool shouldDefer = CollectionViewSource?.IsSourceGrouped == true;

if (itemsCount == 0)
{
if (shouldDefer)
{
var dispatcherQueue = PlatformView?.DispatcherQueue;

if (dispatcherQueue is null)
return;
}

if (VirtualView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepItemsInView)
{
var firstItem = items[0];
// Keeps the first item in the list displayed when new items are added.
ListViewBase.ScrollIntoView(firstItem);
}
dispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal, ScrollIntoViewIfNeeded);
}
else
{
ScrollIntoViewIfNeeded();
}
}

if (VirtualView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepLastItemInView)
{
var lastItem = items[itemsCount - 1];
// Adjusts the scroll offset to keep the last item in the list displayed when new items are added.
ListViewBase.ScrollIntoView(lastItem, ScrollIntoViewAlignment.Leading);
}
});
void ScrollIntoViewIfNeeded()
{
// Handler may have been disconnected before deferred execution runs
if (((IViewHandler)this).VirtualView is null || ListViewBase is null)
return;

var view = CollectionViewSource?.View;
var itemsCount = view?.Count ?? 0;

if (itemsCount == 0)
{
return;
}

// Re-read the mode here (rather than capturing it from the caller) so the
// deferred path picks up the latest value if it changed between enqueue and
// drain, and so this helper has no implicit dependency on caller state.
var mode = VirtualView.ItemsUpdatingScrollMode;

if (mode == ItemsUpdatingScrollMode.KeepItemsInView)
{
// Keeps the first item visible when items are inserted
ListViewBase.ScrollIntoView(view[0]);
}
else if (mode == ItemsUpdatingScrollMode.KeepLastItemInView)
{
// Keeps the last item visible when items are appended
ListViewBase.ScrollIntoView(view[itemsCount - 1], ScrollIntoViewAlignment.Leading);
}
}

protected abstract ListViewBase SelectListViewBase();
Expand Down
Loading