From 8f2e320cf55175ddc99fcf36de8b6d489ded9fc0 Mon Sep 17 00:00:00 2001 From: Karthik Raja Date: Tue, 23 Dec 2025 18:35:27 +0530 Subject: [PATCH 001/109] WinUI CV2 implementation using ItemsRepeater --- .../Items2/ItemsViewHandler2.Windows.cs | 908 ++++++++++++++++++ .../Windows/CollectionViewHandler2.Windows.cs | 269 ++++++ .../Windows/GroupedItemTemplateCollection2.cs | 261 +++++ .../Handlers/Items2/Windows/ItemFactory.cs | 122 +++ .../Items2/Windows/ItemTemplateContext2.cs | 40 + .../Windows/ItemTemplateContextEnumerable2.cs | 41 + .../Windows/ItemTemplateContextList2.cs | 106 ++ .../Handlers/Items2/Windows/MauiItemsView.cs | 244 +++++ .../ObservableItemTemplateCollection2.cs | 288 ++++++ .../Windows/TemplatedItemSourceFactory2.cs | 28 + .../CollectionView/ItemsViewStyles.xaml | 45 +- .../net-windows/PublicAPI.Unshipped.txt | 38 + 12 files changed, 2387 insertions(+), 3 deletions(-) create mode 100644 src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs create mode 100644 src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs create mode 100644 src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs create mode 100644 src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs create mode 100644 src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs create mode 100644 src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextEnumerable2.cs create mode 100644 src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextList2.cs create mode 100644 src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs create mode 100644 src/Controls/src/Core/Handlers/Items2/Windows/ObservableItemTemplateCollection2.cs create mode 100644 src/Controls/src/Core/Handlers/Items2/Windows/TemplatedItemSourceFactory2.cs diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs new file mode 100644 index 000000000000..e772311abd89 --- /dev/null +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -0,0 +1,908 @@ +using System; +using System.Collections; +using System.Collections.Specialized; +using System.ComponentModel; +using Microsoft.Maui.Controls.Internals; +using Microsoft.Maui.Controls.Platform; +using Microsoft.Maui.Dispatching; +using Microsoft.Maui.Graphics; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using WASDKScrollBarVisibility = Microsoft.UI.Xaml.Controls.ScrollBarVisibility; +using WItemsView = Microsoft.UI.Xaml.Controls.ItemsView; +using WScrollPresenter = Microsoft.UI.Xaml.Controls.Primitives.ScrollPresenter; +using WVisibility = Microsoft.UI.Xaml.Visibility; + +namespace Microsoft.Maui.Controls.Handlers.Items2 +{ + public abstract class ItemsViewHandler2 : ViewHandler where TItemsView : ItemsView + { + CollectionViewSource? _collectionViewSource; + IList? _itemsSource; + + FrameworkElement? _emptyView; + View? _mauiEmptyView; + bool _emptyViewDisplayed; + double _previousHorizontalOffset; + double _previousVerticalOffset; + + FrameworkElement? _footer; + View? _mauiFooter; + bool _footerDisplayed; + + FrameworkElement? _header; + View? _mauiHeader; + bool _headerDisplayed; + + WASDKScrollBarVisibility? _defaultHorizontalScrollVisibility; + WASDKScrollBarVisibility? _defaultVerticalScrollVisibility; + + WeakNotifyPropertyChangedProxy? _layoutPropertyChangedProxy; + PropertyChangedEventHandler? _layoutPropertyChanged; + + protected TItemsView ItemsView => VirtualView; + protected TItemsView Element => VirtualView; + + protected abstract IItemsLayout Layout { get; } + + public ItemsViewHandler2() : base(ItemsViewMapper) + { + + } + + public ItemsViewHandler2(PropertyMapper? mapper = null) : base(mapper ?? ItemsViewMapper) + { + + } + + public static PropertyMapper> ItemsViewMapper = new(ViewMapper) + { + [Controls.ItemsView.ItemsSourceProperty.PropertyName] = MapItemsSource, + [Controls.ItemsView.HorizontalScrollBarVisibilityProperty.PropertyName] = MapHorizontalScrollBarVisibility, + [Controls.ItemsView.VerticalScrollBarVisibilityProperty.PropertyName] = MapVerticalScrollBarVisibility, + [Controls.ItemsView.ItemTemplateProperty.PropertyName] = MapItemTemplate, + [Controls.ItemsView.EmptyViewProperty.PropertyName] = MapEmptyView, + [Controls.ItemsView.EmptyViewTemplateProperty.PropertyName] = MapEmptyViewTemplate, + [Controls.ItemsView.FlowDirectionProperty.PropertyName] = MapFlowDirection, + [Controls.ItemsView.IsVisibleProperty.PropertyName] = MapIsVisible, + [Controls.ItemsView.ItemsUpdatingScrollModeProperty.PropertyName] = MapItemsUpdatingScrollMode, + [Controls.StructuredItemsView.ItemsLayoutProperty.PropertyName] = MapItemsLayout, + [Controls.StructuredItemsView.HeaderProperty.PropertyName] = MapHeader, + [Controls.StructuredItemsView.HeaderTemplateProperty.PropertyName] = MapHeaderTemplate, + [Controls.StructuredItemsView.FooterProperty.PropertyName] = MapFooter, + [Controls.StructuredItemsView.FooterTemplateProperty.PropertyName] = MapFooterTemplate, + }; + + public static void MapItemsSource(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.UpdateItemsSource(); + } + + public static void MapHorizontalScrollBarVisibility(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.UpdateHorizontalScrollBarVisibility(); + } + + public static void MapVerticalScrollBarVisibility(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.UpdateVerticalScrollBarVisibility(); + } + + public static void MapItemTemplate(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.UpdateItemsSource(); + } + + public static void MapEmptyView(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.UpdateEmptyView(); + } + + public static void MapEmptyViewTemplate(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.UpdateEmptyView(); + } + + public static void MapFlowDirection(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.PlatformView.UpdateFlowDirection(itemsView); + } + + public static void MapIsVisible(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.PlatformView.UpdateVisibility(itemsView); + } + + public static void MapItemsUpdatingScrollMode(ItemsViewHandler2 handler, ItemsView itemsView) + { + } + + public static void MapItemsLayout(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.UpdateItemsLayout(); + } + + public static void MapHeader(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.UpdateHeader(); + } + + public static void MapHeaderTemplate(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.UpdateHeader(); + } + + public static void MapFooter(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.UpdateFooter(); + } + + public static void MapFooterTemplate(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.UpdateFooter(); + } + + protected override WItemsView CreatePlatformView() + { + var itemsView = SelectListViewBase(); + + return itemsView; + } + + protected override void ConnectHandler(WItemsView platformView) + { + base.ConnectHandler(platformView); + + if (Layout is not null) + { + _layoutPropertyChanged ??= LayoutPropertyChanged; + _layoutPropertyChangedProxy = new WeakNotifyPropertyChangedProxy(Layout, _layoutPropertyChanged); + } + else + { + _layoutPropertyChangedProxy?.Unsubscribe(); + _layoutPropertyChangedProxy = null; + } + + VirtualView.ScrollToRequested += ScrollToRequested; + FindScrollViewer(); + } + + + protected override void DisconnectHandler(WItemsView platformView) + { + base.DisconnectHandler(platformView); + + _layoutPropertyChangedProxy?.Unsubscribe(); + _layoutPropertyChangedProxy = null; + + VirtualView.ScrollToRequested -= ScrollToRequested; + } + + CollectionViewSource CreateCollectionViewSource() + { + var itemsSource = Element.ItemsSource; + var itemTemplate = Element.ItemTemplate; + + if (itemTemplate is not null) + { + if (ItemsView is GroupableItemsView groupableItemsView && groupableItemsView.IsGrouped) + { + return new CollectionViewSource + { + Source = TemplatedItemSourceFactory2.CreateGrouped(itemsSource, itemTemplate, + groupableItemsView.GroupHeaderTemplate, groupableItemsView.GroupFooterTemplate, + Element, mauiContext: MauiContext) + }; + } + else + { + return new CollectionViewSource + { + Source = TemplatedItemSourceFactory2.Create(itemsSource, itemTemplate, Element, mauiContext: MauiContext), + IsSourceGrouped = false + }; + } + } + + return new CollectionViewSource + { + Source = itemsSource, + IsSourceGrouped = false + }; + } + + protected virtual void UpdateItemsSource() + { + if (PlatformView is null) + { + return; + } + + CleanUpCollectionViewSource(); + + + + _collectionViewSource = CreateCollectionViewSource(); + _itemsSource = _collectionViewSource?.Source as IList; + + if (_collectionViewSource?.Source is INotifyCollectionChanged incc) + { + incc.CollectionChanged += ItemsChanged; + } + + PlatformView.ItemsSource = _collectionViewSource?.View; + + if (VirtualView.ItemTemplate is not null) + { + PlatformView.ItemTemplate = new ItemFactory(Element); + } + else if (PlatformView.ItemTemplate is not null) + { + PlatformView.ItemTemplate = null; + } + + UpdateEmptyViewVisibility(); + } + + void CleanUpCollectionViewSource() + { + if (_collectionViewSource is not null) + { + if (_collectionViewSource.Source is ObservableItemTemplateCollection2 observableItemTemplateCollection) + { + observableItemTemplateCollection.CleanUp(); + } + + if (_collectionViewSource.Source is INotifyCollectionChanged incc) + { + incc.CollectionChanged -= ItemsChanged; + } + + _collectionViewSource.Source = null; + _collectionViewSource = null; + } + + // Remove all children inside the ItemsSource + if (VirtualView is not null) + { + foreach (var item in PlatformView.GetChildren()) + { + if (item is not null) + { + var element = item.GetVisualElement(); + VirtualView.RemoveLogicalChild(element); + } + } + } + + if (VirtualView?.ItemsSource is null) + { + PlatformView.ItemsSource = null; + } + } + + void ItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + UpdateEmptyViewVisibility(); + + VirtualView.Dispatcher.DispatchAsync(() => ApplyItemsUpdatingScrollMode()); + } + + void ApplyItemsUpdatingScrollMode() + { + if (_itemsSource is null || _itemsSource.Count == 0 || _collectionViewSource?.View?.Count <= 1) + { + return; + } + + if (VirtualView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepItemsInView) + { + // Keeps the first item in the list displayed when new items are added. + PlatformView.StartBringItemIntoView(0, new BringIntoViewOptions() { AnimationDesired = false }); + } + else if (VirtualView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepLastItemInView) + { + // Adjusts the scroll offset to keep the last item in the list displayed when new items are added. + PlatformView.StartBringItemIntoView(_itemsSource.Count - 1, new BringIntoViewOptions() { AnimationDesired = false }); + } + } + + MauiItemsView SelectListViewBase() + { + var itemsView = new MauiItemsView() + { + Layout = CreateItemsLayout() + }; + + if (Layout is LinearItemsLayout listItemsLayout && + listItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal) + { + ScrollViewer.SetHorizontalScrollMode(itemsView, UI.Xaml.Controls.ScrollMode.Enabled); + ScrollViewer.SetHorizontalScrollBarVisibility(itemsView, WASDKScrollBarVisibility.Visible); + + ScrollViewer.SetVerticalScrollMode(itemsView, UI.Xaml.Controls.ScrollMode.Disabled); + ScrollViewer.SetVerticalScrollBarVisibility(itemsView, WASDKScrollBarVisibility.Disabled); + + // Set header/footer to horizontal layout + itemsView.SetLayoutOrientation(true); + } + else + { + // Set header/footer to vertical layout + itemsView.SetLayoutOrientation(false); + } + return itemsView; + } + + protected void UpdateItemsLayout() + { + FindScrollViewer(); + + _defaultHorizontalScrollVisibility = null; + _defaultVerticalScrollVisibility = null; + + UpdateItemsSource(); + + PlatformView.Layout = CreateItemsLayout(); + + // Update header/footer orientation + if (PlatformView is MauiItemsView mauiItemsView && Layout is LinearItemsLayout linearLayout) + { + mauiItemsView.SetLayoutOrientation(linearLayout.Orientation == ItemsLayoutOrientation.Horizontal); + } + + UpdateVerticalScrollBarVisibility(); + UpdateHorizontalScrollBarVisibility(); + UpdateEmptyView(); + } + + UI.Xaml.Controls.Layout CreateItemsLayout() + { + switch (Layout) + { + case GridItemsLayout gridItemsLayout: + return CreateGridView(gridItemsLayout); + case LinearItemsLayout listItemsLayout: + return CreateStackLayout(listItemsLayout); + default: + break; + } + + throw new NotImplementedException("The layout is not implemented"); + } + + static UI.Xaml.Controls.StackLayout CreateStackLayout(LinearItemsLayout listItemsLayout) + { + return new UI.Xaml.Controls.StackLayout() + { + Orientation = listItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal + ? Orientation.Horizontal : Orientation.Vertical, + Spacing = listItemsLayout.ItemSpacing + }; + } + + static UniformGridLayout CreateGridView(GridItemsLayout gridItemsLayout) + { + return new UniformGridLayout() + { + Orientation = gridItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal + ? Orientation.Vertical : Orientation.Horizontal, + MaximumRowsOrColumns = gridItemsLayout.Span, + MinColumnSpacing = gridItemsLayout.HorizontalItemSpacing, + MinRowSpacing = gridItemsLayout.VerticalItemSpacing, + ItemsStretch = UniformGridLayoutItemsStretch.Fill, + ItemsJustification = UniformGridLayoutItemsJustification.Start, + }; + } + + void FindScrollViewer() + { + if (PlatformView.ScrollView is not null) + { + OnScrollViewerFound(); + return; + } + + void ListViewLoaded(object sender, RoutedEventArgs e) + { + var lv = (WItemsView)sender; + lv.Loaded -= ListViewLoaded; + FindScrollViewer(); + } + + PlatformView.Loaded += ListViewLoaded; + } + + void OnScrollViewerFound() + { + if (PlatformView.ScrollView is null) + { + return; + } + + PlatformView.ScrollView.ViewChanged -= ScrollViewChanged; + PlatformView.ScrollView.PointerWheelChanged -= PointerScrollChanged; + + PlatformView.ScrollView.ViewChanged += ScrollViewChanged; + PlatformView.ScrollView.PointerWheelChanged += PointerScrollChanged; + } + + void LayoutPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == GridItemsLayout.SpanProperty.PropertyName) + { + UpdateItemsLayoutSpan(); + } + else if (e.PropertyName == GridItemsLayout.HorizontalItemSpacingProperty.PropertyName || e.PropertyName == GridItemsLayout.VerticalItemSpacingProperty.PropertyName) + { + UpdateItemsLayoutItemSpacing(); + } + else if (e.PropertyName == LinearItemsLayout.ItemSpacingProperty.PropertyName) + { + UpdateItemsLayoutItemSpacing(); + } + } + + void UpdateItemsLayoutSpan() + { + if (PlatformView.Layout is UniformGridLayout listViewLayout && + Layout is GridItemsLayout gridItemsLayout) + { + listViewLayout.MaximumRowsOrColumns = gridItemsLayout.Span; + } + } + + void UpdateItemsLayoutItemSpacing() + { + if (PlatformView.Layout is UniformGridLayout listViewLayout && + Layout is GridItemsLayout gridItemsLayout) + { + listViewLayout.MinColumnSpacing = gridItemsLayout.HorizontalItemSpacing; + listViewLayout.MinRowSpacing = gridItemsLayout.VerticalItemSpacing; + } + else if (PlatformView.Layout is UI.Xaml.Controls.StackLayout stackLayout && + Layout is LinearItemsLayout linearItemsLayout) + { + stackLayout.Spacing = linearItemsLayout.ItemSpacing; + } + } + + void UpdateEmptyView() + { + if (Element is null || PlatformView is null) + { + return; + } + + var emptyView = Element.EmptyView; + + var emptyViewTemplate = Element.EmptyViewTemplate; + if (emptyViewTemplate is not null) + { + // If EmptyViewTemplate is provided, use it instead of EmptyView + _emptyView = RealizeEmptyViewTemplate(emptyView, emptyViewTemplate); + } + else if (emptyView is not null) + { + switch (emptyView) + { + case string text: + _emptyView = new TextBlock + { + HorizontalAlignment = Microsoft.UI.Xaml.HorizontalAlignment.Center, + VerticalAlignment = Microsoft.UI.Xaml.VerticalAlignment.Center, + Text = text + }; + break; + case View view: + _emptyView = RealizeEmptyView(view); + break; + default: + _emptyView = RealizeEmptyViewTemplate(emptyView, null); // Fallback + break; + } + } + + (PlatformView as IEmptyView)?.SetEmptyView(_emptyView, _mauiEmptyView); + UpdateEmptyViewVisibility(); + } + + void UpdateEmptyViewVisibility() + { + // Check both CollectionViewSource.View and the underlying _itemsSource + // After a Reset action, CollectionViewSource.View.Count might not be immediately updated + bool isEmpty = (_collectionViewSource?.View?.Count ?? 0) == 0 && (_itemsSource?.Count ?? 0) == 0; + + if (isEmpty) + { + if (_mauiEmptyView is not null) + { + if (_emptyViewDisplayed) + { + ItemsView.RemoveLogicalChild(_mauiEmptyView); + } + + if (ItemsView.EmptyViewTemplate is null) + { + ItemsView.AddLogicalChild(_mauiEmptyView); + } + } + + if (_emptyView is not null && PlatformView is IEmptyView emptyView) + { + emptyView.EmptyViewVisibility = WVisibility.Visible; + } + + _emptyViewDisplayed = true; + } + else + { + if (_emptyViewDisplayed) + { + if (_emptyView is not null && PlatformView is IEmptyView emptyView) + { + emptyView.EmptyViewVisibility = WVisibility.Collapsed; + } + + ItemsView.RemoveLogicalChild(_mauiEmptyView); + } + + _emptyViewDisplayed = false; + } + } + + void UpdateHeader() + { + if (Element is not StructuredItemsView structuredItemsView || PlatformView is null) + { + return; + } + + var header = structuredItemsView.Header; + + if (header is null) + { + if (_headerDisplayed) + { + if (PlatformView is MauiItemsView mauiItemsView) + { + mauiItemsView.HeaderVisibility = WVisibility.Collapsed; + } + + if (_mauiHeader is not null) + { + ItemsView.RemoveLogicalChild(_mauiHeader); + } + _headerDisplayed = false; + } + return; + } + + _header = header switch + { + string text => new TextBlock + { + Text = text, + Margin = new Microsoft.UI.Xaml.Thickness(0, 0, 0, 10) + }, + + View view => RealizeHeaderFooterView(view, ref _mauiHeader), + _ => RealizeHeaderFooterTemplate(header, structuredItemsView.HeaderTemplate, ref _mauiHeader), + }; + + if (PlatformView is MauiItemsView platformItemsView && _mauiHeader is not null) + { + platformItemsView.SetHeader(_header, _mauiHeader); + platformItemsView.HeaderVisibility = WVisibility.Visible; + } + + // Add logical child if it's a View (not a template) + if (_mauiHeader is not null && structuredItemsView.HeaderTemplate is null) + { + ItemsView.AddLogicalChild(_mauiHeader); + } + + _headerDisplayed = true; + } + + void UpdateFooter() + { + if (Element is not StructuredItemsView structuredItemsView || PlatformView is null) + { + return; + } + + var footer = structuredItemsView.Footer; + + if (footer is null) + { + if (_footerDisplayed) + { + if (PlatformView is MauiItemsView mauiItemsView) + { + mauiItemsView.FooterVisibility = WVisibility.Collapsed; + } + + if (_mauiFooter is not null) + { + ItemsView.RemoveLogicalChild(_mauiFooter); + } + _footerDisplayed = false; + } + return; + } + + _footer = footer switch + { + string text => new TextBlock + { + Text = text, + Margin = new Microsoft.UI.Xaml.Thickness(0, 10, 0, 0) + }, + View view => RealizeHeaderFooterView(view, ref _mauiFooter), + _ => RealizeHeaderFooterTemplate(footer, structuredItemsView.FooterTemplate, ref _mauiFooter), + }; + + if (PlatformView is MauiItemsView platformItemsView && _mauiFooter is not null) + { + platformItemsView.SetFooter(_footer, _mauiFooter); + platformItemsView.FooterVisibility = WVisibility.Visible; + } + + // Add logical child if it's a View (not a template) + if (_mauiFooter is not null && structuredItemsView.FooterTemplate is null) + { + ItemsView.AddLogicalChild(_mauiFooter); + } + + _footerDisplayed = true; + } + +#nullable disable + FrameworkElement RealizeEmptyViewTemplate(object bindingContext, DataTemplate emptyViewTemplate) + { + if (emptyViewTemplate is null) + { + return new TextBlock + { + HorizontalAlignment = Microsoft.UI.Xaml.HorizontalAlignment.Center, + VerticalAlignment = Microsoft.UI.Xaml.VerticalAlignment.Center, + Text = bindingContext.ToString() + }; + } + + var template = emptyViewTemplate.SelectDataTemplate(bindingContext, null); + var view = template.CreateContent() as View; + view.BindingContext = bindingContext; + + return RealizeEmptyView(view); + } + + FrameworkElement RealizeEmptyView(View view) + { + _mauiEmptyView = view ?? throw new ArgumentNullException(nameof(view)); + + if (MauiContext is null) + throw new InvalidOperationException("MauiContext cannot be null when creating a handler."); + + var handler = view.ToHandler(MauiContext); + var platformView = handler.ContainerView ?? handler.PlatformView; + return platformView; + } + + FrameworkElement RealizeHeaderFooterTemplate(object bindingContext, DataTemplate template, ref View mauiView) + { + if (template is null) + { + return new TextBlock + { + Text = bindingContext.ToString(), + Margin = new Microsoft.UI.Xaml.Thickness(0, 5, 0, 5) + }; + } + + var dataTemplate = template.SelectDataTemplate(bindingContext, null); + var view = dataTemplate.CreateContent() as View; + view.BindingContext = bindingContext; + + return RealizeHeaderFooterView(view, ref mauiView); + } + + FrameworkElement RealizeHeaderFooterView(View view, ref View mauiView) + { + mauiView = view ?? throw new ArgumentNullException(nameof(view)); + + var handler = view.ToHandler(MauiContext); + var platformView = handler.ContainerView ?? handler.PlatformView; + + return platformView; + } +#nullable enable + + void UpdateVerticalScrollBarVisibility() + { + if (Element.VerticalScrollBarVisibility != ScrollBarVisibility.Default) + { + // If the value is changing to anything other than the default, record the default + if (_defaultVerticalScrollVisibility is null) + { + ScrollViewer.SetVerticalScrollBarVisibility(PlatformView, WASDKScrollBarVisibility.Visible); + } + } + + if (_defaultVerticalScrollVisibility is null) + { + // If the default has never been recorded, then this has never been set to anything but the + // default value; there's nothing to do. + return; + } + + switch (Element.VerticalScrollBarVisibility) + { + case (ScrollBarVisibility.Always): + ScrollViewer.SetVerticalScrollBarVisibility(PlatformView, WASDKScrollBarVisibility.Visible); + break; + case (ScrollBarVisibility.Never): + ScrollViewer.SetVerticalScrollBarVisibility(PlatformView, WASDKScrollBarVisibility.Hidden); + break; + case (ScrollBarVisibility.Default): + ScrollViewer.SetVerticalScrollBarVisibility(PlatformView, _defaultVerticalScrollVisibility.Value); + break; + } + } + + void UpdateHorizontalScrollBarVisibility() + { + if (_defaultHorizontalScrollVisibility is null) + { + _defaultHorizontalScrollVisibility = ScrollViewer.GetHorizontalScrollBarVisibility(PlatformView); + } + + switch (Element.HorizontalScrollBarVisibility) + { + case (ScrollBarVisibility.Always): + ScrollViewer.SetHorizontalScrollBarVisibility(PlatformView, WASDKScrollBarVisibility.Visible); + break; + case (ScrollBarVisibility.Never): + ScrollViewer.SetHorizontalScrollBarVisibility(PlatformView, WASDKScrollBarVisibility.Hidden); + break; + case (ScrollBarVisibility.Default): + ScrollViewer.SetHorizontalScrollBarVisibility(PlatformView, _defaultHorizontalScrollVisibility.Value); + break; + } + } + + void PointerScrollChanged(object sender, PointerRoutedEventArgs e) + { + if (PlatformView.ScrollView.ComputedHorizontalScrollMode == ScrollingScrollMode.Enabled) + { + PlatformView.ScrollView.AddScrollVelocity(new(e.GetCurrentPoint(PlatformView.ScrollView).Properties.MouseWheelDelta, 0), null); + } + } + + void ScrollViewChanged(UI.Xaml.Controls.ScrollView sender, object args) + { + HandleScroll(PlatformView.ScrollView.ScrollPresenter); + } + + void HandleScroll(WScrollPresenter scrollViewer) + { + var itemsViewScrolledEventArgs = new ItemsViewScrolledEventArgs + { + HorizontalOffset = scrollViewer.HorizontalOffset, + HorizontalDelta = scrollViewer.HorizontalOffset - _previousHorizontalOffset, + VerticalOffset = scrollViewer.VerticalOffset, + VerticalDelta = scrollViewer.VerticalOffset - _previousVerticalOffset, + }; + + _previousHorizontalOffset = scrollViewer.HorizontalOffset; + _previousVerticalOffset = scrollViewer.VerticalOffset; + + bool advancing = true; + switch (Layout) + { + case LinearItemsLayout linearItemsLayout: + advancing = itemsViewScrolledEventArgs.HorizontalDelta > 0; + break; + case GridItemsLayout gridItemsLayout: + advancing = itemsViewScrolledEventArgs.VerticalDelta > 0; + break; + default: + break; + } + + itemsViewScrolledEventArgs = ComputeVisibleIndexes(itemsViewScrolledEventArgs, advancing); + + Element.SendScrolled(itemsViewScrolledEventArgs); + + var remainingItemsThreshold = Element.RemainingItemsThreshold; + if (_collectionViewSource != null && remainingItemsThreshold > -1 && + _collectionViewSource.View.Count - 1 - itemsViewScrolledEventArgs.LastVisibleItemIndex <= remainingItemsThreshold) + { + Element.SendRemainingItemsThresholdReached(); + } + } + + ItemsViewScrolledEventArgs ComputeVisibleIndexes(ItemsViewScrolledEventArgs args, bool advancing) + { + var (firstVisibleItemIndex, lastVisibleItemIndex, centerItemIndex) = GetVisibleIndexes(advancing); + + args.FirstVisibleItemIndex = firstVisibleItemIndex; + args.CenterItemIndex = centerItemIndex; + args.LastVisibleItemIndex = lastVisibleItemIndex; + + return args; + } + + (int firstVisibleItemIndex, int lastVisibleItemIndex, int centerItemIndex) GetVisibleIndexes(bool advancing) + { + int firstVisibleItemIndex = -1; + int lastVisibleItemIndex = -1; + + PlatformView.TryGetItemIndex(0, 0, out firstVisibleItemIndex); + PlatformView.TryGetItemIndex(1, 1, out lastVisibleItemIndex); + + double center = (lastVisibleItemIndex + firstVisibleItemIndex) / 2.0; + int centerItemIndex = advancing ? (int)Math.Ceiling(center) : (int)Math.Floor(center); + + return (firstVisibleItemIndex, lastVisibleItemIndex, centerItemIndex); + } + + void ScrollToRequested(object? sender, ScrollToRequestEventArgs args) + { + var index = args.Index; + if (args.Mode == ScrollToMode.Element) + { + index = FindItemIndex(args.Item); + } + + if (index >= 0) + { + float offset = 0.0f; + switch (args.ScrollToPosition) + { + case ScrollToPosition.Start: + offset = 0.0f; + break; + case ScrollToPosition.Center: + offset = 0.5f; + break; + case ScrollToPosition.End: + offset = 1.0f; + break; + } + + PlatformView.StartBringItemIntoView(index, new BringIntoViewOptions() + { + AnimationDesired = args.IsAnimated, + VerticalAlignmentRatio = offset, + HorizontalAlignmentRatio = offset + }); + } + } + + int FindItemIndex(object item) + { + if(_collectionViewSource is null) + { + return -1; + } + + for (int index = 0; index < _collectionViewSource.View.Count; index++) + { + if (_collectionViewSource.View[index] is ItemTemplateContext pair) + { + if (pair.Item == item) + { + return index; + } + } + } + + return -1; + } + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs new file mode 100644 index 000000000000..f817430b1942 --- /dev/null +++ b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Maui.Controls.Platform; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Data; +using WItemsView = Microsoft.UI.Xaml.Controls.ItemsView; + +namespace Microsoft.Maui.Controls.Handlers.Items2; + +public partial class CollectionViewHandler2 : ItemsViewHandler2 +{ + bool _ignorePlatformSelectionChange; + + protected override IItemsLayout Layout { get => ItemsView.ItemsLayout; } + + public static void MapCanReorderItems(CollectionViewHandler2 handler, ReorderableItemsView itemsView) + { + } + + public static void MapIsGrouped(CollectionViewHandler2 handler, GroupableItemsView itemsView) + { + handler.UpdateItemsSource(); + } + + public static void MapItemsSource(CollectionViewHandler2 handler, SelectableItemsView itemsView) + { + ItemsViewHandler2.MapItemsSource(handler, itemsView); + } + + public static void MapSelectedItem(CollectionViewHandler2 handler, SelectableItemsView itemsView) + { + + } + + public static void MapSelectedItems(CollectionViewHandler2 handler, SelectableItemsView itemsView) + { + } + + public static void MapSelectionMode(CollectionViewHandler2 handler, SelectableItemsView itemsView) + { + } + + protected override void ConnectHandler(WItemsView platformView) + { + base.ConnectHandler(platformView); + + ItemsView.SelectionChanged += VirtualSelectionChanged; + if (PlatformView is not null) + { + PlatformView.SetBinding(WItemsView.SelectionModeProperty, + new UI.Xaml.Data.Binding + { + Source = ItemsView, + Path = new UI.Xaml.PropertyPath("SelectionMode"), + Converter = new SelectionModeConvert(), + Mode = UI.Xaml.Data.BindingMode.TwoWay + }); + + PlatformView.SelectionChanged += PlatformSelectionChanged; + } + } + + protected override void DisconnectHandler(WItemsView platformView) + { + var oldListViewBase = platformView; + + if (oldListViewBase is not null) + { + oldListViewBase.ClearValue(WItemsView.SelectionModeProperty); + oldListViewBase.SelectionChanged -= PlatformSelectionChanged; + } + + if (ItemsView is not null) + { + ItemsView.SelectionChanged -= VirtualSelectionChanged; + } + + base.DisconnectHandler(platformView); + } + + protected override void UpdateItemsSource() + { + _ignorePlatformSelectionChange = true; + + base.UpdateItemsSource(); + UpdatePlatformSelection(); + + _ignorePlatformSelectionChange = false; + } + + void VirtualSelectionChanged(object? sender, SelectionChangedEventArgs? e) + { + UpdatePlatformSelection(); + } + + void PlatformSelectionChanged(WItemsView sender, ItemsViewSelectionChangedEventArgs args) + { + UpdateVirtualSelection(); + } + + void UpdateVirtualSelection() + { + if (_ignorePlatformSelectionChange || ItemsView is null) + { + return; + } + + switch (PlatformView.SelectionMode) + { + case ItemsViewSelectionMode.Single: + UpdateVirtualSingleSelection(); + break; + case ItemsViewSelectionMode.Multiple: + UpdateVirtualMultipleSelection(); + break; + case ItemsViewSelectionMode.None: + default: + break; + } + + var formsItemContentControls = PlatformView.GetChildren(); + foreach (var formsItemContentControl in formsItemContentControls) + { + if (formsItemContentControl is not null) + { + bool isSelected = ItemsView.SelectedItem == formsItemContentControl.FormsDataContext || + ItemsView.SelectedItems.Contains(formsItemContentControl.FormsDataContext); + formsItemContentControl.UpdateIsSelected(isSelected); + } + } + } + + void UpdateVirtualSingleSelection() + { + var selectedItem = PlatformView.SelectedItem is ItemTemplateContext2 itemPair + ? itemPair.Item + : PlatformView.SelectedItem; + + if (ItemsView is not null) + { + ItemsView.SelectionChanged -= VirtualSelectionChanged; + ItemsView.SelectedItem = selectedItem; + + ItemsView.SelectionChanged += VirtualSelectionChanged; + } + } + + void UpdateVirtualMultipleSelection() + { + ItemsView.SelectionChanged -= VirtualSelectionChanged; + + var selection = new List(); + for (int index = 0; index < PlatformView.SelectedItems.Count; index++) + { + var item = PlatformView.SelectedItems[index]; + var selectedItem = item is ItemTemplateContext2 itc ? itc.Item : item; + if (selectedItem is not null) + selection.Add(selectedItem); + } + + ItemsView.UpdateSelectedItems(selection); + ItemsView.SelectionChanged += VirtualSelectionChanged; + } + + void UpdatePlatformSelection() + { + _ignorePlatformSelectionChange = true; + + var itemList = PlatformView.ItemsSource as ICollectionView; + + if (itemList is null) + { + return; + } + + switch (PlatformView.SelectionMode) + { + case ItemsViewSelectionMode.Single: + if (ItemsView is not null) + { + if (ItemsView.SelectedItem is null) + { + PlatformView.DeselectAll(); + } + else + { + var selectedItem = itemList.FirstOrDefault(item => + { + if (item is ItemTemplateContext2 itemPair) + { + return itemPair.Item == ItemsView.SelectedItem; + } + else + { + return item == ItemsView.SelectedItem; + } + }); + + if (selectedItem is not null) + { + PlatformView.Select(itemList.IndexOf(selectedItem)); + } + } + } + + break; + case ItemsViewSelectionMode.Multiple: + PlatformView.DeselectAll(); + + // Use safe enumeration to avoid ArgumentOutOfRangeException during collection updates + int index = 0; + foreach (var nativeItem in itemList) + { + if (nativeItem is ItemTemplateContext2 itemPair && ItemsView.SelectedItems.Contains(itemPair.Item)) + { + PlatformView.Select(index); + } + else if (ItemsView.SelectedItems.Contains(nativeItem)) + { + PlatformView.Select(index); + } + index++; + } + break; + case ItemsViewSelectionMode.None: + case ItemsViewSelectionMode.Extended: + default: + break; + } + + _ignorePlatformSelectionChange = false; + } +} + +partial class SelectionModeConvert : UI.Xaml.Data.IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + { + var formSelectionMode = (SelectionMode)value; + switch (formSelectionMode) + { + case SelectionMode.Single: + return ItemsViewSelectionMode.Single; + case SelectionMode.Multiple: + return ItemsViewSelectionMode.Multiple; + default: + return ItemsViewSelectionMode.None; + } + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + var uwpListViewSelectionMode = (ItemsViewSelectionMode)value; + switch (uwpListViewSelectionMode) + { + case ItemsViewSelectionMode.None: + return SelectionMode.None; + case ItemsViewSelectionMode.Single: + return SelectionMode.Single; + case ItemsViewSelectionMode.Multiple: + return SelectionMode.Multiple; + case ItemsViewSelectionMode.Extended: + return SelectionMode.None; + default: + return SelectionMode.None; + } + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs new file mode 100644 index 000000000000..4aea33c23857 --- /dev/null +++ b/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs @@ -0,0 +1,261 @@ +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace Microsoft.Maui.Controls.Handlers.Items2 +{ + internal class GroupedItemTemplateCollection2 : ObservableCollection + { + readonly IEnumerable _itemsSource; + readonly DataTemplate _itemTemplate; + readonly DataTemplate _groupHeaderTemplate; + readonly DataTemplate _groupFooterTemplate; + readonly BindableObject _container; + readonly IMauiContext? _mauiContext; + readonly Dictionary _groupSubscriptions = new(); + + public GroupedItemTemplateCollection2(IEnumerable itemsSource, + DataTemplate itemTemplate, DataTemplate groupHeaderTemplate, DataTemplate groupFooterTemplate, + BindableObject container, IMauiContext? mauiContext = null) + { + _itemsSource = itemsSource; + _itemTemplate = itemTemplate; + _groupHeaderTemplate = groupHeaderTemplate; + _groupFooterTemplate = groupFooterTemplate; + _container = container; + _mauiContext = mauiContext; + + AddItems(_itemsSource); + SubscribeToGroups(_itemsSource); + if (_itemsSource is IList groupList && _itemsSource is INotifyCollectionChanged incc) + { + incc.CollectionChanged += GroupsChanged; + } + } + + void SubscribeToGroups(IEnumerable? groups) + { + if (groups is null) return; + foreach (var group in groups) + { + if (group is INotifyCollectionChanged incc && !_groupSubscriptions.ContainsKey(group)) + { + incc.CollectionChanged += (s, e) => GroupItemsChanged(group, e); + _groupSubscriptions[group] = incc; + } + } + } + + void UnsubscribeFromGroups(IEnumerable? groups) + { + if (groups is null) return; + foreach (var group in groups) + { + if (_groupSubscriptions.TryGetValue(group, out var incc)) + { + incc.CollectionChanged -= (s, e) => GroupItemsChanged(group, e); + _groupSubscriptions.Remove(group); + } + } + } + + void AddItems(IEnumerable? items) + { + if (items is null) + { + return; + } + + var newItems = new List(); + int index = Items.Count; + foreach (var group in items) + { + if (group is IList itemsList) + { + if (_groupHeaderTemplate is not null) + { + var newItem = new ItemTemplateContext2(_groupHeaderTemplate, group, _container, null, + null, null, true, false, mauiContext: _mauiContext); + newItems.Add(newItem); + Items.Add(newItem); + } + + foreach (var item in itemsList) + { + var newItem = new ItemTemplateContext2(_itemTemplate, item, _container, mauiContext: _mauiContext); + newItems.Add(newItem); + Items.Add(newItem); + } + + if (_groupFooterTemplate is not null) + { + var newItem = new ItemTemplateContext2(_groupFooterTemplate, group, _container, null, + null, null, isHeader: false, isFooter: true, mauiContext: _mauiContext); + newItems.Add(newItem); + Items.Add(newItem); + } + } + } + + OnCollectionChanged(new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, newItems, index)); + } + + void GroupsChanged(object? sender, NotifyCollectionChangedEventArgs args) + { + _container.Dispatcher.DispatchIfRequired(() => OnNotifyCollectionChanged(args)); + } + + void OnNotifyCollectionChanged(NotifyCollectionChangedEventArgs args) + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + AddItems(args.NewItems); + SubscribeToGroups(args.NewItems); + break; + case NotifyCollectionChangedAction.Move: + break; + case NotifyCollectionChangedAction.Remove: + if (args.OldItems is not null) + { + UnsubscribeFromGroups(args.OldItems); + var startIndex = args.OldStartingIndex; + if (startIndex < 0) + { + Reset(); + return; + } + + int count = 0; + foreach (var item in args.OldItems) + { + count++; + if (item is IList itemsList) + { + count += itemsList.Count; + } + } + + for (int index = startIndex + count - 1; index >= startIndex; index--) + { + RemoveAt(index); + } + } + break; + case NotifyCollectionChangedAction.Replace: + case NotifyCollectionChangedAction.Reset: + Reset(); + break; + } + } + + void GroupItemsChanged(object group, NotifyCollectionChangedEventArgs e) + { + // Find the start index of this group's items in the flat list + int groupIndex = -1; + int current = 0; + foreach (var g in _itemsSource) + { + if (g == group) + { + groupIndex = current; + break; + } + current++; + } + if (groupIndex == -1) return; + + // Calculate the flat list index for the first item of this group + int flatIndex = 0; + current = 0; + foreach (var g in _itemsSource) + { + if (g is IList itemsList) + { + if (_groupHeaderTemplate is not null) flatIndex++; + if (current == groupIndex) break; + flatIndex += itemsList.Count; + if (_groupFooterTemplate is not null) flatIndex++; + } + current++; + } + + // Do NOT increment flatIndex for header here; it is already handled above + + if (group is IList groupList) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + if (e.NewItems is not null) + { + int insertIndex = flatIndex + (e.NewStartingIndex >= 0 ? e.NewStartingIndex : groupList.Count - e.NewItems.Count); + var newItems = new List(); + foreach (var item in e.NewItems) + { + var newItem = new ItemTemplateContext2(_itemTemplate, item, _container, mauiContext: _mauiContext); + newItems.Add(newItem); + Items.Insert(insertIndex++, newItem); + } + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newItems, insertIndex - newItems.Count)); + } + break; + case NotifyCollectionChangedAction.Remove: + if (e.OldItems is not null) + { + int removeIndex = flatIndex + (e.OldStartingIndex >= 0 ? e.OldStartingIndex : 0); + for (int i = 0; i < e.OldItems.Count; i++) + { + Items.RemoveAt(removeIndex); + } + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, e.OldItems, removeIndex)); + } + break; + case NotifyCollectionChangedAction.Replace: + if (e.NewItems is not null && e.OldItems is not null) + { + int replaceIndex = flatIndex + (e.NewStartingIndex >= 0 ? e.NewStartingIndex : 0); + for (int i = 0; i < e.NewItems.Count; i++) + { + var newItem = new ItemTemplateContext2(_itemTemplate, e.NewItems[i]!, _container, mauiContext: _mauiContext); + Items[replaceIndex + i] = newItem; + } + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, e.NewItems, e.OldItems, replaceIndex)); + } + break; + case NotifyCollectionChangedAction.Move: + if (e.OldItems is not null && e.NewItems is not null) + { + int oldIndex = flatIndex + (e.OldStartingIndex >= 0 ? e.OldStartingIndex : 0); + int newIndex = flatIndex + (e.NewStartingIndex >= 0 ? e.NewStartingIndex : 0); + var moved = new List(); + for (int i = 0; i < e.OldItems.Count; i++) + { + var item = Items[oldIndex]; + Items.RemoveAt(oldIndex); + Items.Insert(newIndex + i, item); + moved.Add(item); + } + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, moved, newIndex, oldIndex)); + } + break; + case NotifyCollectionChangedAction.Reset: + Reset(); + break; + } + } + } + + public void Reset() + { + UnsubscribeFromGroups(_itemsSource); + Items.Clear(); + AddItems(_itemsSource); + SubscribeToGroups(_itemsSource); + var reset = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + OnCollectionChanged(reset); + } + } +} diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs new file mode 100644 index 000000000000..8a3420034e34 --- /dev/null +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs @@ -0,0 +1,122 @@ +using System.Collections.Generic; +using Microsoft.Maui.Controls.Platform; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Microsoft.Maui.Controls.Handlers.Items2 +{ + internal partial class ItemFactory(ItemsView view) : IElementFactory + { + private readonly ItemsView _view = view; + private Dictionary> _recyclePool = new(); + + internal static readonly BindableProperty OriginTemplateProperty = + BindableProperty.CreateAttached( + "OriginTemplate", typeof(DataTemplate), typeof(ItemFactory), null); + + public UIElement? GetElement(ElementFactoryGetArgs args) + { + // NOTE: 1.6: replace w/ RecyclePool + if (args.Data is ItemTemplateContext2 templateContext) + { + DataTemplate? template = templateContext.MauiDataTemplate; + if (template is DataTemplateSelector selector) + { + template = selector.SelectTemplate(templateContext.Item, _view); + } + + if (template is null) + { + template = _view.EmptyViewTemplate; + } + + ItemContainer? container = null; + ElementWrapper? wrapper = null; + + if (_recyclePool.TryGetValue(template, out var itemContainers)) + { + if (itemContainers.Count > 0) + { + container = itemContainers[0]; + if (container is not null) + { + wrapper = container.Child as ElementWrapper; + } + + itemContainers.RemoveAt(0); + } + } + + if (wrapper is null) + { + var viewContent = template.CreateContent() as View; + if (_view.Handler?.MauiContext is not null && viewContent is not null) + { + wrapper = new ElementWrapper(_view.Handler.MauiContext); + wrapper.HorizontalAlignment = HorizontalAlignment.Stretch; + wrapper.HorizontalContentAlignment = HorizontalAlignment.Stretch; + wrapper.SetContent(viewContent); + + if (wrapper?.VirtualView is View virtualView) + { + virtualView.SetValue(OriginTemplateProperty, template); + } + } + } + + if (wrapper?.VirtualView is View view) + { + view.BindingContext = templateContext.Item ?? _view.BindingContext; + _view.AddLogicalChild(view); + } + + container ??= new ItemContainer() + { + Child = wrapper, + HorizontalAlignment = HorizontalAlignment.Stretch + }; + + return container; + } + + return null; + } + + public void RecycleElement(ElementFactoryRecycleArgs args) + { + var item = args.Element as ItemContainer; + var wrapper = item?.Child as ElementWrapper; + var wrapperView = wrapper?.VirtualView as View; + DataTemplate? template = wrapperView?.GetValue(OriginTemplateProperty) as DataTemplate; + if (template != null && item is not null) + { + if (_recyclePool.TryGetValue(template, out var itemContainers)) + { + itemContainers.Add(item); + } + else + { + _recyclePool[template] = new List { item }; + } + } + + _view.RemoveLogicalChild(wrapperView); + } + } + + internal partial class ElementWrapper(IMauiContext context) : ContentControl + { + public IView? VirtualView { get; private set; } + + private IMauiContext _context = context; + + public void SetContent(IView view) + { + if (VirtualView is null || VirtualView.Handler is null) + { + Content = view.ToPlatform(_context); + VirtualView = view; + } + } + } +} diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs new file mode 100644 index 000000000000..b9e26488a25b --- /dev/null +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs @@ -0,0 +1,40 @@ +using System; + +namespace Microsoft.Maui.Controls.Handlers.Items2; + +internal class ItemTemplateContext2 +{ + readonly WeakReference _container; + public DataTemplate? MauiDataTemplate { get; } + public IMauiContext? MauiContext { get; } + public object? Item { get; } + public BindableObject? Container => _container.TryGetTarget(out var c) ? c : null; + public double ItemHeight { get; } + public double ItemWidth { get; } + public Thickness ItemSpacing { get; } + + public bool IsHeader { get; } + public bool IsFooter { get; } + + public ItemTemplateContext2(DataTemplate mauiDataTemplate, object item, BindableObject container, + double? height = null, double? width = null, Thickness? itemSpacing = null, + bool isHeader = false, bool isFooter = false, IMauiContext? mauiContext = null) + { + MauiDataTemplate = mauiDataTemplate; + Item = item; + _container = new(container); + MauiContext = mauiContext; + + if (height.HasValue) + ItemHeight = height.Value; + + if (width.HasValue) + ItemWidth = width.Value; + + if (itemSpacing.HasValue) + ItemSpacing = itemSpacing.Value; + + IsHeader = isHeader; + IsFooter = isFooter; + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextEnumerable2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextEnumerable2.cs new file mode 100644 index 000000000000..7529ef21aecb --- /dev/null +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextEnumerable2.cs @@ -0,0 +1,41 @@ +using System.Collections; + +namespace Microsoft.Maui.Controls.Handlers.Items2 +{ + internal class ItemTemplateContextEnumerable2 : IEnumerable + { + readonly IEnumerable _itemsSource; + readonly DataTemplate _formsDataTemplate; + readonly BindableObject _container; + readonly IMauiContext? _mauiContext; + readonly double _itemHeight; + readonly double _itemWidth; + readonly Thickness _itemSpacing; + + public ItemTemplateContextEnumerable2(IEnumerable itemsSource, DataTemplate formsDataTemplate, BindableObject container, + double? itemHeight = null, double? itemWidth = null, Thickness? itemSpacing = null, IMauiContext? mauiContext = null) + { + _itemsSource = itemsSource; + _formsDataTemplate = formsDataTemplate; + _container = container; + _mauiContext = mauiContext; + if (itemHeight.HasValue) + _itemHeight = itemHeight.Value; + + if (itemWidth.HasValue) + _itemWidth = itemWidth.Value; + + if (itemSpacing.HasValue) + _itemSpacing = itemSpacing.Value; + } + + public IEnumerator GetEnumerator() + { + foreach (var item in _itemsSource) + { + yield return new ItemTemplateContext2(_formsDataTemplate, item, _container, _itemHeight, _itemWidth, _itemSpacing, + false, false, _mauiContext); + } + } + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextList2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextList2.cs new file mode 100644 index 000000000000..5ad6dd7f8fc5 --- /dev/null +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextList2.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Microsoft.Maui.Controls.Handlers.Items2 +{ + internal class ItemTemplateContextList2 : IReadOnlyList + { + readonly IList _itemsSource; + readonly DataTemplate _itemTemplate; + readonly BindableObject _container; + readonly IMauiContext? _mauiContext; + readonly double _itemHeight; + readonly double _itemWidth; + readonly Thickness _itemSpacing; + + readonly Dictionary _itemTemplateContexts; + + public int Count => _itemsSource.Count; + + public ItemTemplateContext2 this[int index] + { + get + { + if (!_itemTemplateContexts.TryGetValue(index, out var context)) + { + var item = _itemsSource[index]; + if (item is null) + throw new InvalidOperationException($"Item at index {index} is null. ItemTemplateContext2 requires a non-null item."); + + _itemTemplateContexts[index] = context = new ItemTemplateContext2(_itemTemplate, item, + _container, _itemHeight, _itemWidth, _itemSpacing, false, false, _mauiContext); + } + + return context; + } + } + + public ItemTemplateContextList2(IList itemsSource, DataTemplate itemTemplate, BindableObject container, + double? itemHeight = null, double? itemWidth = null, Thickness? itemSpacing = null, IMauiContext? mauiContext = null) + { + _itemsSource = itemsSource; + _itemTemplate = itemTemplate; + _container = container; + _mauiContext = mauiContext; + if (itemHeight.HasValue) + _itemHeight = itemHeight.Value; + + if (itemWidth.HasValue) + _itemWidth = itemWidth.Value; + + if (itemSpacing.HasValue) + _itemSpacing = itemSpacing.Value; + + _itemTemplateContexts = new(capacity: Math.Min(64, _itemsSource.Count)); + } + + public IEnumerator GetEnumerator() + { + return new ItemTemplateContextListEnumerator2(this); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + +#nullable disable + internal class ItemTemplateContextListEnumerator2 : IEnumerator + { + public ItemTemplateContext2 Current { get; private set; } + object IEnumerator.Current => Current; + int _currentIndex = -1; + private ItemTemplateContextList2 _itemTemplateContextList; + + public ItemTemplateContextListEnumerator2(ItemTemplateContextList2 observableItemTemplateCollection) + { + _itemTemplateContextList = observableItemTemplateCollection; + } + + public void Dispose() + { + } + + public bool MoveNext() + { + if (_currentIndex >= _itemTemplateContextList.Count - 1) + { + return false; + } + + _currentIndex += 1; + Current = _itemTemplateContextList[_currentIndex]; + + return true; + } + + public void Reset() + { + Current = null; + _currentIndex = -1; + } + } +#nullable enable + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs b/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs new file mode 100644 index 000000000000..3845670933d0 --- /dev/null +++ b/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs @@ -0,0 +1,244 @@ +using Microsoft.Maui.Controls.Platform; +using Microsoft.Maui.Graphics; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using WApp = Microsoft.UI.Xaml.Application; +using WControlTemplate = Microsoft.UI.Xaml.Controls.ControlTemplate; +using WVisibility = Microsoft.UI.Xaml.Visibility; +using WStackPanel = Microsoft.UI.Xaml.Controls.StackPanel; + +namespace Microsoft.Maui.Controls.Handlers.Items2 +{ + internal partial class MauiItemsView : UI.Xaml.Controls.ItemsView, IEmptyView + { + ContentControl? _emptyViewContentControl; + FrameworkElement? _emptyView; + View? _mauiEmptyView; + + ContentControl? _headerContentControl; + FrameworkElement? _header; + View? _mauiHeader; + + ContentControl? _footerContentControl; + FrameworkElement? _footer; + View? _mauiFooter; + + WStackPanel? _containerPanel; + FrameworkElement? _itemsRepeater; + bool _isHorizontalLayout; + + public MauiItemsView() + { + Template = (WControlTemplate)WApp.Current.Resources["MauiItemsViewTemplate"]; + } + + public static readonly DependencyProperty EmptyViewVisibilityProperty = + DependencyProperty.Register(nameof(EmptyViewVisibility), typeof(Visibility), + typeof(MauiItemsView), new PropertyMetadata(WVisibility.Collapsed, EmptyViewVisibilityChanged)); + + static void EmptyViewVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is MauiItemsView itemsView) + { + // Update this manually; normally we'd just bind this, but TemplateBinding doesn't seem to work + // for WASDK right now. + itemsView.UpdateEmptyViewVisibility((WVisibility)e.NewValue); + } + } + + public WVisibility EmptyViewVisibility + { + get + { + return (WVisibility)GetValue(EmptyViewVisibilityProperty); + } + set + { + SetValue(EmptyViewVisibilityProperty, value); + } + } + + public static readonly DependencyProperty HeaderVisibilityProperty = + DependencyProperty.Register(nameof(HeaderVisibility), typeof(Visibility), + typeof(MauiItemsView), new PropertyMetadata(WVisibility.Collapsed, HeaderVisibilityChanged)); + + static void HeaderVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is MauiItemsView itemsView) + { + itemsView.UpdateHeaderVisibility((WVisibility)e.NewValue); + } + } + + public WVisibility HeaderVisibility + { + get => (WVisibility)GetValue(HeaderVisibilityProperty); + set => SetValue(HeaderVisibilityProperty, value); + } + + public static readonly DependencyProperty FooterVisibilityProperty = + DependencyProperty.Register(nameof(FooterVisibility), typeof(Visibility), + typeof(MauiItemsView), new PropertyMetadata(WVisibility.Collapsed, FooterVisibilityChanged)); + + static void FooterVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is MauiItemsView itemsView) + { + itemsView.UpdateFooterVisibility((WVisibility)e.NewValue); + } + } + + public WVisibility FooterVisibility + { + get => (WVisibility)GetValue(FooterVisibilityProperty); + set => SetValue(FooterVisibilityProperty, value); + } + + public void SetEmptyView(FrameworkElement emptyView, View mauiEmptyView) + { + _emptyView = emptyView; + _mauiEmptyView = mauiEmptyView; + + if (_emptyViewContentControl is not null) + { + _emptyViewContentControl.Content = emptyView; + UpdateEmptyViewVisibility(EmptyViewVisibility); + } + } + + public void SetHeader(FrameworkElement header, View mauiHeader) + { + _header = header; + _mauiHeader = mauiHeader; + + if (_headerContentControl is not null) + { + _headerContentControl.Content = header; + UpdateHeaderVisibility(HeaderVisibility); + } + } + + public void SetFooter(FrameworkElement footer, View mauiFooter) + { + _footer = footer; + _mauiFooter = mauiFooter; + + if (_footerContentControl is not null) + { + _footerContentControl.Content = footer; + UpdateFooterVisibility(FooterVisibility); + } + } + + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + _emptyViewContentControl = GetTemplateChild("EmptyViewContentControl") as ContentControl; + _headerContentControl = GetTemplateChild("HeaderContentControl") as ContentControl; + _footerContentControl = GetTemplateChild("FooterContentControl") as ContentControl; + _containerPanel = GetTemplateChild("PART_ContainerGrid") as WStackPanel; + _itemsRepeater = GetTemplateChild("PART_ItemsRepeater") as FrameworkElement; + + if (_emptyView is not null && _emptyViewContentControl is not null) + { + _emptyViewContentControl.Content = _emptyView; + UpdateEmptyViewVisibility(EmptyViewVisibility); + } + + if (_header is not null && _headerContentControl is not null) + { + _headerContentControl.Content = _header; + UpdateHeaderVisibility(HeaderVisibility); + } + + if (_footer is not null && _footerContentControl is not null) + { + _footerContentControl.Content = _footer; + UpdateFooterVisibility(FooterVisibility); + } + + // Apply orientation if it was set before template was applied + ApplyLayoutOrientation(); + } + + public void SetLayoutOrientation(bool isHorizontal) + { + _isHorizontalLayout = isHorizontal; + ApplyLayoutOrientation(); + } + + void ApplyLayoutOrientation() + { + if (_containerPanel is null || _headerContentControl is null || _footerContentControl is null || _itemsRepeater is null) + { + return; + } + + if (_isHorizontalLayout) + { + _containerPanel.Orientation = Orientation.Horizontal; + _itemsRepeater.VerticalAlignment = UI.Xaml.VerticalAlignment.Top; + _itemsRepeater.HorizontalAlignment = UI.Xaml.HorizontalAlignment.Left; + _headerContentControl.VerticalContentAlignment = UI.Xaml.VerticalAlignment.Top; + _headerContentControl.HorizontalContentAlignment = UI.Xaml.HorizontalAlignment.Left; + _footerContentControl.VerticalContentAlignment = UI.Xaml.VerticalAlignment.Top; + _footerContentControl.HorizontalContentAlignment = UI.Xaml.HorizontalAlignment.Left; + } + else + { + _containerPanel.Orientation = Orientation.Vertical; + _itemsRepeater.VerticalAlignment = UI.Xaml.VerticalAlignment.Top; + _itemsRepeater.HorizontalAlignment = UI.Xaml.HorizontalAlignment.Stretch; + _headerContentControl.VerticalContentAlignment = UI.Xaml.VerticalAlignment.Top; + _headerContentControl.HorizontalContentAlignment = UI.Xaml.HorizontalAlignment.Stretch; + _footerContentControl.VerticalContentAlignment = UI.Xaml.VerticalAlignment.Top; + _footerContentControl.HorizontalContentAlignment = UI.Xaml.HorizontalAlignment.Stretch; + } + } + + protected override global::Windows.Foundation.Size MeasureOverride(global::Windows.Foundation.Size availableSize) + { + _mauiEmptyView?.Measure(availableSize.Width, availableSize.Height); + + return base.MeasureOverride(availableSize); + } + + protected override global::Windows.Foundation.Size ArrangeOverride(global::Windows.Foundation.Size finalSize) + { + _mauiEmptyView?.Arrange(new Rect(0, 0, finalSize.Width, finalSize.Height)); + + return base.ArrangeOverride(finalSize); + } + + void UpdateEmptyViewVisibility(WVisibility visibility) + { + if (_emptyViewContentControl is null) + { + return; + } + + _emptyViewContentControl.Visibility = visibility; + } + + void UpdateHeaderVisibility(WVisibility visibility) + { + if (_headerContentControl is null) + { + return; + } + + _headerContentControl.Visibility = visibility; + } + + void UpdateFooterVisibility(WVisibility visibility) + { + if (_footerContentControl is null) + { + return; + } + + _footerContentControl.Visibility = visibility; + } + } +} diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ObservableItemTemplateCollection2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ObservableItemTemplateCollection2.cs new file mode 100644 index 000000000000..6ae24beffc7d --- /dev/null +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ObservableItemTemplateCollection2.cs @@ -0,0 +1,288 @@ +using System; +using System.Collections; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace Microsoft.Maui.Controls.Handlers.Items2 +{ + internal class ObservableItemTemplateCollection2 : ObservableCollection + { + readonly IList _itemsSource; + readonly DataTemplate _itemTemplate; + readonly BindableObject _container; + readonly IMauiContext? _mauiContext; + readonly double _itemHeight; + readonly double _itemWidth; + readonly Thickness _itemSpacing; + readonly NotifyCollectionChangedEventHandler _collectionChanged; + readonly WeakNotifyCollectionChangedProxy _proxy = new(); + + bool _innerCollectionChange = false; + bool _observeChanges = true; + + ~ObservableItemTemplateCollection2() => _proxy.Unsubscribe(); + + public ObservableItemTemplateCollection2(IList itemsSource, DataTemplate itemTemplate, BindableObject container, + double? itemHeight = null, double? itemWidth = null, Thickness? itemSpacing = null, IMauiContext? mauiContext = null) + { + + + _itemsSource = itemsSource; + _itemTemplate = itemTemplate; + _container = container; + _mauiContext = mauiContext; + + if (itemHeight.HasValue) + { + _itemHeight = itemHeight.Value; + } + + if (itemWidth.HasValue) + { + _itemWidth = itemWidth.Value; + } + + if (itemSpacing.HasValue) + { + _itemSpacing = itemSpacing.Value; + } + + for (int index = 0; index < itemsSource.Count; index++) + { + // We're using this as a source for a ListViewBase, and we need INCC to work. So ListViewBase is going + // to iterate over the entire source list right off the bat, no matter what we do. Creating one + // ItemTemplateContext per item in the collection is unavoidable. Luckily, ITC is pretty cheap. + Add(new ItemTemplateContext2(itemTemplate, itemsSource[index]!, container, _itemHeight, _itemWidth, _itemSpacing, + false, false, _mauiContext)); + } + + _collectionChanged = InnerCollectionChanged; + if (itemsSource is INotifyCollectionChanged notifyCollectionChanged) + { + _proxy.Subscribe(notifyCollectionChanged, _collectionChanged); + } + + CollectionChanged += TemplateCollectionChanged; + } + + public void CleanUp() + { + CollectionChanged -= TemplateCollectionChanged; + _proxy.Unsubscribe(); + } + + void TemplateCollectionChanged(object? sender, NotifyCollectionChangedEventArgs args) + { + if (!_innerCollectionChange) + { + // When the template collection changes not as result of an inner collection change. + // The only time this happens is during a drag/drop item reorder (CanReorderItems). + // The ListView/GridView has notified us now we need to move those changes into the source. + // One might think it would be a "Move" event but it is actually a "Remove" followed by "Add". + _observeChanges = false; + + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + AddToSource(args); + break; + case NotifyCollectionChangedAction.Remove: + RemoveFromSource(args); + break; + default: + break; + } + + _observeChanges = true; + } + } + + void AddToSource(NotifyCollectionChangedEventArgs args) + { + if (args.NewItems is null) + { + return; + } + + var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : IndexOf((ItemTemplateContext2)args.NewItems[0]!); + + var count = args.NewItems.Count; + + for (int index = 0; index < count; index++) + { + var newItem = (ItemTemplateContext2?)args.NewItems[index]; + if (newItem != null) + _itemsSource.Insert(startIndex, newItem.Item); + } + } + + void RemoveFromSource(NotifyCollectionChangedEventArgs args) + { + if (args.OldItems is null) + { + return; + } + + var startIndex = args.OldStartingIndex; + + if (startIndex < 0) + { + // INCC implementation isn't giving us enough information to know where the removed items were in the + return; + } + + var count = args.OldItems.Count; + + for (int index = startIndex + count - 1; index >= startIndex; index--) + { + _itemsSource.RemoveAt(index); + } + } + + void InnerCollectionChanged(object? sender, NotifyCollectionChangedEventArgs args) + { + if (!_observeChanges) + { + return; + } + + _container.Dispatcher.DispatchIfRequired(() => InnerCollectionChanged(args)); + } + + void InnerCollectionChanged(NotifyCollectionChangedEventArgs args) + { + _innerCollectionChange = true; + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + Add(args); + break; + case NotifyCollectionChangedAction.Move: + Move(args); + break; + case NotifyCollectionChangedAction.Remove: + Remove(args); + break; + case NotifyCollectionChangedAction.Replace: + Replace(args); + break; + case NotifyCollectionChangedAction.Reset: + Reset(); + break; + default: + throw new ArgumentOutOfRangeException(); + } + _innerCollectionChange = false; + } + + void Add(NotifyCollectionChangedEventArgs args) + { + if (args.NewItems is null) + { + return; + } + + var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _itemsSource.IndexOf(args.NewItems[0]); + + var count = args.NewItems.Count; + + for (int index = 0; index < count; index++) + { + Insert(startIndex, new ItemTemplateContext2(_itemTemplate, args.NewItems[index]!, _container, _itemHeight, _itemWidth, _itemSpacing, + false, false, _mauiContext)); + } + } + + void Move(NotifyCollectionChangedEventArgs args) + { + if (args.NewItems is null) + { + return; + } + + var count = args.NewItems.Count; + if (args.OldStartingIndex > args.NewStartingIndex) + { + for (int index = 0; index < count; index++) + { + Move(args.OldStartingIndex + index, args.NewStartingIndex + index); + } + + return; + } + + for (int index = count - 1; index >= 0; index--) + { + Move(args.OldStartingIndex + index, args.NewStartingIndex + index); + } + } + + void Remove(NotifyCollectionChangedEventArgs args) + { + if (args.OldItems is null) + { + return; + } + + var startIndex = args.OldStartingIndex; + + if (startIndex < 0) + { + // INCC implementation isn't giving us enough information to know where the removed items were in the + // collection. So the best we can do is a full Reset. + Reset(); + return; + } + + var count = args.OldItems.Count; + + for (int index = startIndex + count - 1; index >= startIndex; index--) + { + RemoveAt(index); + } + } + + void Replace(NotifyCollectionChangedEventArgs args) + { + if (args.OldItems == null || args.NewItems == null) + { + return; + } + + var newItemCount = args.NewItems.Count; + + if (newItemCount == args.OldItems.Count) + { + for (int index = 0; index < newItemCount; index++) + { + var itemIndex = args.OldStartingIndex + index; + var oldItem = this[itemIndex]; + var newItem = new ItemTemplateContext2(_itemTemplate, args.NewItems[index]!, _container, _itemHeight, _itemWidth, _itemSpacing, + false, false, _mauiContext); + Items[itemIndex] = newItem; + var update = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItem, oldItem, itemIndex); + OnCollectionChanged(update); + } + } + else + { + // If we're replacing one set with an equal size set, we can do a soft reset; if not, we have to completely + // rebuild the collection + Reset(); + } + } + + void Reset() + { + Items.Clear(); + foreach (var item in _itemsSource) + { + Items.Add(new ItemTemplateContext2(_itemTemplate, item, _container, _itemHeight, _itemWidth, _itemSpacing, + false, false, _mauiContext)); + } + + var reset = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + OnCollectionChanged(reset); + } + } +} \ No newline at end of file diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/TemplatedItemSourceFactory2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/TemplatedItemSourceFactory2.cs new file mode 100644 index 000000000000..1eabd82c930a --- /dev/null +++ b/src/Controls/src/Core/Handlers/Items2/Windows/TemplatedItemSourceFactory2.cs @@ -0,0 +1,28 @@ +using System.Collections; +using System.Collections.Specialized; + +namespace Microsoft.Maui.Controls.Handlers.Items2 +{ + internal static class TemplatedItemSourceFactory2 + { + internal static object Create(IEnumerable itemsSource, DataTemplate itemTemplate, BindableObject container, + double? itemHeight = null, double? itemWidth = null, Thickness? itemSpacing = null, IMauiContext? mauiContext = null) + { + switch (itemsSource) + { + case IList observable when itemsSource is INotifyCollectionChanged: + return new ObservableItemTemplateCollection2(observable, itemTemplate, container, itemHeight, itemWidth, itemSpacing, mauiContext); + case IList list: + return new ItemTemplateContextList2(list, itemTemplate, container, itemHeight, itemWidth, itemSpacing, mauiContext); + } + + return new ItemTemplateContextEnumerable2(itemsSource, itemTemplate, container, itemHeight, itemWidth, itemSpacing, mauiContext); + } + + internal static object CreateGrouped(IEnumerable itemsSource, DataTemplate itemTemplate, + DataTemplate groupHeaderTemplate, DataTemplate groupFooterTemplate, BindableObject container, IMauiContext? mauiContext = null) + { + return new GroupedItemTemplateCollection2(itemsSource, itemTemplate, groupHeaderTemplate, groupFooterTemplate, container, mauiContext); + } + } +} diff --git a/src/Controls/src/Core/Platform/Windows/CollectionView/ItemsViewStyles.xaml b/src/Controls/src/Core/Platform/Windows/CollectionView/ItemsViewStyles.xaml index fabf34302705..bfe057e832b6 100644 --- a/src/Controls/src/Core/Platform/Windows/CollectionView/ItemsViewStyles.xaml +++ b/src/Controls/src/Core/Platform/Windows/CollectionView/ItemsViewStyles.xaml @@ -1,7 +1,9 @@ + xmlns:local="using:Microsoft.Maui.Controls.Platform" + xmlns:controls="using:Microsoft.UI.Xaml.Controls" + xmlns:local2="using:Microsoft.Maui.Controls.Handlers.Items2"> @@ -258,8 +260,45 @@ - - + + + + + + + + + + + + + + + + diff --git a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt index 7dc5c58110bf..d9f3e6b9e54a 100644 --- a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -1 +1,39 @@ #nullable enable +abstract Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.Layout.get -> Microsoft.Maui.Controls.IItemsLayout! +Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2 +Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.CollectionViewHandler2() -> void +Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2 +Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.Element.get -> TItemsView! +Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.ItemsView.get -> TItemsView! +Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.ItemsViewHandler2() -> void +Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.ItemsViewHandler2(Microsoft.Maui.PropertyMapper? mapper = null) -> void +Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.UpdateItemsLayout() -> void +override Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.ConnectHandler(Microsoft.UI.Xaml.Controls.ItemsView! platformView) -> void +override Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.DisconnectHandler(Microsoft.UI.Xaml.Controls.ItemsView! platformView) -> void +override Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.Layout.get -> Microsoft.Maui.Controls.IItemsLayout! +override Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.UpdateItemsSource() -> void +override Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.ConnectHandler(Microsoft.UI.Xaml.Controls.ItemsView! platformView) -> void +override Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.CreatePlatformView() -> Microsoft.UI.Xaml.Controls.ItemsView! +override Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.DisconnectHandler(Microsoft.UI.Xaml.Controls.ItemsView! platformView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapCanReorderItems(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.ReorderableItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapIsGrouped(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.GroupableItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapItemsSource(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.SelectableItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapSelectedItem(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.SelectableItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapSelectedItems(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.SelectableItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapSelectionMode(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.SelectableItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.ItemsViewMapper -> Microsoft.Maui.PropertyMapper!>! +static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapEmptyView(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapHorizontalScrollBarVisibility(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapItemsSource(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapItemTemplate(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapVerticalScrollBarVisibility(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapEmptyViewTemplate(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapFlowDirection(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapIsVisible(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapItemsUpdatingScrollMode(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapItemsLayout(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapHeader(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapHeaderTemplate(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapFooter(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapFooterTemplate(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void +virtual Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.UpdateItemsSource() -> void From be0e5e42e79caa05b5075b01e7da9d50867c73c1 Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Fri, 2 Jan 2026 12:02:58 +0530 Subject: [PATCH 002/109] Grouping and Selection fixes --- .../Items2/ItemsViewHandler2.Windows.cs | 40 ++++++++++++++++-- .../Windows/CollectionViewHandler2.Windows.cs | 42 +++++++++++++++++-- .../net-windows/PublicAPI.Unshipped.txt | 4 ++ 3 files changed, 79 insertions(+), 7 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index e772311abd89..f4a42ab7f992 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -2,6 +2,8 @@ using System.Collections; using System.Collections.Specialized; using System.ComponentModel; +using System.Collections.Generic; +using System.Linq; using Microsoft.Maui.Controls.Internals; using Microsoft.Maui.Controls.Platform; using Microsoft.Maui.Dispatching; @@ -15,6 +17,7 @@ using WScrollPresenter = Microsoft.UI.Xaml.Controls.Primitives.ScrollPresenter; using WVisibility = Microsoft.UI.Xaml.Visibility; + namespace Microsoft.Maui.Controls.Handlers.Items2 { public abstract class ItemsViewHandler2 : ViewHandler where TItemsView : ItemsView @@ -194,14 +197,20 @@ CollectionViewSource CreateCollectionViewSource() { Source = TemplatedItemSourceFactory2.CreateGrouped(itemsSource, itemTemplate, groupableItemsView.GroupHeaderTemplate, groupableItemsView.GroupFooterTemplate, - Element, mauiContext: MauiContext) + Element, mauiContext: MauiContext, IsSourceGrouped = false) }; } else { + var flattenedSource = itemsSource; + if (itemsSource is not null && IsItemsSourceGrouped(itemsSource)) + { + flattenedSource = FlattenGroupedItemsSource(itemsSource); + } + return new CollectionViewSource { - Source = TemplatedItemSourceFactory2.Create(itemsSource, itemTemplate, Element, mauiContext: MauiContext), + Source = TemplatedItemSourceFactory2.Create(flattenedSource, itemTemplate, Element, mauiContext: MauiContext), IsSourceGrouped = false }; } @@ -214,6 +223,29 @@ CollectionViewSource CreateCollectionViewSource() }; } + bool IsItemsSourceGrouped(object itemsSource) + { + if (itemsSource is IEnumerable enumerable) + { + foreach (var item in enumerable) + { + if (item is IEnumerable && item is not string) + { + return true; + } + break; + } + } + return false; + } + + IEnumerable FlattenGroupedItemsSource(IEnumerable groupedSource) + { + return groupedSource.Cast(). + Where(group => group is IEnumerable && group is not string). + SelectMany(group => ((IEnumerable)group).Cast()); + } + protected virtual void UpdateItemsSource() { if (PlatformView is null) @@ -317,7 +349,7 @@ MauiItemsView SelectListViewBase() Layout = CreateItemsLayout() }; - if (Layout is LinearItemsLayout listItemsLayout && + if (Layout is LinearItemsLayout listItemsLayout && listItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal) { ScrollViewer.SetHorizontalScrollMode(itemsView, UI.Xaml.Controls.ScrollMode.Enabled); @@ -886,7 +918,7 @@ void ScrollToRequested(object? sender, ScrollToRequestEventArgs args) int FindItemIndex(object item) { - if(_collectionViewSource is null) + if (_collectionViewSource is null) { return -1; } diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs index f817430b1942..2baadd5e0add 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs @@ -8,6 +8,28 @@ namespace Microsoft.Maui.Controls.Handlers.Items2; +public partial class CollectionViewHandler2 +{ + public CollectionViewHandler2() : base(Mapper) + { + } + + + public CollectionViewHandler2(PropertyMapper? mapper = null) : base(mapper ?? Mapper) + { + } + + public static PropertyMapper Mapper = new(ItemsViewMapper) + { + [ReorderableItemsView.CanReorderItemsProperty.PropertyName] = MapCanReorderItems, + [GroupableItemsView.IsGroupedProperty.PropertyName] = MapIsGrouped, + [GroupableItemsView.GroupHeaderTemplateProperty.PropertyName] = MapGroupHeaderTemplate, + [GroupableItemsView.GroupFooterTemplateProperty.PropertyName] = MapGroupFooterTemplate, + [SelectableItemsView.SelectedItemProperty.PropertyName] = MapSelectedItem, + [SelectableItemsView.SelectedItemsProperty.PropertyName] = MapSelectedItems, + [SelectableItemsView.SelectionModeProperty.PropertyName] = MapSelectionMode, + }; +} public partial class CollectionViewHandler2 : ItemsViewHandler2 { bool _ignorePlatformSelectionChange; @@ -23,6 +45,17 @@ public static void MapIsGrouped(CollectionViewHandler2 handler, GroupableItemsVi handler.UpdateItemsSource(); } + public static void MapGroupHeaderTemplate(CollectionViewHandler2 handler, GroupableItemsView itemsView) + { + handler.UpdateItemsSource(); + } + + public static void MapGroupFooterTemplate(CollectionViewHandler2 handler, GroupableItemsView itemsView) + { + handler.UpdateItemsSource(); + } + + public static void MapItemsSource(CollectionViewHandler2 handler, SelectableItemsView itemsView) { ItemsViewHandler2.MapItemsSource(handler, itemsView); @@ -67,8 +100,8 @@ protected override void DisconnectHandler(WItemsView platformView) if (oldListViewBase is not null) { - oldListViewBase.ClearValue(WItemsView.SelectionModeProperty); oldListViewBase.SelectionChanged -= PlatformSelectionChanged; + oldListViewBase.ClearValue(WItemsView.SelectionModeProperty); } if (ItemsView is not null) @@ -96,12 +129,15 @@ void VirtualSelectionChanged(object? sender, SelectionChangedEventArgs? e) void PlatformSelectionChanged(WItemsView sender, ItemsViewSelectionChangedEventArgs args) { + if (PlatformView is null) + return; + UpdateVirtualSelection(); } void UpdateVirtualSelection() { - if (_ignorePlatformSelectionChange || ItemsView is null) + if (_ignorePlatformSelectionChange || ItemsView is null || PlatformView is null) { return; } @@ -223,7 +259,7 @@ void UpdatePlatformSelection() index++; } break; - case ItemsViewSelectionMode.None: + case ItemsViewSelectionMode.None: case ItemsViewSelectionMode.Extended: default: break; diff --git a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt index d9f3e6b9e54a..8fc4acbd8ff9 100644 --- a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -2,6 +2,7 @@ abstract Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.Layout.get -> Microsoft.Maui.Controls.IItemsLayout! Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2 Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.CollectionViewHandler2() -> void +Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.CollectionViewHandler2(Microsoft.Maui.PropertyMapper? mapper = null) -> void Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2 Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.Element.get -> TItemsView! Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.ItemsView.get -> TItemsView! @@ -16,8 +17,11 @@ override Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.C override Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.CreatePlatformView() -> Microsoft.UI.Xaml.Controls.ItemsView! override Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.DisconnectHandler(Microsoft.UI.Xaml.Controls.ItemsView! platformView) -> void static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapCanReorderItems(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.ReorderableItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapGroupFooterTemplate(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.GroupableItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapGroupHeaderTemplate(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.GroupableItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapIsGrouped(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.GroupableItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapItemsSource(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.SelectableItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.Mapper -> Microsoft.Maui.PropertyMapper! static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapSelectedItem(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.SelectableItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapSelectedItems(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.SelectableItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapSelectionMode(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.SelectableItemsView! itemsView) -> void From 1d00e77e412cc47329a9949b5300fca1ab6e2e20 Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Fri, 2 Jan 2026 12:48:06 +0530 Subject: [PATCH 003/109] Update ItemsViewHandler2.Windows.cs --- .../src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index f4a42ab7f992..0f893003a095 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -197,7 +197,7 @@ CollectionViewSource CreateCollectionViewSource() { Source = TemplatedItemSourceFactory2.CreateGrouped(itemsSource, itemTemplate, groupableItemsView.GroupHeaderTemplate, groupableItemsView.GroupFooterTemplate, - Element, mauiContext: MauiContext, IsSourceGrouped = false) + Element, mauiContext: MauiContext),IsSourceGrouped= false }; } else From 9b8381569e70d19299691f56b88433133b09e2c3 Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Mon, 5 Jan 2026 12:33:20 +0530 Subject: [PATCH 004/109] Header/Footer Fix --- .../Items2/ItemsViewHandler2.Windows.cs | 91 +++++++++++++------ .../Handlers/Items2/Windows/MauiItemsView.cs | 4 +- 2 files changed, 66 insertions(+), 29 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index 0f893003a095..bbd7298c2c67 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -594,8 +594,10 @@ void UpdateHeader() } var header = structuredItemsView.Header; + var headerTemplate = structuredItemsView.HeaderTemplate; - if (header is null) + // Hide header only if both Header and HeaderTemplate are null + if (header is null && headerTemplate is null) { if (_headerDisplayed) { @@ -613,26 +615,42 @@ void UpdateHeader() return; } - _header = header switch + // If HeaderTemplate is set, use it regardless of header value + if (headerTemplate is not null) { - string text => new TextBlock + _header = RealizeHeaderFooterTemplate(header, headerTemplate, ref _mauiHeader); + } + else if (header is not null) + { + _header = header switch { - Text = text, - Margin = new Microsoft.UI.Xaml.Thickness(0, 0, 0, 10) - }, - - View view => RealizeHeaderFooterView(view, ref _mauiHeader), - _ => RealizeHeaderFooterTemplate(header, structuredItemsView.HeaderTemplate, ref _mauiHeader), - }; + string text => new TextBlock + { + Text = text, + Margin = new Microsoft.UI.Xaml.Thickness(0, 0, 0, 10) + }, + View view => RealizeHeaderFooterView(view, ref _mauiHeader), + _ => new TextBlock + { + Text = header.ToString(), + Margin = new Microsoft.UI.Xaml.Thickness(0, 0, 0, 10) + } + }; + } + else + { + // This shouldn't happen due to null check above, but handle it safely + return; + } - if (PlatformView is MauiItemsView platformItemsView && _mauiHeader is not null) + if (PlatformView is MauiItemsView platformItemsView && _header is not null) { platformItemsView.SetHeader(_header, _mauiHeader); platformItemsView.HeaderVisibility = WVisibility.Visible; } - - // Add logical child if it's a View (not a template) - if (_mauiHeader is not null && structuredItemsView.HeaderTemplate is null) + + // Add logical child for View + if (_mauiHeader is not null) { ItemsView.AddLogicalChild(_mauiHeader); } @@ -648,8 +666,10 @@ void UpdateFooter() } var footer = structuredItemsView.Footer; + var footerTemplate = structuredItemsView.FooterTemplate; - if (footer is null) + // Hide footer only if both Footer and FooterTemplate are null + if (footer is null && footerTemplate is null) { if (_footerDisplayed) { @@ -667,25 +687,42 @@ void UpdateFooter() return; } - _footer = footer switch + // If FooterTemplate is set, use it regardless of footer value + if (footerTemplate is not null) { - string text => new TextBlock + _footer = RealizeHeaderFooterTemplate(footer, footerTemplate, ref _mauiFooter); + } + else if (footer is not null) + { + _footer = footer switch { - Text = text, - Margin = new Microsoft.UI.Xaml.Thickness(0, 10, 0, 0) - }, - View view => RealizeHeaderFooterView(view, ref _mauiFooter), - _ => RealizeHeaderFooterTemplate(footer, structuredItemsView.FooterTemplate, ref _mauiFooter), - }; + string text => new TextBlock + { + Text = text, + Margin = new Microsoft.UI.Xaml.Thickness(0, 10, 0, 0) + }, + View view => RealizeHeaderFooterView(view, ref _mauiFooter), + _ => new TextBlock + { + Text = footer.ToString(), + Margin = new Microsoft.UI.Xaml.Thickness(0, 10, 0, 0) + } + }; + } + else + { + // This shouldn't happen due to null check above, but handle it safely + return; + } - if (PlatformView is MauiItemsView platformItemsView && _mauiFooter is not null) + if (PlatformView is MauiItemsView platformItemsView && _footer is not null) { platformItemsView.SetFooter(_footer, _mauiFooter); platformItemsView.FooterVisibility = WVisibility.Visible; } - - // Add logical child if it's a View (not a template) - if (_mauiFooter is not null && structuredItemsView.FooterTemplate is null) + + // Add logical child for View + if (_mauiFooter is not null) { ItemsView.AddLogicalChild(_mauiFooter); } diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs b/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs index 3845670933d0..98f1ed6bf3c9 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs @@ -106,7 +106,7 @@ public void SetEmptyView(FrameworkElement emptyView, View mauiEmptyView) } } - public void SetHeader(FrameworkElement header, View mauiHeader) + public void SetHeader(FrameworkElement header, View? mauiHeader) { _header = header; _mauiHeader = mauiHeader; @@ -118,7 +118,7 @@ public void SetHeader(FrameworkElement header, View mauiHeader) } } - public void SetFooter(FrameworkElement footer, View mauiFooter) + public void SetFooter(FrameworkElement footer, View? mauiFooter) { _footer = footer; _mauiFooter = mauiFooter; From a9491eaeac260bbe837d3f0eb29fb55e7eabfd38 Mon Sep 17 00:00:00 2001 From: Karthik Raja Date: Mon, 5 Jan 2026 16:12:21 +0530 Subject: [PATCH 005/109] WinUI CV2 extension class included --- .../Items/ItemsViewExtensions.Windows.cs | 134 ++++++++++++++++ .../Items/ItemsViewHandler.Windows.cs | 53 ++----- .../Items2/ItemsViewHandler2.Windows.cs | 147 +++--------------- 3 files changed, 169 insertions(+), 165 deletions(-) create mode 100644 src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs diff --git a/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs b/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs new file mode 100644 index 000000000000..7690dfce768d --- /dev/null +++ b/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs @@ -0,0 +1,134 @@ +using System; +using Microsoft.Maui.Controls.Internals; +using Microsoft.Maui.Controls.Platform; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using WASDKScrollBarVisibility = Microsoft.UI.Xaml.Controls.ScrollBarVisibility; + +namespace Microsoft.Maui.Controls.Handlers.Items +{ + internal static class ItemsViewExtensions + { + internal static FrameworkElement RealizeEmptyViewTemplate(object bindingContext, DataTemplate? emptyViewTemplate, IMauiContext mauiContext, ref View? mauiEmptyView) + { + if (emptyViewTemplate is null) + { + return new TextBlock + { + HorizontalAlignment = Microsoft.UI.Xaml.HorizontalAlignment.Center, + VerticalAlignment = Microsoft.UI.Xaml.VerticalAlignment.Center, + Text = bindingContext?.ToString() ?? string.Empty + }; + } + + var template = emptyViewTemplate.SelectDataTemplate(bindingContext, null); + var view = template.CreateContent() as View; + if (view is not null) + { + view.BindingContext = bindingContext; + } + + return RealizeEmptyView(view, mauiContext, ref mauiEmptyView); + } + + internal static FrameworkElement RealizeEmptyView(View? view, IMauiContext mauiContext, ref View? mauiEmptyView) + { + mauiEmptyView = view ?? throw new ArgumentNullException(nameof(view)); + + var handler = view.ToHandler(mauiContext); + var platformView = handler.ContainerView ?? handler.PlatformView; + + return platformView as FrameworkElement ?? throw new InvalidOperationException("Unable to convert view to FrameworkElement"); + } + + internal static FrameworkElement RealizeHeaderFooterTemplate(object bindingContext, DataTemplate? template, IMauiContext mauiContext, ref View? mauiView) + { + if (template is null) + { + return new TextBlock + { + Text = bindingContext?.ToString() ?? string.Empty, + Margin = new Microsoft.UI.Xaml.Thickness(0, 5, 0, 5) + }; + } + + var dataTemplate = template.SelectDataTemplate(bindingContext, null); + var view = dataTemplate.CreateContent() as View; + if (view is not null) + { + view.BindingContext = bindingContext; + } + + return RealizeHeaderFooterView(view, mauiContext, ref mauiView); + } + + internal static FrameworkElement RealizeHeaderFooterView(View? view, IMauiContext mauiContext, ref View? mauiView) + { + mauiView = view ?? throw new ArgumentNullException(nameof(view)); + + var handler = view.ToHandler(mauiContext); + var platformView = handler.ContainerView ?? handler.PlatformView; + + return platformView as FrameworkElement ?? throw new InvalidOperationException("Unable to convert view to FrameworkElement"); + } + + internal static void UpdateVerticalScrollBarVisibility( + UIElement control, + ScrollBarVisibility scrollBarVisibility, + ref WASDKScrollBarVisibility? defaultVerticalScrollVisibility) + { + if (scrollBarVisibility != ScrollBarVisibility.Default) + { + // If the value is changing to anything other than the default, record the default + if (defaultVerticalScrollVisibility is null) + { + defaultVerticalScrollVisibility = ScrollViewer.GetVerticalScrollBarVisibility(control); + } + } + + if (defaultVerticalScrollVisibility is null) + { + // If the default has never been recorded, then this has never been set to anything but the + // default value; there's nothing to do. + return; + } + + switch (scrollBarVisibility) + { + case ScrollBarVisibility.Always: + ScrollViewer.SetVerticalScrollBarVisibility(control, WASDKScrollBarVisibility.Visible); + break; + case ScrollBarVisibility.Never: + ScrollViewer.SetVerticalScrollBarVisibility(control, WASDKScrollBarVisibility.Hidden); + break; + case ScrollBarVisibility.Default: + ScrollViewer.SetVerticalScrollBarVisibility(control, defaultVerticalScrollVisibility.Value); + break; + } + } + + internal static void UpdateHorizontalScrollBarVisibility( + UIElement control, + ScrollBarVisibility scrollBarVisibility, + ref WASDKScrollBarVisibility? defaultHorizontalScrollVisibility) + { + if (defaultHorizontalScrollVisibility is null) + { + defaultHorizontalScrollVisibility = ScrollViewer.GetHorizontalScrollBarVisibility(control); + } + + switch (scrollBarVisibility) + { + case ScrollBarVisibility.Always: + ScrollViewer.SetHorizontalScrollBarVisibility(control, WASDKScrollBarVisibility.Visible); + break; + case ScrollBarVisibility.Never: + ScrollViewer.SetHorizontalScrollBarVisibility(control, WASDKScrollBarVisibility.Hidden); + break; + case ScrollBarVisibility.Default: + ScrollViewer.SetHorizontalScrollBarVisibility(control, defaultHorizontalScrollVisibility.Value); + break; + } + } + } +} diff --git a/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.Windows.cs b/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.Windows.cs index f1d75f9cba54..40d1b855f959 100644 --- a/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.Windows.cs @@ -328,10 +328,10 @@ protected virtual void UpdateEmptyView() }; break; case View view: - _emptyView = RealizeEmptyView(view); + _emptyView = ItemsViewExtensions.RealizeEmptyView(view, MauiContext, ref _formsEmptyView); break; default: - _emptyView = RealizeEmptyViewTemplate(emptyView, Element.EmptyViewTemplate); + _emptyView = ItemsViewExtensions.RealizeEmptyViewTemplate(emptyView, Element.EmptyViewTemplate, MauiContext, ref _formsEmptyView); break; } @@ -383,51 +383,18 @@ internal void UpdateScrollBarVisibility() void UpdateVerticalScrollBarVisibility() { - if (Element.VerticalScrollBarVisibility != ScrollBarVisibility.Default) - { - // If the value is changing to anything other than the default, record the default - if (_defaultVerticalScrollVisibility == null) - _defaultVerticalScrollVisibility = ScrollViewer.GetVerticalScrollBarVisibility(Control); - } - - if (_defaultVerticalScrollVisibility == null) - { - // If the default has never been recorded, then this has never been set to anything but the - // default value; there's nothing to do. - return; - } - - switch (Element.VerticalScrollBarVisibility) - { - case (ScrollBarVisibility.Always): - ScrollViewer.SetVerticalScrollBarVisibility(Control, WASDKScrollBarVisibility.Visible); - break; - case (ScrollBarVisibility.Never): - ScrollViewer.SetVerticalScrollBarVisibility(Control, WASDKScrollBarVisibility.Hidden); - break; - case (ScrollBarVisibility.Default): - ScrollViewer.SetVerticalScrollBarVisibility(Control, _defaultVerticalScrollVisibility.Value); - break; - } + ItemsViewExtensions.UpdateVerticalScrollBarVisibility( + Control, + Element.VerticalScrollBarVisibility, + ref _defaultVerticalScrollVisibility); } void UpdateHorizontalScrollBarVisibility() { - if (_defaultHorizontalScrollVisibility == null) - _defaultHorizontalScrollVisibility = ScrollViewer.GetHorizontalScrollBarVisibility(Control); - - switch (Element.HorizontalScrollBarVisibility) - { - case (ScrollBarVisibility.Always): - ScrollViewer.SetHorizontalScrollBarVisibility(Control, WASDKScrollBarVisibility.Visible); - break; - case (ScrollBarVisibility.Never): - ScrollViewer.SetHorizontalScrollBarVisibility(Control, WASDKScrollBarVisibility.Hidden); - break; - case (ScrollBarVisibility.Default): - ScrollViewer.SetHorizontalScrollBarVisibility(Control, _defaultHorizontalScrollVisibility.Value); - break; - } + ItemsViewExtensions.UpdateHorizontalScrollBarVisibility( + Control, + Element.HorizontalScrollBarVisibility, + ref _defaultHorizontalScrollVisibility); } protected virtual void OnScrollViewerFound(ScrollViewer scrollViewer) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index bbd7298c2c67..7e378e315f5d 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -126,22 +126,22 @@ public static void MapItemsLayout(ItemsViewHandler2 handler, ItemsVi { handler.UpdateItemsLayout(); } - + public static void MapHeader(ItemsViewHandler2 handler, ItemsView itemsView) { handler.UpdateHeader(); } - + public static void MapHeaderTemplate(ItemsViewHandler2 handler, ItemsView itemsView) { handler.UpdateHeader(); } - + public static void MapFooter(ItemsViewHandler2 handler, ItemsView itemsView) { handler.UpdateFooter(); } - + public static void MapFooterTemplate(ItemsViewHandler2 handler, ItemsView itemsView) { handler.UpdateFooter(); @@ -150,7 +150,7 @@ public static void MapFooterTemplate(ItemsViewHandler2 handler, Item protected override WItemsView CreatePlatformView() { var itemsView = SelectListViewBase(); - + return itemsView; } @@ -197,7 +197,8 @@ CollectionViewSource CreateCollectionViewSource() { Source = TemplatedItemSourceFactory2.CreateGrouped(itemsSource, itemTemplate, groupableItemsView.GroupHeaderTemplate, groupableItemsView.GroupFooterTemplate, - Element, mauiContext: MauiContext),IsSourceGrouped= false + Element, mauiContext: MauiContext), + IsSourceGrouped = false }; } else @@ -255,7 +256,7 @@ protected virtual void UpdateItemsSource() CleanUpCollectionViewSource(); - + _collectionViewSource = CreateCollectionViewSource(); _itemsSource = _collectionViewSource?.Source as IList; @@ -357,7 +358,7 @@ MauiItemsView SelectListViewBase() ScrollViewer.SetVerticalScrollMode(itemsView, UI.Xaml.Controls.ScrollMode.Disabled); ScrollViewer.SetVerticalScrollBarVisibility(itemsView, WASDKScrollBarVisibility.Disabled); - + // Set header/footer to horizontal layout itemsView.SetLayoutOrientation(true); } @@ -379,7 +380,7 @@ protected void UpdateItemsLayout() UpdateItemsSource(); PlatformView.Layout = CreateItemsLayout(); - + // Update header/footer orientation if (PlatformView is MauiItemsView mauiItemsView && Layout is LinearItemsLayout linearLayout) { @@ -515,7 +516,7 @@ void UpdateEmptyView() if (emptyViewTemplate is not null) { // If EmptyViewTemplate is provided, use it instead of EmptyView - _emptyView = RealizeEmptyViewTemplate(emptyView, emptyViewTemplate); + _emptyView = ItemsViewExtensions.RealizeEmptyViewTemplate(emptyView, emptyViewTemplate, MauiContext!, ref _mauiEmptyView); } else if (emptyView is not null) { @@ -530,10 +531,10 @@ void UpdateEmptyView() }; break; case View view: - _emptyView = RealizeEmptyView(view); + _emptyView = ItemsViewExtensions.RealizeEmptyView(view, MauiContext!, ref _mauiEmptyView); break; default: - _emptyView = RealizeEmptyViewTemplate(emptyView, null); // Fallback + _emptyView = ItemsViewExtensions.RealizeEmptyViewTemplate(emptyView, null, MauiContext!, ref _mauiEmptyView); break; } } @@ -585,7 +586,7 @@ void UpdateEmptyViewVisibility() _emptyViewDisplayed = false; } } - + void UpdateHeader() { if (Element is not StructuredItemsView structuredItemsView || PlatformView is null) @@ -605,7 +606,7 @@ void UpdateHeader() { mauiItemsView.HeaderVisibility = WVisibility.Collapsed; } - + if (_mauiHeader is not null) { ItemsView.RemoveLogicalChild(_mauiHeader); @@ -657,7 +658,7 @@ void UpdateHeader() _headerDisplayed = true; } - + void UpdateFooter() { if (Element is not StructuredItemsView structuredItemsView || PlatformView is null) @@ -677,7 +678,7 @@ void UpdateFooter() { mauiItemsView.FooterVisibility = WVisibility.Collapsed; } - + if (_mauiFooter is not null) { ItemsView.RemoveLogicalChild(_mauiFooter); @@ -730,118 +731,20 @@ void UpdateFooter() _footerDisplayed = true; } -#nullable disable - FrameworkElement RealizeEmptyViewTemplate(object bindingContext, DataTemplate emptyViewTemplate) - { - if (emptyViewTemplate is null) - { - return new TextBlock - { - HorizontalAlignment = Microsoft.UI.Xaml.HorizontalAlignment.Center, - VerticalAlignment = Microsoft.UI.Xaml.VerticalAlignment.Center, - Text = bindingContext.ToString() - }; - } - - var template = emptyViewTemplate.SelectDataTemplate(bindingContext, null); - var view = template.CreateContent() as View; - view.BindingContext = bindingContext; - - return RealizeEmptyView(view); - } - - FrameworkElement RealizeEmptyView(View view) - { - _mauiEmptyView = view ?? throw new ArgumentNullException(nameof(view)); - - if (MauiContext is null) - throw new InvalidOperationException("MauiContext cannot be null when creating a handler."); - - var handler = view.ToHandler(MauiContext); - var platformView = handler.ContainerView ?? handler.PlatformView; - return platformView; - } - - FrameworkElement RealizeHeaderFooterTemplate(object bindingContext, DataTemplate template, ref View mauiView) - { - if (template is null) - { - return new TextBlock - { - Text = bindingContext.ToString(), - Margin = new Microsoft.UI.Xaml.Thickness(0, 5, 0, 5) - }; - } - - var dataTemplate = template.SelectDataTemplate(bindingContext, null); - var view = dataTemplate.CreateContent() as View; - view.BindingContext = bindingContext; - - return RealizeHeaderFooterView(view, ref mauiView); - } - - FrameworkElement RealizeHeaderFooterView(View view, ref View mauiView) - { - mauiView = view ?? throw new ArgumentNullException(nameof(view)); - - var handler = view.ToHandler(MauiContext); - var platformView = handler.ContainerView ?? handler.PlatformView; - - return platformView; - } -#nullable enable - void UpdateVerticalScrollBarVisibility() { - if (Element.VerticalScrollBarVisibility != ScrollBarVisibility.Default) - { - // If the value is changing to anything other than the default, record the default - if (_defaultVerticalScrollVisibility is null) - { - ScrollViewer.SetVerticalScrollBarVisibility(PlatformView, WASDKScrollBarVisibility.Visible); - } - } - - if (_defaultVerticalScrollVisibility is null) - { - // If the default has never been recorded, then this has never been set to anything but the - // default value; there's nothing to do. - return; - } - - switch (Element.VerticalScrollBarVisibility) - { - case (ScrollBarVisibility.Always): - ScrollViewer.SetVerticalScrollBarVisibility(PlatformView, WASDKScrollBarVisibility.Visible); - break; - case (ScrollBarVisibility.Never): - ScrollViewer.SetVerticalScrollBarVisibility(PlatformView, WASDKScrollBarVisibility.Hidden); - break; - case (ScrollBarVisibility.Default): - ScrollViewer.SetVerticalScrollBarVisibility(PlatformView, _defaultVerticalScrollVisibility.Value); - break; - } + ItemsViewExtensions.UpdateVerticalScrollBarVisibility( + PlatformView, + Element.VerticalScrollBarVisibility, + ref _defaultVerticalScrollVisibility); } void UpdateHorizontalScrollBarVisibility() { - if (_defaultHorizontalScrollVisibility is null) - { - _defaultHorizontalScrollVisibility = ScrollViewer.GetHorizontalScrollBarVisibility(PlatformView); - } - - switch (Element.HorizontalScrollBarVisibility) - { - case (ScrollBarVisibility.Always): - ScrollViewer.SetHorizontalScrollBarVisibility(PlatformView, WASDKScrollBarVisibility.Visible); - break; - case (ScrollBarVisibility.Never): - ScrollViewer.SetHorizontalScrollBarVisibility(PlatformView, WASDKScrollBarVisibility.Hidden); - break; - case (ScrollBarVisibility.Default): - ScrollViewer.SetHorizontalScrollBarVisibility(PlatformView, _defaultHorizontalScrollVisibility.Value); - break; - } + ItemsViewExtensions.UpdateHorizontalScrollBarVisibility( + PlatformView, + Element.HorizontalScrollBarVisibility, + ref _defaultHorizontalScrollVisibility); } void PointerScrollChanged(object sender, PointerRoutedEventArgs e) From 3593fcaab07518405653c0e0391e463bf1d9a95d Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Fri, 2 Jan 2026 12:02:58 +0530 Subject: [PATCH 006/109] Grouping and Selection fixes --- .../src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index 7e378e315f5d..4d70e9932349 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Specialized; using System.ComponentModel; +using Microsoft.Maui.Controls.Handlers.Items; using System.Collections.Generic; using System.Linq; using Microsoft.Maui.Controls.Internals; @@ -197,8 +198,7 @@ CollectionViewSource CreateCollectionViewSource() { Source = TemplatedItemSourceFactory2.CreateGrouped(itemsSource, itemTemplate, groupableItemsView.GroupHeaderTemplate, groupableItemsView.GroupFooterTemplate, - Element, mauiContext: MauiContext), - IsSourceGrouped = false + Element, mauiContext: MauiContext, IsSourceGrouped = false) }; } else From 69fff2a1f1ac6ee42e2edcd0861c26bfb0723ec4 Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Fri, 2 Jan 2026 12:48:06 +0530 Subject: [PATCH 007/109] Update ItemsViewHandler2.Windows.cs --- .../src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index 4d70e9932349..4433621d0dae 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -198,7 +198,7 @@ CollectionViewSource CreateCollectionViewSource() { Source = TemplatedItemSourceFactory2.CreateGrouped(itemsSource, itemTemplate, groupableItemsView.GroupHeaderTemplate, groupableItemsView.GroupFooterTemplate, - Element, mauiContext: MauiContext, IsSourceGrouped = false) + Element, mauiContext: MauiContext),IsSourceGrouped= false }; } else From 9a0aa917f9e15d47eb6ee3cd22e46732939a07c9 Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Mon, 5 Jan 2026 12:33:20 +0530 Subject: [PATCH 008/109] Header/Footer Fix --- .../Items2/ItemsViewHandler2.Windows.cs | 41 +++---------------- 1 file changed, 5 insertions(+), 36 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index 4433621d0dae..1b97fdb9bccd 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -1,10 +1,9 @@ -using System; + +using System; using System.Collections; using System.Collections.Specialized; using System.ComponentModel; using Microsoft.Maui.Controls.Handlers.Items; -using System.Collections.Generic; -using System.Linq; using Microsoft.Maui.Controls.Internals; using Microsoft.Maui.Controls.Platform; using Microsoft.Maui.Dispatching; @@ -18,7 +17,6 @@ using WScrollPresenter = Microsoft.UI.Xaml.Controls.Primitives.ScrollPresenter; using WVisibility = Microsoft.UI.Xaml.Visibility; - namespace Microsoft.Maui.Controls.Handlers.Items2 { public abstract class ItemsViewHandler2 : ViewHandler where TItemsView : ItemsView @@ -198,20 +196,14 @@ CollectionViewSource CreateCollectionViewSource() { Source = TemplatedItemSourceFactory2.CreateGrouped(itemsSource, itemTemplate, groupableItemsView.GroupHeaderTemplate, groupableItemsView.GroupFooterTemplate, - Element, mauiContext: MauiContext),IsSourceGrouped= false + Element, mauiContext: MauiContext) }; } else { - var flattenedSource = itemsSource; - if (itemsSource is not null && IsItemsSourceGrouped(itemsSource)) - { - flattenedSource = FlattenGroupedItemsSource(itemsSource); - } - return new CollectionViewSource { - Source = TemplatedItemSourceFactory2.Create(flattenedSource, itemTemplate, Element, mauiContext: MauiContext), + Source = TemplatedItemSourceFactory2.Create(itemsSource, itemTemplate, Element, mauiContext: MauiContext), IsSourceGrouped = false }; } @@ -224,29 +216,6 @@ CollectionViewSource CreateCollectionViewSource() }; } - bool IsItemsSourceGrouped(object itemsSource) - { - if (itemsSource is IEnumerable enumerable) - { - foreach (var item in enumerable) - { - if (item is IEnumerable && item is not string) - { - return true; - } - break; - } - } - return false; - } - - IEnumerable FlattenGroupedItemsSource(IEnumerable groupedSource) - { - return groupedSource.Cast(). - Where(group => group is IEnumerable && group is not string). - SelectMany(group => ((IEnumerable)group).Cast()); - } - protected virtual void UpdateItemsSource() { if (PlatformView is null) @@ -877,4 +846,4 @@ int FindItemIndex(object item) return -1; } } -} \ No newline at end of file +} From 8cd66f480037de07d9228f9c08e7845f9f921394 Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Tue, 6 Jan 2026 11:55:08 +0530 Subject: [PATCH 009/109] Update ItemsViewHandler2.Windows.cs --- .../src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index 1b97fdb9bccd..69d5c13d676b 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -1,5 +1,4 @@ - -using System; +using System; using System.Collections; using System.Collections.Specialized; using System.ComponentModel; From dddac39cf4b97d7f40fc608b0e5ec99211cfca32 Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Tue, 6 Jan 2026 11:56:44 +0530 Subject: [PATCH 010/109] ScrollBar and RemainingThreshold fix --- .../Items/ItemsViewExtensions.Windows.cs | 142 ++++++++++++++---- .../Items2/ItemsViewHandler2.Windows.cs | 28 +++- 2 files changed, 133 insertions(+), 37 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs b/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs index 7690dfce768d..ddb469a8b9e4 100644 --- a/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs @@ -77,33 +77,71 @@ internal static void UpdateVerticalScrollBarVisibility( ScrollBarVisibility scrollBarVisibility, ref WASDKScrollBarVisibility? defaultVerticalScrollVisibility) { - if (scrollBarVisibility != ScrollBarVisibility.Default) + // Check if this is a ScrollView (CV2 - new ItemsView control) + if (control is Microsoft.UI.Xaml.Controls.ItemsView itemsView && itemsView.ScrollView is not null) { - // If the value is changing to anything other than the default, record the default + var scrollView = itemsView.ScrollView; + + if (scrollBarVisibility != ScrollBarVisibility.Default) + { + // If the value is changing to anything other than the default, record the default + if (defaultVerticalScrollVisibility is null) + { + defaultVerticalScrollVisibility = (WASDKScrollBarVisibility)scrollView.VerticalScrollBarVisibility; + } + } + if (defaultVerticalScrollVisibility is null) { - defaultVerticalScrollVisibility = ScrollViewer.GetVerticalScrollBarVisibility(control); + // If the default has never been recorded, then this has never been set to anything but the + // default value; there's nothing to do. + return; } - } - if (defaultVerticalScrollVisibility is null) - { - // If the default has never been recorded, then this has never been set to anything but the - // default value; there's nothing to do. - return; + switch (scrollBarVisibility) + { + case ScrollBarVisibility.Always: + scrollView.VerticalScrollBarVisibility = (ScrollingScrollBarVisibility)WASDKScrollBarVisibility.Visible; + break; + case ScrollBarVisibility.Never: + scrollView.VerticalScrollBarVisibility = (ScrollingScrollBarVisibility)WASDKScrollBarVisibility.Hidden; + break; + case ScrollBarVisibility.Default: + scrollView.VerticalScrollBarVisibility = (ScrollingScrollBarVisibility)defaultVerticalScrollVisibility.Value; + break; + } } - - switch (scrollBarVisibility) + else { - case ScrollBarVisibility.Always: - ScrollViewer.SetVerticalScrollBarVisibility(control, WASDKScrollBarVisibility.Visible); - break; - case ScrollBarVisibility.Never: - ScrollViewer.SetVerticalScrollBarVisibility(control, WASDKScrollBarVisibility.Hidden); - break; - case ScrollBarVisibility.Default: - ScrollViewer.SetVerticalScrollBarVisibility(control, defaultVerticalScrollVisibility.Value); - break; + // CV1 - Use ScrollViewer attached properties + if (scrollBarVisibility != ScrollBarVisibility.Default) + { + // If the value is changing to anything other than the default, record the default + if (defaultVerticalScrollVisibility is null) + { + defaultVerticalScrollVisibility = ScrollViewer.GetVerticalScrollBarVisibility(control); + } + } + + if (defaultVerticalScrollVisibility is null) + { + // If the default has never been recorded, then this has never been set to anything but the + // default value; there's nothing to do. + return; + } + + switch (scrollBarVisibility) + { + case ScrollBarVisibility.Always: + ScrollViewer.SetVerticalScrollBarVisibility(control, WASDKScrollBarVisibility.Visible); + break; + case ScrollBarVisibility.Never: + ScrollViewer.SetVerticalScrollBarVisibility(control, WASDKScrollBarVisibility.Hidden); + break; + case ScrollBarVisibility.Default: + ScrollViewer.SetVerticalScrollBarVisibility(control, defaultVerticalScrollVisibility.Value); + break; + } } } @@ -112,22 +150,60 @@ internal static void UpdateHorizontalScrollBarVisibility( ScrollBarVisibility scrollBarVisibility, ref WASDKScrollBarVisibility? defaultHorizontalScrollVisibility) { - if (defaultHorizontalScrollVisibility is null) + // Check if this is a ScrollView (CV2 - new ItemsView control) + if (control is Microsoft.UI.Xaml.Controls.ItemsView itemsView && itemsView.ScrollView is not null) { - defaultHorizontalScrollVisibility = ScrollViewer.GetHorizontalScrollBarVisibility(control); - } + var scrollView = itemsView.ScrollView; + + if (scrollBarVisibility != ScrollBarVisibility.Default) + { + // If the value is changing to anything other than the default, record the default + if (defaultHorizontalScrollVisibility is null) + { + defaultHorizontalScrollVisibility = (WASDKScrollBarVisibility)scrollView.HorizontalScrollBarVisibility; + } + } + + if (defaultHorizontalScrollVisibility is null) + { + // If the default has never been recorded, then this has never been set to anything but the + // default value; there's nothing to do. + return; + } - switch (scrollBarVisibility) + switch (scrollBarVisibility) + { + case ScrollBarVisibility.Always: + scrollView.HorizontalScrollBarVisibility = (ScrollingScrollBarVisibility)WASDKScrollBarVisibility.Visible; + break; + case ScrollBarVisibility.Never: + scrollView.HorizontalScrollBarVisibility = (ScrollingScrollBarVisibility)WASDKScrollBarVisibility.Hidden; + break; + case ScrollBarVisibility.Default: + scrollView.HorizontalScrollBarVisibility = (ScrollingScrollBarVisibility)defaultHorizontalScrollVisibility.Value; + break; + } + } + else { - case ScrollBarVisibility.Always: - ScrollViewer.SetHorizontalScrollBarVisibility(control, WASDKScrollBarVisibility.Visible); - break; - case ScrollBarVisibility.Never: - ScrollViewer.SetHorizontalScrollBarVisibility(control, WASDKScrollBarVisibility.Hidden); - break; - case ScrollBarVisibility.Default: - ScrollViewer.SetHorizontalScrollBarVisibility(control, defaultHorizontalScrollVisibility.Value); - break; + // CV1 - Use ScrollViewer attached properties + if (defaultHorizontalScrollVisibility is null) + { + defaultHorizontalScrollVisibility = ScrollViewer.GetHorizontalScrollBarVisibility(control); + } + + switch (scrollBarVisibility) + { + case ScrollBarVisibility.Always: + ScrollViewer.SetHorizontalScrollBarVisibility(control, WASDKScrollBarVisibility.Visible); + break; + case ScrollBarVisibility.Never: + ScrollViewer.SetHorizontalScrollBarVisibility(control, WASDKScrollBarVisibility.Hidden); + break; + case ScrollBarVisibility.Default: + ScrollViewer.SetHorizontalScrollBarVisibility(control, defaultHorizontalScrollVisibility.Value); + break; + } } } } diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index 69d5c13d676b..4304f4bb97fb 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -40,6 +40,8 @@ public abstract class ItemsViewHandler2 : ViewHandler ApplyItemsUpdatingScrollMode()); } @@ -429,6 +436,9 @@ void OnScrollViewerFound() PlatformView.ScrollView.ViewChanged += ScrollViewChanged; PlatformView.ScrollView.PointerWheelChanged += PointerScrollChanged; + + UpdateVerticalScrollBarVisibility(); + UpdateHorizontalScrollBarVisibility(); } void LayoutPropertyChanged(object? sender, PropertyChangedEventArgs e) @@ -759,11 +769,21 @@ void HandleScroll(WScrollPresenter scrollViewer) Element.SendScrolled(itemsViewScrolledEventArgs); var remainingItemsThreshold = Element.RemainingItemsThreshold; - if (_collectionViewSource != null && remainingItemsThreshold > -1 && - _collectionViewSource.View.Count - 1 - itemsViewScrolledEventArgs.LastVisibleItemIndex <= remainingItemsThreshold) + if (_collectionViewSource != null && remainingItemsThreshold > -1) { - Element.SendRemainingItemsThresholdReached(); - } + var itemsRemaining = _collectionViewSource.View.Count - 1 - itemsViewScrolledEventArgs.LastVisibleItemIndex; + + if(itemsRemaining<= remainingItemsThreshold) + { + if (itemsViewScrolledEventArgs.LastVisibleItemIndex > _lastRemainingItemsThresholdIndex) + { + _lastRemainingItemsThresholdIndex = itemsViewScrolledEventArgs.LastVisibleItemIndex; + Element.SendRemainingItemsThresholdReached(); + } + + } + + } } ItemsViewScrolledEventArgs ComputeVisibleIndexes(ItemsViewScrolledEventArgs args, bool advancing) From 6ab7577b566d9ed3bb0a86a4b910db6aa523be3d Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Tue, 6 Jan 2026 12:29:27 +0530 Subject: [PATCH 011/109] Update for ItemsViewExtensions --- .../Core/Handlers/Items/ItemsViewExtensions.Windows.cs | 9 +++++---- .../Core/Handlers/Items2/ItemsViewHandler2.Windows.cs | 10 ++++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs b/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs index ddb469a8b9e4..77e376069f34 100644 --- a/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs @@ -4,6 +4,7 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using WASDKScrollBarVisibility = Microsoft.UI.Xaml.Controls.ScrollBarVisibility; +using WASDKScrollingScrollBarVisibility = Microsoft.UI.Xaml.Controls.ScrollingScrollBarVisibility; namespace Microsoft.Maui.Controls.Handlers.Items { @@ -41,7 +42,7 @@ internal static FrameworkElement RealizeEmptyView(View? view, IMauiContext mauiC return platformView as FrameworkElement ?? throw new InvalidOperationException("Unable to convert view to FrameworkElement"); } - internal static FrameworkElement RealizeHeaderFooterTemplate(object bindingContext, DataTemplate? template, IMauiContext mauiContext, ref View? mauiView) + internal static FrameworkElement RealizeHeaderFooterTemplate(object? bindingContext, DataTemplate? template, IMauiContext mauiContext, ref View? mauiView) { if (template is null) { @@ -101,13 +102,13 @@ internal static void UpdateVerticalScrollBarVisibility( switch (scrollBarVisibility) { case ScrollBarVisibility.Always: - scrollView.VerticalScrollBarVisibility = (ScrollingScrollBarVisibility)WASDKScrollBarVisibility.Visible; + scrollView.VerticalScrollBarVisibility = WASDKScrollingScrollBarVisibility.Visible; break; case ScrollBarVisibility.Never: - scrollView.VerticalScrollBarVisibility = (ScrollingScrollBarVisibility)WASDKScrollBarVisibility.Hidden; + scrollView.VerticalScrollBarVisibility = WASDKScrollingScrollBarVisibility.Hidden; break; case ScrollBarVisibility.Default: - scrollView.VerticalScrollBarVisibility = (ScrollingScrollBarVisibility)defaultVerticalScrollVisibility.Value; + scrollView.VerticalScrollBarVisibility = (WASDKScrollingScrollBarVisibility)defaultVerticalScrollVisibility.Value; break; } } diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index 4304f4bb97fb..6e879112838a 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -597,7 +597,8 @@ void UpdateHeader() // If HeaderTemplate is set, use it regardless of header value if (headerTemplate is not null) { - _header = RealizeHeaderFooterTemplate(header, headerTemplate, ref _mauiHeader); + var bindingContext = header ?? (object)string.Empty; + _header = ItemsViewExtensions.RealizeHeaderFooterTemplate(bindingContext, headerTemplate, MauiContext!, ref _mauiHeader); } else if (header is not null) { @@ -608,7 +609,7 @@ void UpdateHeader() Text = text, Margin = new Microsoft.UI.Xaml.Thickness(0, 0, 0, 10) }, - View view => RealizeHeaderFooterView(view, ref _mauiHeader), + View view => ItemsViewExtensions.RealizeHeaderFooterView(view, MauiContext!, ref _mauiHeader), _ => new TextBlock { Text = header.ToString(), @@ -669,7 +670,8 @@ void UpdateFooter() // If FooterTemplate is set, use it regardless of footer value if (footerTemplate is not null) { - _footer = RealizeHeaderFooterTemplate(footer, footerTemplate, ref _mauiFooter); + var bindingContext = footer ?? (object)string.Empty; + _footer = ItemsViewExtensions.RealizeHeaderFooterTemplate(bindingContext, footerTemplate, MauiContext!, ref _mauiFooter); } else if (footer is not null) { @@ -680,7 +682,7 @@ void UpdateFooter() Text = text, Margin = new Microsoft.UI.Xaml.Thickness(0, 10, 0, 0) }, - View view => RealizeHeaderFooterView(view, ref _mauiFooter), + View view => ItemsViewExtensions.RealizeHeaderFooterView(view, MauiContext!, ref _mauiFooter), _ => new TextBlock { Text = footer.ToString(), From c9d72dde88203a4d5e71ff5d5882bc4b13a5c653 Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Wed, 7 Jan 2026 18:34:14 +0530 Subject: [PATCH 012/109] ItemsLayout and Scrolling Fix --- .../Handlers/Items2/ItemsViewHandler2.Windows.cs | 6 ++++++ .../Core/Handlers/Items2/Windows/MauiItemsView.cs | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index 6e879112838a..24fa61906f28 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -306,6 +306,12 @@ void ApplyItemsUpdatingScrollMode() return; } + if (PlatformView.Layout is UniformGridLayout layout) + { + PlatformView.UpdateLayout(); + } + + if (VirtualView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepItemsInView) { // Keeps the first item in the list displayed when new items are added. diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs b/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs index 98f1ed6bf3c9..166794ea4f94 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs @@ -6,6 +6,7 @@ using WControlTemplate = Microsoft.UI.Xaml.Controls.ControlTemplate; using WVisibility = Microsoft.UI.Xaml.Visibility; using WStackPanel = Microsoft.UI.Xaml.Controls.StackPanel; +using WScrollView = Microsoft.UI.Xaml.Controls.ScrollView; namespace Microsoft.Maui.Controls.Handlers.Items2 { @@ -26,6 +27,7 @@ internal partial class MauiItemsView : UI.Xaml.Controls.ItemsView, IEmptyView WStackPanel? _containerPanel; FrameworkElement? _itemsRepeater; bool _isHorizontalLayout; + WScrollView? _scrollView; public MauiItemsView() { @@ -139,6 +141,7 @@ protected override void OnApplyTemplate() _footerContentControl = GetTemplateChild("FooterContentControl") as ContentControl; _containerPanel = GetTemplateChild("PART_ContainerGrid") as WStackPanel; _itemsRepeater = GetTemplateChild("PART_ItemsRepeater") as FrameworkElement; + _scrollView = GetTemplateChild("PART_ScrollView") as WScrollView; if (_emptyView is not null && _emptyViewContentControl is not null) { @@ -184,7 +187,12 @@ void ApplyLayoutOrientation() _headerContentControl.HorizontalContentAlignment = UI.Xaml.HorizontalAlignment.Left; _footerContentControl.VerticalContentAlignment = UI.Xaml.VerticalAlignment.Top; _footerContentControl.HorizontalContentAlignment = UI.Xaml.HorizontalAlignment.Left; - } + + if (_scrollView is not null) + { + _scrollView.ContentOrientation = UI.Xaml.Controls.ScrollingContentOrientation.Horizontal; + } + } else { _containerPanel.Orientation = Orientation.Vertical; @@ -194,6 +202,11 @@ void ApplyLayoutOrientation() _headerContentControl.HorizontalContentAlignment = UI.Xaml.HorizontalAlignment.Stretch; _footerContentControl.VerticalContentAlignment = UI.Xaml.VerticalAlignment.Top; _footerContentControl.HorizontalContentAlignment = UI.Xaml.HorizontalAlignment.Stretch; + + if(_scrollView is not null) + { + _scrollView.ContentOrientation = UI.Xaml.Controls.ScrollingContentOrientation.Vertical; + } } } From a6c0d475e7d7ea54abe8bf72b6e64a419e9394cc Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Thu, 8 Jan 2026 18:07:18 +0530 Subject: [PATCH 013/109] Fix for ItemSizingStatergy and KeepItemsInView --- .../Items2/ItemsViewHandler2.Windows.cs | 31 ++++++++++--- .../Windows/CollectionViewHandler2.Windows.cs | 42 ++++++++++++++++++ .../Handlers/Items2/Windows/ItemFactory.cs | 44 +++++++++++++++++++ .../net-windows/PublicAPI.Unshipped.txt | 2 +- 4 files changed, 111 insertions(+), 8 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index 24fa61906f28..3ee3f003c389 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -44,7 +44,7 @@ public abstract class ItemsViewHandler2 : ViewHandler VirtualView; protected TItemsView Element => VirtualView; @@ -70,7 +70,6 @@ public ItemsViewHandler2(PropertyMapper? mapper = null) : base(mapper ?? ItemsVi [Controls.ItemsView.EmptyViewTemplateProperty.PropertyName] = MapEmptyViewTemplate, [Controls.ItemsView.FlowDirectionProperty.PropertyName] = MapFlowDirection, [Controls.ItemsView.IsVisibleProperty.PropertyName] = MapIsVisible, - [Controls.ItemsView.ItemsUpdatingScrollModeProperty.PropertyName] = MapItemsUpdatingScrollMode, [Controls.StructuredItemsView.ItemsLayoutProperty.PropertyName] = MapItemsLayout, [Controls.StructuredItemsView.HeaderProperty.PropertyName] = MapHeader, [Controls.StructuredItemsView.HeaderTemplateProperty.PropertyName] = MapHeaderTemplate, @@ -78,6 +77,10 @@ public ItemsViewHandler2(PropertyMapper? mapper = null) : base(mapper ?? ItemsVi [Controls.StructuredItemsView.FooterTemplateProperty.PropertyName] = MapFooterTemplate, }; + + + private bool _scrollUpdatePending; + public static void MapItemsSource(ItemsViewHandler2 handler, ItemsView itemsView) { handler.UpdateItemsSource(); @@ -118,10 +121,6 @@ public static void MapIsVisible(ItemsViewHandler2 handler, ItemsView handler.PlatformView.UpdateVisibility(itemsView); } - public static void MapItemsUpdatingScrollMode(ItemsViewHandler2 handler, ItemsView itemsView) - { - } - public static void MapItemsLayout(ItemsViewHandler2 handler, ItemsView itemsView) { handler.UpdateItemsLayout(); @@ -296,7 +295,18 @@ void ItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) _lastRemainingItemsThresholdIndex = -1; } - VirtualView.Dispatcher.DispatchAsync(() => ApplyItemsUpdatingScrollMode()); + if (!_scrollUpdatePending && PlatformView is not null) + { + _scrollUpdatePending = true; + + PlatformView.LayoutUpdated += OnLayoutUpdated; + } + } + void OnLayoutUpdated(object? s, object args) + { + PlatformView.LayoutUpdated -= OnLayoutUpdated; + _scrollUpdatePending = false; + ApplyItemsUpdatingScrollMode(); } void ApplyItemsUpdatingScrollMode() @@ -314,6 +324,7 @@ void ApplyItemsUpdatingScrollMode() if (VirtualView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepItemsInView) { + _isScrollingForItemsUpdate = true; // Keeps the first item in the list displayed when new items are added. PlatformView.StartBringItemIntoView(0, new BringIntoViewOptions() { AnimationDesired = false }); } @@ -748,6 +759,12 @@ void ScrollViewChanged(UI.Xaml.Controls.ScrollView sender, object args) void HandleScroll(WScrollPresenter scrollViewer) { + if(_isScrollingForItemsUpdate) + { + _isScrollingForItemsUpdate = false; + return; + } + var itemsViewScrolledEventArgs = new ItemsViewScrolledEventArgs { HorizontalOffset = scrollViewer.HorizontalOffset, diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs index 2baadd5e0add..79bea3611695 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs @@ -28,6 +28,8 @@ public CollectionViewHandler2(PropertyMapper? mapper = null) : base(mapper ?? Ma [SelectableItemsView.SelectedItemProperty.PropertyName] = MapSelectedItem, [SelectableItemsView.SelectedItemsProperty.PropertyName] = MapSelectedItems, [SelectableItemsView.SelectionModeProperty.PropertyName] = MapSelectionMode, + [StructuredItemsView.ItemSizingStrategyProperty.PropertyName] = MapItemsUpdatingScrollMode, + }; } public partial class CollectionViewHandler2 : ItemsViewHandler2 @@ -36,6 +38,9 @@ public partial class CollectionViewHandler2 : ItemsViewHandler2 ItemsView.ItemsLayout; } + // Cache for MeasureFirstItem optimization + global::Windows.Foundation.Size _firstItemMeasuredSize = global::Windows.Foundation.Size.Empty; + public static void MapCanReorderItems(CollectionViewHandler2 handler, ReorderableItemsView itemsView) { } @@ -55,6 +60,11 @@ public static void MapGroupFooterTemplate(CollectionViewHandler2 handler, Groupa handler.UpdateItemsSource(); } + public static void MapItemsUpdatingScrollMode(CollectionViewHandler2 handler, ItemsView itemsView) + { + handler.InvalidateFirstItemSize(); + handler.UpdateItemsSource(); + } public static void MapItemsSource(CollectionViewHandler2 handler, SelectableItemsView itemsView) { @@ -74,6 +84,38 @@ public static void MapSelectionMode(CollectionViewHandler2 handler, SelectableIt { } + /// + /// Gets the cached first item measured size for MeasureFirstItem optimization. + /// Returns Size.Empty if not cached or not using MeasureFirstItem strategy. + /// + internal global::Windows.Foundation.Size GetCachedFirstItemSize() + { + if (VirtualView is CollectionView cv && cv.ItemSizingStrategy == ItemSizingStrategy.MeasureFirstItem) + { + return _firstItemMeasuredSize; + } + return global::Windows.Foundation.Size.Empty; + } + + /// + /// Sets the cached first item measured size for MeasureFirstItem optimization. + /// + internal void SetCachedFirstItemSize(global::Windows.Foundation.Size size) + { + if (VirtualView is CollectionView cv && cv.ItemSizingStrategy == ItemSizingStrategy.MeasureFirstItem) + { + _firstItemMeasuredSize = size; + } + } + + /// + /// Invalidates the cached first item size. + /// + internal void InvalidateFirstItemSize() + { + _firstItemMeasuredSize = global::Windows.Foundation.Size.Empty; + } + protected override void ConnectHandler(WItemsView platformView) { base.ConnectHandler(platformView); diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs index 8a3420034e34..d031a0e20758 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs @@ -57,6 +57,11 @@ internal partial class ItemFactory(ItemsView view) : IElementFactory wrapper.HorizontalContentAlignment = HorizontalAlignment.Stretch; wrapper.SetContent(viewContent); + if(wrapper is not null) + { + wrapper.IsHeaderOrFooter = templateContext.IsHeader || templateContext.IsFooter; + } + if (wrapper?.VirtualView is View virtualView) { virtualView.SetValue(OriginTemplateProperty, template); @@ -110,6 +115,8 @@ internal partial class ElementWrapper(IMauiContext context) : ContentControl private IMauiContext _context = context; + public bool IsHeaderOrFooter { get; set; } + public void SetContent(IView view) { if (VirtualView is null || VirtualView.Handler is null) @@ -118,5 +125,42 @@ public void SetContent(IView view) VirtualView = view; } } + + protected override global::Windows.Foundation.Size MeasureOverride(global::Windows.Foundation.Size availableSize) + { + // Access handler through view parent chain (same pattern as iOS) + CollectionViewHandler2? handler = null; + if (VirtualView is View view && + view.Parent is ItemsView itemsView && + itemsView.Handler is CollectionViewHandler2 cvHandler) + { + handler = cvHandler; + } + + // Check if we should use cached first item size + var cachedSize = handler?.GetCachedFirstItemSize() ?? global::Windows.Foundation.Size.Empty; + + if (!cachedSize.IsEmpty) + { + // Use cached size for MeasureFirstItem strategy + base.MeasureOverride(cachedSize); + return cachedSize; + } + + // Measure normally + var measuredSize = base.MeasureOverride(availableSize); + + // Cache the size if this is the first item being measured + if (handler != null && !IsHeaderOrFooter) + { + var currentCached = handler.GetCachedFirstItemSize(); + if (currentCached.IsEmpty) + { + handler.SetCachedFirstItemSize(measuredSize); + } + } + + return measuredSize; + } } } diff --git a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt index 8fc4acbd8ff9..2d9c6da882c2 100644 --- a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -21,6 +21,7 @@ static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapGroupFo static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapGroupHeaderTemplate(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.GroupableItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapIsGrouped(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.GroupableItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapItemsSource(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.SelectableItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapItemsUpdatingScrollMode(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.Mapper -> Microsoft.Maui.PropertyMapper! static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapSelectedItem(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.SelectableItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapSelectedItems(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.SelectableItemsView! itemsView) -> void @@ -34,7 +35,6 @@ static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.Map static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapEmptyViewTemplate(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapFlowDirection(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapIsVisible(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void -static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapItemsUpdatingScrollMode(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapItemsLayout(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapHeader(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapHeaderTemplate(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void From 06add2c1216ec2fa3c058b336c9678f46e2f7645 Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Fri, 9 Jan 2026 12:44:24 +0530 Subject: [PATCH 014/109] ScrollBarChanges --- .../Items/ItemsViewExtensions.Windows.cs | 135 ------------------ .../Items/ItemsViewHandler.Windows.cs | 49 +++++-- .../Items2/ItemsViewHandler2.Windows.cs | 84 +++++++++-- .../CollectionView/ItemsViewStyles.xaml | 1 - 4 files changed, 114 insertions(+), 155 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs b/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs index 77e376069f34..c562bd5dd7d8 100644 --- a/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs @@ -72,140 +72,5 @@ internal static FrameworkElement RealizeHeaderFooterView(View? view, IMauiContex return platformView as FrameworkElement ?? throw new InvalidOperationException("Unable to convert view to FrameworkElement"); } - - internal static void UpdateVerticalScrollBarVisibility( - UIElement control, - ScrollBarVisibility scrollBarVisibility, - ref WASDKScrollBarVisibility? defaultVerticalScrollVisibility) - { - // Check if this is a ScrollView (CV2 - new ItemsView control) - if (control is Microsoft.UI.Xaml.Controls.ItemsView itemsView && itemsView.ScrollView is not null) - { - var scrollView = itemsView.ScrollView; - - if (scrollBarVisibility != ScrollBarVisibility.Default) - { - // If the value is changing to anything other than the default, record the default - if (defaultVerticalScrollVisibility is null) - { - defaultVerticalScrollVisibility = (WASDKScrollBarVisibility)scrollView.VerticalScrollBarVisibility; - } - } - - if (defaultVerticalScrollVisibility is null) - { - // If the default has never been recorded, then this has never been set to anything but the - // default value; there's nothing to do. - return; - } - - switch (scrollBarVisibility) - { - case ScrollBarVisibility.Always: - scrollView.VerticalScrollBarVisibility = WASDKScrollingScrollBarVisibility.Visible; - break; - case ScrollBarVisibility.Never: - scrollView.VerticalScrollBarVisibility = WASDKScrollingScrollBarVisibility.Hidden; - break; - case ScrollBarVisibility.Default: - scrollView.VerticalScrollBarVisibility = (WASDKScrollingScrollBarVisibility)defaultVerticalScrollVisibility.Value; - break; - } - } - else - { - // CV1 - Use ScrollViewer attached properties - if (scrollBarVisibility != ScrollBarVisibility.Default) - { - // If the value is changing to anything other than the default, record the default - if (defaultVerticalScrollVisibility is null) - { - defaultVerticalScrollVisibility = ScrollViewer.GetVerticalScrollBarVisibility(control); - } - } - - if (defaultVerticalScrollVisibility is null) - { - // If the default has never been recorded, then this has never been set to anything but the - // default value; there's nothing to do. - return; - } - - switch (scrollBarVisibility) - { - case ScrollBarVisibility.Always: - ScrollViewer.SetVerticalScrollBarVisibility(control, WASDKScrollBarVisibility.Visible); - break; - case ScrollBarVisibility.Never: - ScrollViewer.SetVerticalScrollBarVisibility(control, WASDKScrollBarVisibility.Hidden); - break; - case ScrollBarVisibility.Default: - ScrollViewer.SetVerticalScrollBarVisibility(control, defaultVerticalScrollVisibility.Value); - break; - } - } - } - - internal static void UpdateHorizontalScrollBarVisibility( - UIElement control, - ScrollBarVisibility scrollBarVisibility, - ref WASDKScrollBarVisibility? defaultHorizontalScrollVisibility) - { - // Check if this is a ScrollView (CV2 - new ItemsView control) - if (control is Microsoft.UI.Xaml.Controls.ItemsView itemsView && itemsView.ScrollView is not null) - { - var scrollView = itemsView.ScrollView; - - if (scrollBarVisibility != ScrollBarVisibility.Default) - { - // If the value is changing to anything other than the default, record the default - if (defaultHorizontalScrollVisibility is null) - { - defaultHorizontalScrollVisibility = (WASDKScrollBarVisibility)scrollView.HorizontalScrollBarVisibility; - } - } - - if (defaultHorizontalScrollVisibility is null) - { - // If the default has never been recorded, then this has never been set to anything but the - // default value; there's nothing to do. - return; - } - - switch (scrollBarVisibility) - { - case ScrollBarVisibility.Always: - scrollView.HorizontalScrollBarVisibility = (ScrollingScrollBarVisibility)WASDKScrollBarVisibility.Visible; - break; - case ScrollBarVisibility.Never: - scrollView.HorizontalScrollBarVisibility = (ScrollingScrollBarVisibility)WASDKScrollBarVisibility.Hidden; - break; - case ScrollBarVisibility.Default: - scrollView.HorizontalScrollBarVisibility = (ScrollingScrollBarVisibility)defaultHorizontalScrollVisibility.Value; - break; - } - } - else - { - // CV1 - Use ScrollViewer attached properties - if (defaultHorizontalScrollVisibility is null) - { - defaultHorizontalScrollVisibility = ScrollViewer.GetHorizontalScrollBarVisibility(control); - } - - switch (scrollBarVisibility) - { - case ScrollBarVisibility.Always: - ScrollViewer.SetHorizontalScrollBarVisibility(control, WASDKScrollBarVisibility.Visible); - break; - case ScrollBarVisibility.Never: - ScrollViewer.SetHorizontalScrollBarVisibility(control, WASDKScrollBarVisibility.Hidden); - break; - case ScrollBarVisibility.Default: - ScrollViewer.SetHorizontalScrollBarVisibility(control, defaultHorizontalScrollVisibility.Value); - break; - } - } - } } } diff --git a/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.Windows.cs b/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.Windows.cs index 40d1b855f959..cf2514890fa8 100644 --- a/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.Windows.cs @@ -383,18 +383,51 @@ internal void UpdateScrollBarVisibility() void UpdateVerticalScrollBarVisibility() { - ItemsViewExtensions.UpdateVerticalScrollBarVisibility( - Control, - Element.VerticalScrollBarVisibility, - ref _defaultVerticalScrollVisibility); + if (Element.VerticalScrollBarVisibility != ScrollBarVisibility.Default) + { + // If the value is changing to anything other than the default, record the default + if (_defaultVerticalScrollVisibility == null) + _defaultVerticalScrollVisibility = ScrollViewer.GetVerticalScrollBarVisibility(Control); + } + + if (_defaultVerticalScrollVisibility == null) + { + // If the default has never been recorded, then this has never been set to anything but the + // default value; there's nothing to do. + return; + } + + switch (Element.VerticalScrollBarVisibility) + { + case (ScrollBarVisibility.Always): + ScrollViewer.SetVerticalScrollBarVisibility(Control, WASDKScrollBarVisibility.Visible); + break; + case (ScrollBarVisibility.Never): + ScrollViewer.SetVerticalScrollBarVisibility(Control, WASDKScrollBarVisibility.Hidden); + break; + case (ScrollBarVisibility.Default): + ScrollViewer.SetVerticalScrollBarVisibility(Control, _defaultVerticalScrollVisibility.Value); + break; + } } void UpdateHorizontalScrollBarVisibility() { - ItemsViewExtensions.UpdateHorizontalScrollBarVisibility( - Control, - Element.HorizontalScrollBarVisibility, - ref _defaultHorizontalScrollVisibility); + if (_defaultHorizontalScrollVisibility == null) + _defaultHorizontalScrollVisibility = ScrollViewer.GetHorizontalScrollBarVisibility(Control); + + switch (Element.HorizontalScrollBarVisibility) + { + case (ScrollBarVisibility.Always): + ScrollViewer.SetHorizontalScrollBarVisibility(Control, WASDKScrollBarVisibility.Visible); + break; + case (ScrollBarVisibility.Never): + ScrollViewer.SetHorizontalScrollBarVisibility(Control, WASDKScrollBarVisibility.Hidden); + break; + case (ScrollBarVisibility.Default): + ScrollViewer.SetHorizontalScrollBarVisibility(Control, _defaultHorizontalScrollVisibility.Value); + break; + } } protected virtual void OnScrollViewerFound(ScrollViewer scrollViewer) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index 3ee3f003c389..f7d0d9cf6480 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -37,8 +37,8 @@ public abstract class ItemsViewHandler2 : ViewHandler From ab3feea407028143afa4882ea611b0a8ad54daf4 Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:32:55 +0530 Subject: [PATCH 015/109] Update ItemsViewHandler2.Windows.cs --- .../Items2/ItemsViewHandler2.Windows.cs | 88 ++++++++++++++----- 1 file changed, 65 insertions(+), 23 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index f7d0d9cf6480..d33cd4518a06 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -1,7 +1,10 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; +using System.Diagnostics; +using System.Linq; using Microsoft.Maui.Controls.Handlers.Items; using Microsoft.Maui.Controls.Internals; using Microsoft.Maui.Controls.Platform; @@ -197,13 +200,19 @@ CollectionViewSource CreateCollectionViewSource() Source = TemplatedItemSourceFactory2.CreateGrouped(itemsSource, itemTemplate, groupableItemsView.GroupHeaderTemplate, groupableItemsView.GroupFooterTemplate, Element, mauiContext: MauiContext) + , IsSourceGrouped = false }; } else { + var flattenedSource = itemsSource; + if (itemsSource is not null && IsItemsSourceGrouped(itemsSource)) + { + flattenedSource = FlattenGroupedItemsSource(itemsSource); + } return new CollectionViewSource { - Source = TemplatedItemSourceFactory2.Create(itemsSource, itemTemplate, Element, mauiContext: MauiContext), + Source = TemplatedItemSourceFactory2.Create(flattenedSource, itemTemplate, Element, mauiContext: MauiContext), IsSourceGrouped = false }; } @@ -216,6 +225,30 @@ CollectionViewSource CreateCollectionViewSource() }; } + bool IsItemsSourceGrouped(object itemsSource) + { + if (itemsSource is IEnumerable enumerable) + { + foreach (var item in enumerable) + { + if (item is IEnumerable && item is not string) + { + return true; + } + break; + } + } + return false; + } + + IEnumerable FlattenGroupedItemsSource(IEnumerable groupedSource) + { + return groupedSource.Cast(). + Where(group => group is IEnumerable && group is not string). + SelectMany(group => ((IEnumerable)group).Cast()); + } + + protected virtual void UpdateItemsSource() { if (PlatformView is null) @@ -506,34 +539,43 @@ void UpdateEmptyView() } var emptyView = Element.EmptyView; - var emptyViewTemplate = Element.EmptyViewTemplate; - if (emptyViewTemplate is not null) - { - // If EmptyViewTemplate is provided, use it instead of EmptyView - _emptyView = ItemsViewExtensions.RealizeEmptyViewTemplate(emptyView, emptyViewTemplate, MauiContext!, ref _mauiEmptyView); - } - else if (emptyView is not null) + + // Clear empty view + if (emptyView is null && emptyViewTemplate is null) { - switch (emptyView) + if (_emptyViewDisplayed) { - case string text: - _emptyView = new TextBlock - { - HorizontalAlignment = Microsoft.UI.Xaml.HorizontalAlignment.Center, - VerticalAlignment = Microsoft.UI.Xaml.VerticalAlignment.Center, - Text = text - }; - break; - case View view: - _emptyView = ItemsViewExtensions.RealizeEmptyView(view, MauiContext!, ref _mauiEmptyView); - break; - default: - _emptyView = ItemsViewExtensions.RealizeEmptyViewTemplate(emptyView, null, MauiContext!, ref _mauiEmptyView); - break; + if (_emptyView != null && PlatformView is IEmptyView ev) + ev.EmptyViewVisibility = WVisibility.Collapsed; + + if (_mauiEmptyView != null) + ItemsView.RemoveLogicalChild(_mauiEmptyView); + + _emptyViewDisplayed = false; } + + _emptyView = null; + _mauiEmptyView = null; + (PlatformView as IEmptyView)?.SetEmptyView(null, null); + return; } + // Resolve empty view + _emptyView = emptyViewTemplate != null + ? ItemsViewExtensions.RealizeEmptyViewTemplate(emptyView, emptyViewTemplate, MauiContext!, ref _mauiEmptyView) + : emptyView switch + { + string text => new TextBlock + { + HorizontalAlignment = Microsoft.UI.Xaml.HorizontalAlignment.Center, + VerticalAlignment = Microsoft.UI.Xaml.VerticalAlignment.Center, + Text = text + }, + View view => ItemsViewExtensions.RealizeEmptyView(view, MauiContext!, ref _mauiEmptyView), + _ => ItemsViewExtensions.RealizeEmptyViewTemplate(emptyView, null, MauiContext!, ref _mauiEmptyView) + }; + (PlatformView as IEmptyView)?.SetEmptyView(_emptyView, _mauiEmptyView); UpdateEmptyViewVisibility(); } From 76fa1d5f42e48092301ad48958e0630d01a53cda Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Mon, 19 Jan 2026 11:49:22 +0530 Subject: [PATCH 016/109] EmptyView Fix --- .../Items/ItemsViewExtensions.Windows.cs | 2 +- .../Windows/CollectionView/ItemsViewStyles.xaml | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs b/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs index c562bd5dd7d8..fbd9cd3e55a0 100644 --- a/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs @@ -10,7 +10,7 @@ namespace Microsoft.Maui.Controls.Handlers.Items { internal static class ItemsViewExtensions { - internal static FrameworkElement RealizeEmptyViewTemplate(object bindingContext, DataTemplate? emptyViewTemplate, IMauiContext mauiContext, ref View? mauiEmptyView) + internal static FrameworkElement RealizeEmptyViewTemplate(object? bindingContext, DataTemplate? emptyViewTemplate, IMauiContext mauiContext, ref View? mauiEmptyView) { if (emptyViewTemplate is null) { diff --git a/src/Controls/src/Core/Platform/Windows/CollectionView/ItemsViewStyles.xaml b/src/Controls/src/Core/Platform/Windows/CollectionView/ItemsViewStyles.xaml index 43a8a908999e..3915cd8032a8 100644 --- a/src/Controls/src/Core/Platform/Windows/CollectionView/ItemsViewStyles.xaml +++ b/src/Controls/src/Core/Platform/Windows/CollectionView/ItemsViewStyles.xaml @@ -263,7 +263,7 @@ - + - - + + - - + From 7dc9c12230d0cd018f666c759abfe8b00cb98101 Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:32:30 +0530 Subject: [PATCH 017/109] Selection and VisualStates --- .../Items2/ItemsViewHandler2.Windows.cs | 38 ++++++++--- .../Windows/CollectionViewHandler2.Windows.cs | 63 +++++++++++++++---- .../Handlers/Items2/Windows/ItemFactory.cs | 14 +++++ 3 files changed, 96 insertions(+), 19 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index d33cd4518a06..9bf28dfa8ce1 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -53,6 +53,13 @@ public abstract class ItemsViewHandler2 : ViewHandler Layout switch + { + LinearItemsLayout linearLayout => linearLayout.Orientation == ItemsLayoutOrientation.Horizontal, + GridItemsLayout gridLayout => gridLayout.Orientation == ItemsLayoutOrientation.Horizontal, + _ => false + }; + public ItemsViewHandler2() : base(ItemsViewMapper) { @@ -375,8 +382,7 @@ MauiItemsView SelectListViewBase() Layout = CreateItemsLayout() }; - if (Layout is LinearItemsLayout listItemsLayout && - listItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal) + if (IsLayoutHorizontal) { ScrollViewer.SetHorizontalScrollMode(itemsView, UI.Xaml.Controls.ScrollMode.Enabled); ScrollViewer.SetHorizontalScrollBarVisibility(itemsView, WASDKScrollBarVisibility.Visible); @@ -384,14 +390,17 @@ MauiItemsView SelectListViewBase() ScrollViewer.SetVerticalScrollMode(itemsView, UI.Xaml.Controls.ScrollMode.Disabled); ScrollViewer.SetVerticalScrollBarVisibility(itemsView, WASDKScrollBarVisibility.Disabled); - // Set header/footer to horizontal layout - itemsView.SetLayoutOrientation(true); } else { - // Set header/footer to vertical layout - itemsView.SetLayoutOrientation(false); + ScrollViewer.SetHorizontalScrollMode(itemsView, UI.Xaml.Controls.ScrollMode.Disabled); + ScrollViewer.SetHorizontalScrollBarVisibility(itemsView, WASDKScrollBarVisibility.Disabled); + + ScrollViewer.SetVerticalScrollMode(itemsView, UI.Xaml.Controls.ScrollMode.Enabled); + ScrollViewer.SetVerticalScrollBarVisibility(itemsView, WASDKScrollBarVisibility.Visible); } + + itemsView.SetLayoutOrientation(IsLayoutHorizontal); return itemsView; } @@ -406,10 +415,23 @@ protected void UpdateItemsLayout() PlatformView.Layout = CreateItemsLayout(); + bool isHorizontal = IsLayoutHorizontal; + // Update header/footer orientation - if (PlatformView is MauiItemsView mauiItemsView && Layout is LinearItemsLayout linearLayout) + if (PlatformView is MauiItemsView mauiItemsView) { - mauiItemsView.SetLayoutOrientation(linearLayout.Orientation == ItemsLayoutOrientation.Horizontal); + if (IsLayoutHorizontal) + { + ScrollViewer.SetHorizontalScrollMode(mauiItemsView, UI.Xaml.Controls.ScrollMode.Enabled); + ScrollViewer.SetVerticalScrollMode(mauiItemsView, UI.Xaml.Controls.ScrollMode.Disabled); + } + else + { + ScrollViewer.SetHorizontalScrollMode(mauiItemsView, UI.Xaml.Controls.ScrollMode.Disabled); + ScrollViewer.SetVerticalScrollMode(mauiItemsView, UI.Xaml.Controls.ScrollMode.Enabled); + } + + mauiItemsView.SetLayoutOrientation(IsLayoutHorizontal); } UpdateVerticalScrollBarVisibility(); diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs index 79bea3611695..52a9904ca037 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs @@ -197,14 +197,21 @@ void UpdateVirtualSelection() break; } - var formsItemContentControls = PlatformView.GetChildren(); - foreach (var formsItemContentControl in formsItemContentControls) + UpdateVisualStates(); + } + + void UpdateVisualStates() + { + if (PlatformView is null || ItemsView is null) + return; + + foreach (var itemcontainer in PlatformView.GetChildren()) { - if (formsItemContentControl is not null) + if (itemcontainer?.Child is ElementWrapper wrapper && wrapper.VirtualView is VisualElement visualElement) { - bool isSelected = ItemsView.SelectedItem == formsItemContentControl.FormsDataContext || - ItemsView.SelectedItems.Contains(formsItemContentControl.FormsDataContext); - formsItemContentControl.UpdateIsSelected(isSelected); + var actualItem = visualElement.BindingContext; + bool isSelected = ItemsView.SelectedItem == actualItem || ItemsView.SelectedItems.Contains(actualItem); + VisualStateManager.GoToState(visualElement, isSelected ? VisualStateManager.CommonStates.Selected : VisualStateManager.CommonStates.Normal); } } } @@ -228,16 +235,49 @@ void UpdateVirtualMultipleSelection() { ItemsView.SelectionChanged -= VirtualSelectionChanged; - var selection = new List(); - for (int index = 0; index < PlatformView.SelectedItems.Count; index++) + // Get current platform selection + var currentPlatformSelection = new HashSet(); + foreach (var item in PlatformView.SelectedItems) { - var item = PlatformView.SelectedItems[index]; var selectedItem = item is ItemTemplateContext2 itc ? itc.Item : item; if (selectedItem is not null) - selection.Add(selectedItem); + currentPlatformSelection.Add(selectedItem); + } + + // Get previous virtual selection + var previousSelection = new HashSet(ItemsView.SelectedItems); + + // Find newly selected items (in platform but not in virtual) + var newlySelected = new List(); + foreach (var item in PlatformView.SelectedItems) + { + var selectedItem = item is ItemTemplateContext2 itc ? itc.Item : item; + if (selectedItem is not null && !previousSelection.Contains(selectedItem)) + newlySelected.Add(selectedItem); + } + + // Build new selection: existing items still selected + newly selected items + var newSelection = new List(); + + // Keep existing items that are still selected (maintains their order) + foreach (var existingItem in ItemsView.SelectedItems) + { + if (currentPlatformSelection.Contains(existingItem)) + { + newSelection.Add(existingItem); + } + } + + // Add newly selected items at the end (in selection order) + foreach (var item in newlySelected) + { + if (!newSelection.Contains(item)) + { + newSelection.Add(item); + } } - ItemsView.UpdateSelectedItems(selection); + ItemsView.UpdateSelectedItems(newSelection); ItemsView.SelectionChanged += VirtualSelectionChanged; } @@ -308,6 +348,7 @@ void UpdatePlatformSelection() } _ignorePlatformSelectionChange = false; + UpdateVisualStates(); } } diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs index d031a0e20758..4438c1abcbd8 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs @@ -73,6 +73,20 @@ internal partial class ItemFactory(ItemsView view) : IElementFactory { view.BindingContext = templateContext.Item ?? _view.BindingContext; _view.AddLogicalChild(view); + if (_view is SelectableItemsView selectableItemsView && selectableItemsView.SelectionMode != SelectionMode.None) + { + bool isSelected = false; + if (selectableItemsView.SelectionMode == SelectionMode.Single) + isSelected = selectableItemsView.SelectedItem == templateContext.Item; + else + isSelected = selectableItemsView.SelectedItems.Contains(templateContext.Item); + + if (isSelected && view is VisualElement visualElement) + { + VisualStateManager.GoToState(visualElement, VisualStateManager.CommonStates.Selected); + } + } + } container ??= new ItemContainer() From 8e178aed524ead5fd775994c8e603b3591746f09 Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Fri, 23 Jan 2026 09:32:53 +0530 Subject: [PATCH 018/109] Fix for Itemspacing --- .../Core/Handlers/Items2/ItemsViewHandler2.Windows.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index 9bf28dfa8ce1..260c6f615349 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -411,6 +411,16 @@ protected void UpdateItemsLayout() _defaultHorizontalScrollVisibility = null; _defaultVerticalScrollVisibility = null; + // Unsubscribe from the old layout's property changes and subscribe to the new layout + _layoutPropertyChangedProxy?.Unsubscribe(); + _layoutPropertyChangedProxy = null; + + if (Layout is not null) + { + _layoutPropertyChanged ??= LayoutPropertyChanged; + _layoutPropertyChangedProxy = new WeakNotifyPropertyChangedProxy(Layout, _layoutPropertyChanged); + } + UpdateItemsSource(); PlatformView.Layout = CreateItemsLayout(); From f3539c71e46ed9fa3c4bb10b68ca7ce17134a4ec Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:15:21 +0530 Subject: [PATCH 019/109] Null check for crash issues --- .../Items2/ItemsViewHandler2.Windows.cs | 100 +++++++++++++----- .../Windows/CollectionViewHandler2.Windows.cs | 20 ++-- 2 files changed, 90 insertions(+), 30 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index 260c6f615349..3d7dffa98a01 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -185,12 +185,22 @@ protected override void ConnectHandler(WItemsView platformView) protected override void DisconnectHandler(WItemsView platformView) { - base.DisconnectHandler(platformView); + // Unsubscribe from LayoutUpdated before disconnecting to prevent exceptions + if (_scrollUpdatePending) + { + platformView.LayoutUpdated -= OnLayoutUpdated; + _scrollUpdatePending = false; + } _layoutPropertyChangedProxy?.Unsubscribe(); _layoutPropertyChangedProxy = null; - VirtualView.ScrollToRequested -= ScrollToRequested; + if (VirtualView is not null) + { + VirtualView.ScrollToRequested -= ScrollToRequested; + } + + base.DisconnectHandler(platformView); } CollectionViewSource CreateCollectionViewSource() @@ -320,7 +330,7 @@ void CleanUpCollectionViewSource() } } - if (VirtualView?.ItemsSource is null) + if (VirtualView?.ItemsSource is null && PlatformView is not null) { PlatformView.ItemsSource = null; } @@ -328,6 +338,12 @@ void CleanUpCollectionViewSource() void ItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) { + // Skip if handler is disconnected + if (PlatformView is null || VirtualView is null) + { + return; + } + UpdateEmptyViewVisibility(); if(e.Action == NotifyCollectionChangedAction.Add || e.Action == NotifyCollectionChangedAction.Reset ) @@ -344,6 +360,12 @@ void ItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) } void OnLayoutUpdated(object? s, object args) { + if (PlatformView is null) + { + _scrollUpdatePending = false; + return; + } + PlatformView.LayoutUpdated -= OnLayoutUpdated; _scrollUpdatePending = false; ApplyItemsUpdatingScrollMode(); @@ -351,6 +373,11 @@ void OnLayoutUpdated(object? s, object args) void ApplyItemsUpdatingScrollMode() { + if (PlatformView is null || VirtualView is null) + { + return; + } + if (_itemsSource is null || _itemsSource.Count == 0 || _collectionViewSource?.View?.Count <= 1) { return; @@ -490,6 +517,11 @@ static UniformGridLayout CreateGridView(GridItemsLayout gridItemsLayout) void FindScrollViewer() { + if (PlatformView is null) + { + return; + } + if (PlatformView.ScrollView is not null) { OnScrollViewerFound(); @@ -508,7 +540,7 @@ void ListViewLoaded(object sender, RoutedEventArgs e) void OnScrollViewerFound() { - if (PlatformView.ScrollView is null) + if (PlatformView is null || PlatformView.ScrollView is null) { return; } @@ -614,6 +646,11 @@ void UpdateEmptyView() void UpdateEmptyViewVisibility() { + if (PlatformView is null || VirtualView is null) + { + return; + } + // Check both CollectionViewSource.View and the underlying _itemsSource // After a Reset action, CollectionViewSource.View.Count might not be immediately updated bool isEmpty = (_collectionViewSource?.View?.Count ?? 0) == 0 && (_itemsSource?.Count ?? 0) == 0; @@ -882,6 +919,9 @@ void UpdateHorizontalScrollBarVisibility() void PointerScrollChanged(object sender, PointerRoutedEventArgs e) { + if (PlatformView?.ScrollView is null) + return; + if (PlatformView.ScrollView.ComputedHorizontalScrollMode == ScrollingScrollMode.Enabled) { PlatformView.ScrollView.AddScrollVelocity(new(e.GetCurrentPoint(PlatformView.ScrollView).Properties.MouseWheelDelta, 0), null); @@ -890,6 +930,9 @@ void PointerScrollChanged(object sender, PointerRoutedEventArgs e) void ScrollViewChanged(UI.Xaml.Controls.ScrollView sender, object args) { + if (PlatformView?.ScrollView?.ScrollPresenter is null) + return; + HandleScroll(PlatformView.ScrollView.ScrollPresenter); } @@ -963,6 +1006,11 @@ ItemsViewScrolledEventArgs ComputeVisibleIndexes(ItemsViewScrolledEventArgs args int firstVisibleItemIndex = -1; int lastVisibleItemIndex = -1; + if (PlatformView is null) + { + return (firstVisibleItemIndex, lastVisibleItemIndex, -1); + } + PlatformView.TryGetItemIndex(0, 0, out firstVisibleItemIndex); PlatformView.TryGetItemIndex(1, 1, out lastVisibleItemIndex); @@ -980,29 +1028,33 @@ void ScrollToRequested(object? sender, ScrollToRequestEventArgs args) index = FindItemIndex(args.Item); } - if (index >= 0) + // Validate index is within bounds + var itemCount = _collectionViewSource?.View?.Count ?? 0; + if (index < 0 || index >= itemCount) { - float offset = 0.0f; - switch (args.ScrollToPosition) - { - case ScrollToPosition.Start: - offset = 0.0f; - break; - case ScrollToPosition.Center: - offset = 0.5f; - break; - case ScrollToPosition.End: - offset = 1.0f; - break; - } + return; + } - PlatformView.StartBringItemIntoView(index, new BringIntoViewOptions() - { - AnimationDesired = args.IsAnimated, - VerticalAlignmentRatio = offset, - HorizontalAlignmentRatio = offset - }); + float offset = 0.0f; + switch (args.ScrollToPosition) + { + case ScrollToPosition.Start: + offset = 0.0f; + break; + case ScrollToPosition.Center: + offset = 0.5f; + break; + case ScrollToPosition.End: + offset = 1.0f; + break; } + + PlatformView.StartBringItemIntoView(index, new BringIntoViewOptions() + { + AnimationDesired = args.IsAnimated, + VerticalAlignmentRatio = offset, + HorizontalAlignmentRatio = offset + }); } int FindItemIndex(object item) diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs index 52a9904ca037..8c312bf9285e 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs @@ -218,21 +218,24 @@ void UpdateVisualStates() void UpdateVirtualSingleSelection() { + if (PlatformView is null || ItemsView is null) + return; + var selectedItem = PlatformView.SelectedItem is ItemTemplateContext2 itemPair ? itemPair.Item : PlatformView.SelectedItem; - if (ItemsView is not null) - { - ItemsView.SelectionChanged -= VirtualSelectionChanged; - ItemsView.SelectedItem = selectedItem; + ItemsView.SelectionChanged -= VirtualSelectionChanged; + ItemsView.SelectedItem = selectedItem; - ItemsView.SelectionChanged += VirtualSelectionChanged; - } + ItemsView.SelectionChanged += VirtualSelectionChanged; } void UpdateVirtualMultipleSelection() { + if (PlatformView is null || ItemsView is null) + return; + ItemsView.SelectionChanged -= VirtualSelectionChanged; // Get current platform selection @@ -283,6 +286,11 @@ void UpdateVirtualMultipleSelection() void UpdatePlatformSelection() { + if (PlatformView is null || ItemsView is null) + { + return; + } + _ignorePlatformSelectionChange = true; var itemList = PlatformView.ItemsSource as ICollectionView; From 644172c528d4f7fb07e8e4b55ddf12ab9b87552b Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Mon, 2 Feb 2026 12:29:35 +0530 Subject: [PATCH 020/109] DataTemplate Selector and CollectionChanged fix --- .../Items/ItemsViewExtensions.Windows.cs | 17 +++++++++++------ .../Items2/ItemsViewHandler2.Windows.cs | 13 +++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs b/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs index fbd9cd3e55a0..69078dfd7c40 100644 --- a/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs @@ -14,15 +14,11 @@ internal static FrameworkElement RealizeEmptyViewTemplate(object? bindingContext { if (emptyViewTemplate is null) { - return new TextBlock - { - HorizontalAlignment = Microsoft.UI.Xaml.HorizontalAlignment.Center, - VerticalAlignment = Microsoft.UI.Xaml.VerticalAlignment.Center, - Text = bindingContext?.ToString() ?? string.Empty - }; + return CreateDefaultEmptyViewTextBlock(bindingContext); } var template = emptyViewTemplate.SelectDataTemplate(bindingContext, null); + var view = template.CreateContent() as View; if (view is not null) { @@ -32,6 +28,15 @@ internal static FrameworkElement RealizeEmptyViewTemplate(object? bindingContext return RealizeEmptyView(view, mauiContext, ref mauiEmptyView); } + static TextBlock CreateDefaultEmptyViewTextBlock(object? bindingContext) + { + return new TextBlock + { + HorizontalAlignment = Microsoft.UI.Xaml.HorizontalAlignment.Center, + VerticalAlignment = Microsoft.UI.Xaml.VerticalAlignment.Center, + Text = bindingContext?.ToString() ?? string.Empty + }; + } internal static FrameworkElement RealizeEmptyView(View? view, IMauiContext mauiContext, ref View? mauiEmptyView) { mauiEmptyView = view ?? throw new ArgumentNullException(nameof(view)); diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index 3d7dffa98a01..f4045449d0dc 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -185,6 +185,11 @@ protected override void ConnectHandler(WItemsView platformView) protected override void DisconnectHandler(WItemsView platformView) { + if(_collectionViewSource?.Source is INotifyCollectionChanged incc) + { + incc.CollectionChanged -= ItemsChanged; + } + // Unsubscribe from LayoutUpdated before disconnecting to prevent exceptions if (_scrollUpdatePending) { @@ -625,6 +630,14 @@ void UpdateEmptyView() return; } + if (emptyViewTemplate is DataTemplateSelector && emptyView is null) + { + _emptyView = null; + _mauiEmptyView = null; + (PlatformView as IEmptyView)?.SetEmptyView(null, null); + return; + } + // Resolve empty view _emptyView = emptyViewTemplate != null ? ItemsViewExtensions.RealizeEmptyViewTemplate(emptyView, emptyViewTemplate, MauiContext!, ref _mauiEmptyView) From c1de96fd1384bfcea8882036febca969e50ab4ea Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:11:03 +0530 Subject: [PATCH 021/109] Fix changes --- .../src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs | 7 +++++-- .../Items2/Windows/CollectionViewHandler2.Windows.cs | 4 ++-- .../src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt | 3 ++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index f4045449d0dc..09b9ebd26eb5 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -80,6 +80,7 @@ public ItemsViewHandler2(PropertyMapper? mapper = null) : base(mapper ?? ItemsVi [Controls.ItemsView.EmptyViewTemplateProperty.PropertyName] = MapEmptyViewTemplate, [Controls.ItemsView.FlowDirectionProperty.PropertyName] = MapFlowDirection, [Controls.ItemsView.IsVisibleProperty.PropertyName] = MapIsVisible, + [Controls.ItemsView.ItemsUpdatingScrollModeProperty.PropertyName] = MapItemsUpdatingScrollMode, [Controls.StructuredItemsView.ItemsLayoutProperty.PropertyName] = MapItemsLayout, [Controls.StructuredItemsView.HeaderProperty.PropertyName] = MapHeader, [Controls.StructuredItemsView.HeaderTemplateProperty.PropertyName] = MapHeaderTemplate, @@ -87,8 +88,6 @@ public ItemsViewHandler2(PropertyMapper? mapper = null) : base(mapper ?? ItemsVi [Controls.StructuredItemsView.FooterTemplateProperty.PropertyName] = MapFooterTemplate, }; - - private bool _scrollUpdatePending; public static void MapItemsSource(ItemsViewHandler2 handler, ItemsView itemsView) @@ -96,6 +95,10 @@ public static void MapItemsSource(ItemsViewHandler2 handler, ItemsVi handler.UpdateItemsSource(); } + public static void MapItemsUpdatingScrollMode(ItemsViewHandler2 handler, ItemsView itemsView) + { + } + public static void MapHorizontalScrollBarVisibility(ItemsViewHandler2 handler, ItemsView itemsView) { handler.UpdateHorizontalScrollBarVisibility(); diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs index 8c312bf9285e..a8ad8882f5df 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs @@ -28,7 +28,7 @@ public CollectionViewHandler2(PropertyMapper? mapper = null) : base(mapper ?? Ma [SelectableItemsView.SelectedItemProperty.PropertyName] = MapSelectedItem, [SelectableItemsView.SelectedItemsProperty.PropertyName] = MapSelectedItems, [SelectableItemsView.SelectionModeProperty.PropertyName] = MapSelectionMode, - [StructuredItemsView.ItemSizingStrategyProperty.PropertyName] = MapItemsUpdatingScrollMode, + [StructuredItemsView.ItemSizingStrategyProperty.PropertyName] = MapItemSizingStrategy, }; } @@ -60,7 +60,7 @@ public static void MapGroupFooterTemplate(CollectionViewHandler2 handler, Groupa handler.UpdateItemsSource(); } - public static void MapItemsUpdatingScrollMode(CollectionViewHandler2 handler, ItemsView itemsView) + public static void MapItemSizingStrategy(CollectionViewHandler2 handler, ItemsView itemsView) { handler.InvalidateFirstItemSize(); handler.UpdateItemsSource(); diff --git a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt index 2d9c6da882c2..9eb540c690c0 100644 --- a/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-windows/PublicAPI.Unshipped.txt @@ -20,8 +20,8 @@ static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapCanReor static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapGroupFooterTemplate(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.GroupableItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapGroupHeaderTemplate(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.GroupableItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapIsGrouped(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.GroupableItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapItemSizingStrategy(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapItemsSource(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.SelectableItemsView! itemsView) -> void -static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapItemsUpdatingScrollMode(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.Mapper -> Microsoft.Maui.PropertyMapper! static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapSelectedItem(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.SelectableItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2.MapSelectedItems(Microsoft.Maui.Controls.Handlers.Items2.CollectionViewHandler2! handler, Microsoft.Maui.Controls.SelectableItemsView! itemsView) -> void @@ -30,6 +30,7 @@ static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.Ite static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapEmptyView(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapHorizontalScrollBarVisibility(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapItemsSource(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void +static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapItemsUpdatingScrollMode(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapItemTemplate(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapVerticalScrollBarVisibility(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void static Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2.MapEmptyViewTemplate(Microsoft.Maui.Controls.Handlers.Items2.ItemsViewHandler2! handler, Microsoft.Maui.Controls.ItemsView! itemsView) -> void From 755d2304ead5ed8dfe31489b0d2f0206f3342127 Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Mon, 2 Feb 2026 20:19:00 +0530 Subject: [PATCH 022/109] ScrollTo Group --- .../Items2/ItemsViewHandler2.Windows.cs | 119 +++++++++++++++++- 1 file changed, 114 insertions(+), 5 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index 09b9ebd26eb5..94f91274e087 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -1038,11 +1038,31 @@ ItemsViewScrolledEventArgs ComputeVisibleIndexes(ItemsViewScrolledEventArgs args void ScrollToRequested(object? sender, ScrollToRequestEventArgs args) { - var index = args.Index; - if (args.Mode == ScrollToMode.Element) + // Use base.PlatformView to avoid InvalidOperationException + if (base.PlatformView is not WItemsView platformView) + { + return; + } + + int index; + + // Handle grouped scrolling by position + if (args.Mode == ScrollToMode.Position && + ItemsView is GroupableItemsView groupableItemsView && + groupableItemsView.IsGrouped && + args.GroupIndex >= 0) + { + index = FindGroupedItemIndex(args.GroupIndex, args.Index); + } + else if (args.Mode == ScrollToMode.Element) { index = FindItemIndex(args.Item); } + else + { + // Non-grouped position-based scroll + index = args.Index; + } // Validate index is within bounds var itemCount = _collectionViewSource?.View?.Count ?? 0; @@ -1065,7 +1085,7 @@ void ScrollToRequested(object? sender, ScrollToRequestEventArgs args) break; } - PlatformView.StartBringItemIntoView(index, new BringIntoViewOptions() + platformView.StartBringItemIntoView(index, new BringIntoViewOptions() { AnimationDesired = args.IsAnimated, VerticalAlignmentRatio = offset, @@ -1073,6 +1093,78 @@ void ScrollToRequested(object? sender, ScrollToRequestEventArgs args) }); } + /// + /// Finds the flat index in the flattened grouped collection for the specified group and item index. + /// + int FindGroupedItemIndex(int groupIndex, int itemIndex) + { + if (_collectionViewSource is null) + { + return -1; + } + + if (ItemsView is not GroupableItemsView groupableItemsView) + { + return -1; + } + + var itemsSource = groupableItemsView.ItemsSource; + if (itemsSource is null) + { + return -1; + } + + var hasGroupHeader = groupableItemsView.GroupHeaderTemplate is not null; + var hasGroupFooter = groupableItemsView.GroupFooterTemplate is not null; + + int flatIndex = 0; + int currentGroupIndex = 0; + + foreach (var group in itemsSource) + { + if (group is IList groupList) + { + if (currentGroupIndex == groupIndex) + { + // Found the target group + if (hasGroupHeader) + { + flatIndex++; // Skip group header + } + + // Check if itemIndex is within bounds of this group + if (itemIndex < 0 || itemIndex >= groupList.Count) + { + return -1; + } + + // Return the calculated flat index + return flatIndex + itemIndex; + } + + // Count items in this group to move to next group + if (hasGroupHeader) + { + flatIndex++; + } + flatIndex += groupList.Count; + if (hasGroupFooter) + { + flatIndex++; + } + } + + currentGroupIndex++; + } + + // Group index not found + return -1; + } + + /// + /// Finds the index of an item in the collection view by searching through the flattened list. + /// Used for non-grouped ScrollToMode.Element requests. + /// int FindItemIndex(object item) { if (_collectionViewSource is null) @@ -1082,13 +1174,30 @@ int FindItemIndex(object item) for (int index = 0; index < _collectionViewSource.View.Count; index++) { - if (_collectionViewSource.View[index] is ItemTemplateContext pair) + var viewItem = _collectionViewSource.View[index]; + + // Check for ItemTemplateContext (non-grouped templated items) + if (viewItem is ItemTemplateContext pair) + { + if (Equals(pair.Item, item)) + { + return index; + } + } + // Check for ItemTemplateContext2 (grouped templated items) + else if (viewItem is ItemTemplateContext2 pair2) { - if (pair.Item == item) + // Skip headers and footers, only match actual items + if (!pair2.IsHeader && !pair2.IsFooter && Equals(pair2.Item, item)) { return index; } } + // Check for non-templated items (direct equality) + else if (Equals(viewItem, item)) + { + return index; + } } return -1; From 63daf70a35768c9a2472232a611678212dc96182 Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:17:09 +0530 Subject: [PATCH 023/109] commit for Groups and Orientation --- .../Items2/ItemsViewHandler2.Windows.cs | 4 +- .../Windows/GroupedItemTemplateCollection2.cs | 404 +++++++++++------- .../Handlers/Items2/Windows/ItemFactory.cs | 3 + 3 files changed, 250 insertions(+), 161 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index 94f91274e087..c4f50ee1c143 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -293,7 +293,9 @@ protected virtual void UpdateItemsSource() incc.CollectionChanged += ItemsChanged; } - PlatformView.ItemsSource = _collectionViewSource?.View; + PlatformView.ItemsSource = null; + PlatformView.ItemsSource = _collectionViewSource?.View; + if (VirtualView.ItemTemplate is not null) { diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs index 4aea33c23857..fedb19cd67ee 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs @@ -9,11 +9,12 @@ internal class GroupedItemTemplateCollection2 : ObservableCollection _groupSubscriptions = new(); + readonly Dictionary _groupSubscriptions = new(); + bool _suppressNotifications; public GroupedItemTemplateCollection2(IEnumerable itemsSource, DataTemplate itemTemplate, DataTemplate groupHeaderTemplate, DataTemplate groupFooterTemplate, @@ -26,124 +27,139 @@ public GroupedItemTemplateCollection2(IEnumerable itemsSource, _container = container; _mauiContext = mauiContext; - AddItems(_itemsSource); + RebuildFlatList(); SubscribeToGroups(_itemsSource); - if (_itemsSource is IList groupList && _itemsSource is INotifyCollectionChanged incc) + + if (_itemsSource is INotifyCollectionChanged incc) { incc.CollectionChanged += GroupsChanged; } } + protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + if (!_suppressNotifications) + { + base.OnCollectionChanged(e); + } + } + + ItemTemplateContext2 CreateItemContext(object item) => + new(_itemTemplate, item, _container, mauiContext: _mauiContext); + + ItemTemplateContext2 CreateHeaderContext(object group) => + new(_groupHeaderTemplate!, group, _container, null, null, null, isHeader: true, isFooter: false, mauiContext: _mauiContext); + + ItemTemplateContext2 CreateFooterContext(object group) => + new(_groupFooterTemplate!, group, _container, null, null, null, isHeader: false, isFooter: true, mauiContext: _mauiContext); + void SubscribeToGroups(IEnumerable? groups) { - if (groups is null) return; + if (groups is null) + return; + foreach (var group in groups) { - if (group is INotifyCollectionChanged incc && !_groupSubscriptions.ContainsKey(group)) - { - incc.CollectionChanged += (s, e) => GroupItemsChanged(group, e); - _groupSubscriptions[group] = incc; - } + SubscribeToGroup(group); + } + } + + void SubscribeToGroup(object group) + { + if (group is INotifyCollectionChanged incc && !_groupSubscriptions.ContainsKey(group)) + { + var handler = CreateGroupItemsChangedHandler(group); + incc.CollectionChanged += handler; + _groupSubscriptions[group] = handler; } } + NotifyCollectionChangedEventHandler CreateGroupItemsChangedHandler(object group) => + (s, e) => _container.Dispatcher.DispatchIfRequired(() => GroupItemsChanged(group, e)); + void UnsubscribeFromGroups(IEnumerable? groups) { - if (groups is null) return; + if (groups is null) + return; + foreach (var group in groups) { - if (_groupSubscriptions.TryGetValue(group, out var incc)) + UnsubscribeFromGroup(group); + } + } + + void UnsubscribeFromGroup(object group) + { + if (_groupSubscriptions.TryGetValue(group, out var handler)) + { + if (group is INotifyCollectionChanged incc) { - incc.CollectionChanged -= (s, e) => GroupItemsChanged(group, e); - _groupSubscriptions.Remove(group); + incc.CollectionChanged -= handler; } + _groupSubscriptions.Remove(group); } } - void AddItems(IEnumerable? items) + void UnsubscribeFromAllGroups() { - if (items is null) + foreach (var kvp in _groupSubscriptions) { - return; + if (kvp.Key is INotifyCollectionChanged incc) + { + incc.CollectionChanged -= kvp.Value; + } } + _groupSubscriptions.Clear(); + } - var newItems = new List(); - int index = Items.Count; - foreach (var group in items) + void RebuildFlatList() + { + Items.Clear(); + + foreach (var group in _itemsSource) { - if (group is IList itemsList) + if (group is not IList itemsList) + continue; + + if (_groupHeaderTemplate is not null) { - if (_groupHeaderTemplate is not null) - { - var newItem = new ItemTemplateContext2(_groupHeaderTemplate, group, _container, null, - null, null, true, false, mauiContext: _mauiContext); - newItems.Add(newItem); - Items.Add(newItem); - } + Items.Add(CreateHeaderContext(group)); + } - foreach (var item in itemsList) - { - var newItem = new ItemTemplateContext2(_itemTemplate, item, _container, mauiContext: _mauiContext); - newItems.Add(newItem); - Items.Add(newItem); - } + foreach (var item in itemsList) + { + Items.Add(CreateItemContext(item)); + } - if (_groupFooterTemplate is not null) - { - var newItem = new ItemTemplateContext2(_groupFooterTemplate, group, _container, null, - null, null, isHeader: false, isFooter: true, mauiContext: _mauiContext); - newItems.Add(newItem); - Items.Add(newItem); - } + if (_groupFooterTemplate is not null) + { + Items.Add(CreateFooterContext(group)); } } - - OnCollectionChanged(new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Add, newItems, index)); } - void GroupsChanged(object? sender, NotifyCollectionChangedEventArgs args) - { - _container.Dispatcher.DispatchIfRequired(() => OnNotifyCollectionChanged(args)); - } + void GroupsChanged(object? sender, NotifyCollectionChangedEventArgs args) => + _container.Dispatcher.DispatchIfRequired(() => OnGroupsCollectionChanged(args)); - void OnNotifyCollectionChanged(NotifyCollectionChangedEventArgs args) + void OnGroupsCollectionChanged(NotifyCollectionChangedEventArgs args) { switch (args.Action) { case NotifyCollectionChangedAction.Add: - AddItems(args.NewItems); SubscribeToGroups(args.NewItems); + ResetWithoutResubscribe(); break; case NotifyCollectionChangedAction.Move: + ResetWithoutResubscribe(); break; case NotifyCollectionChangedAction.Remove: if (args.OldItems is not null) { UnsubscribeFromGroups(args.OldItems); - var startIndex = args.OldStartingIndex; - if (startIndex < 0) - { - Reset(); - return; - } - - int count = 0; - foreach (var item in args.OldItems) - { - count++; - if (item is IList itemsList) - { - count += itemsList.Count; - } - } - - for (int index = startIndex + count - 1; index >= startIndex; index--) - { - RemoveAt(index); - } + ResetWithoutResubscribe(); } break; + case NotifyCollectionChangedAction.Replace: case NotifyCollectionChangedAction.Reset: Reset(); @@ -151,111 +167,179 @@ void OnNotifyCollectionChanged(NotifyCollectionChangedEventArgs args) } } - void GroupItemsChanged(object group, NotifyCollectionChangedEventArgs e) + /// + /// Gets the flat index for the first item in a group by iterating through the source collection. + /// Returns the index after the group header (if present), pointing to where group items start. + /// + int GetFlatIndexForGroupItems(object targetGroup) { - // Find the start index of this group's items in the flat list - int groupIndex = -1; - int current = 0; - foreach (var g in _itemsSource) + int flatIndex = 0; + + foreach (var group in _itemsSource) { - if (g == group) + if (ReferenceEquals(group, targetGroup)) { - groupIndex = current; - break; + // Found the group - return index after header (if present) + return _groupHeaderTemplate is not null ? flatIndex + 1 : flatIndex; } - current++; - } - if (groupIndex == -1) return; - // Calculate the flat list index for the first item of this group - int flatIndex = 0; - current = 0; - foreach (var g in _itemsSource) - { - if (g is IList itemsList) + if (group is IList itemsList) { - if (_groupHeaderTemplate is not null) flatIndex++; - if (current == groupIndex) break; + // Count header + items + footer for this group + if (_groupHeaderTemplate is not null) + flatIndex++; + flatIndex += itemsList.Count; - if (_groupFooterTemplate is not null) flatIndex++; + + if (_groupFooterTemplate is not null) + flatIndex++; } - current++; } - // Do NOT increment flatIndex for header here; it is already handled above + return -1; + } + + void GroupItemsChanged(object group, NotifyCollectionChangedEventArgs e) + { + int flatIndex = GetFlatIndexForGroupItems(group); + if (flatIndex == -1) + return; + + if (group is not IList groupList) + return; + + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + HandleGroupItemsAdd(e, flatIndex, groupList); + break; + + case NotifyCollectionChangedAction.Remove: + HandleGroupItemsRemove(e, flatIndex); + break; + + case NotifyCollectionChangedAction.Replace: + HandleGroupItemsReplace(e, flatIndex); + break; + + case NotifyCollectionChangedAction.Move: + HandleGroupItemsMove(e, flatIndex); + break; + + case NotifyCollectionChangedAction.Reset: + ResetWithoutResubscribe(); + break; + } + } + + void HandleGroupItemsAdd(NotifyCollectionChangedEventArgs e, int flatIndex, IList groupList) + { + if (e.NewItems is null) + return; - if (group is IList groupList) + int insertIndex = flatIndex + (e.NewStartingIndex >= 0 ? e.NewStartingIndex : groupList.Count - e.NewItems.Count); + var newItems = new List(e.NewItems.Count); + + _suppressNotifications = true; + foreach (var item in e.NewItems) { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - if (e.NewItems is not null) - { - int insertIndex = flatIndex + (e.NewStartingIndex >= 0 ? e.NewStartingIndex : groupList.Count - e.NewItems.Count); - var newItems = new List(); - foreach (var item in e.NewItems) - { - var newItem = new ItemTemplateContext2(_itemTemplate, item, _container, mauiContext: _mauiContext); - newItems.Add(newItem); - Items.Insert(insertIndex++, newItem); - } - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newItems, insertIndex - newItems.Count)); - } - break; - case NotifyCollectionChangedAction.Remove: - if (e.OldItems is not null) - { - int removeIndex = flatIndex + (e.OldStartingIndex >= 0 ? e.OldStartingIndex : 0); - for (int i = 0; i < e.OldItems.Count; i++) - { - Items.RemoveAt(removeIndex); - } - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, e.OldItems, removeIndex)); - } - break; - case NotifyCollectionChangedAction.Replace: - if (e.NewItems is not null && e.OldItems is not null) - { - int replaceIndex = flatIndex + (e.NewStartingIndex >= 0 ? e.NewStartingIndex : 0); - for (int i = 0; i < e.NewItems.Count; i++) - { - var newItem = new ItemTemplateContext2(_itemTemplate, e.NewItems[i]!, _container, mauiContext: _mauiContext); - Items[replaceIndex + i] = newItem; - } - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, e.NewItems, e.OldItems, replaceIndex)); - } - break; - case NotifyCollectionChangedAction.Move: - if (e.OldItems is not null && e.NewItems is not null) - { - int oldIndex = flatIndex + (e.OldStartingIndex >= 0 ? e.OldStartingIndex : 0); - int newIndex = flatIndex + (e.NewStartingIndex >= 0 ? e.NewStartingIndex : 0); - var moved = new List(); - for (int i = 0; i < e.OldItems.Count; i++) - { - var item = Items[oldIndex]; - Items.RemoveAt(oldIndex); - Items.Insert(newIndex + i, item); - moved.Add(item); - } - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, moved, newIndex, oldIndex)); - } - break; - case NotifyCollectionChangedAction.Reset: - Reset(); - break; - } - } + var newItem = CreateItemContext(item); + newItems.Add(newItem); + Items.Insert(insertIndex++, newItem); + } + _suppressNotifications = false; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, newItems, insertIndex - newItems.Count)); + } + + void HandleGroupItemsRemove(NotifyCollectionChangedEventArgs e, int flatIndex) + { + if (e.OldItems is null) + return; + + int removeIndex = flatIndex + (e.OldStartingIndex >= 0 ? e.OldStartingIndex : 0); + var removedItems = new List(e.OldItems.Count); + + for (int i = 0; i < e.OldItems.Count; i++) + { + removedItems.Add(Items[removeIndex]); + Items.RemoveAt(removeIndex); + } + + OnCollectionChanged(new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Remove, removedItems, removeIndex)); + } + + void HandleGroupItemsReplace(NotifyCollectionChangedEventArgs e, int flatIndex) + { + if (e.NewItems is null || e.OldItems is null) + return; + + int replaceIndex = flatIndex + (e.NewStartingIndex >= 0 ? e.NewStartingIndex : 0); + var oldItems = new List(e.NewItems.Count); + var newItems = new List(e.NewItems.Count); + + _suppressNotifications = true; + for (int i = 0; i < e.NewItems.Count; i++) + { + oldItems.Add(Items[replaceIndex + i]); + var newItem = CreateItemContext(e.NewItems[i]!); + newItems.Add(newItem); + Items[replaceIndex + i] = newItem; + } + _suppressNotifications = false; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Replace, newItems, oldItems, replaceIndex)); } + void HandleGroupItemsMove(NotifyCollectionChangedEventArgs e, int flatIndex) + { + if (e.OldItems is null) + return; + + int oldIndex = flatIndex + (e.OldStartingIndex >= 0 ? e.OldStartingIndex : 0); + int newIndex = flatIndex + (e.NewStartingIndex >= 0 ? e.NewStartingIndex : 0); + var movedItems = new List(e.OldItems.Count); + + _suppressNotifications = true; + for (int i = 0; i < e.OldItems.Count; i++) + { + var item = Items[oldIndex]; + Items.RemoveAt(oldIndex); + Items.Insert(newIndex + i, item); + movedItems.Add(item); + } + _suppressNotifications = false; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Move, movedItems, newIndex, oldIndex)); + } + + void ResetWithoutResubscribe() + { + _suppressNotifications = true; + RebuildFlatList(); + _suppressNotifications = false; + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + /// + /// Full reset that also resubscribes to all groups. + /// Used for Replace and Reset actions where group references may have changed. + /// public void Reset() { - UnsubscribeFromGroups(_itemsSource); - Items.Clear(); - AddItems(_itemsSource); + UnsubscribeFromAllGroups(); + + _suppressNotifications = true; + RebuildFlatList(); + _suppressNotifications = false; + SubscribeToGroups(_itemsSource); - var reset = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); - OnCollectionChanged(reset); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } } } diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs index 4438c1abcbd8..b0be3a156643 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs @@ -55,6 +55,8 @@ internal partial class ItemFactory(ItemsView view) : IElementFactory wrapper = new ElementWrapper(_view.Handler.MauiContext); wrapper.HorizontalAlignment = HorizontalAlignment.Stretch; wrapper.HorizontalContentAlignment = HorizontalAlignment.Stretch; + wrapper.VerticalAlignment = VerticalAlignment.Stretch; + wrapper.VerticalContentAlignment = VerticalAlignment.Stretch; wrapper.SetContent(viewContent); if(wrapper is not null) @@ -92,6 +94,7 @@ internal partial class ItemFactory(ItemsView view) : IElementFactory container ??= new ItemContainer() { Child = wrapper, + VerticalAlignment = VerticalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch }; From c775793f5b122c74c1e21ba1789108847f17ea44 Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Wed, 4 Feb 2026 19:19:52 +0530 Subject: [PATCH 024/109] ItemsUpdatingScrollMode --- .../Items2/ItemsViewHandler2.Windows.cs | 65 +++++++++++++++++-- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index c4f50ee1c143..fb731d97002c 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -48,6 +48,7 @@ public abstract class ItemsViewHandler2 : ViewHandler VirtualView; protected TItemsView Element => VirtualView; @@ -388,27 +389,77 @@ void ApplyItemsUpdatingScrollMode() return; } - if (_itemsSource is null || _itemsSource.Count == 0 || _collectionViewSource?.View?.Count <= 1) + if (_itemsSource is null || _itemsSource.Count == 0) { return; } - if (PlatformView.Layout is UniformGridLayout layout) + // Get the actual count from the CollectionView's view + var viewCount = _collectionViewSource?.View?.Count ?? 0; + if (viewCount == 0) { - PlatformView.UpdateLayout(); + return; } + // Force layout update to ensure items are properly positioned + PlatformView.UpdateLayout(); if (VirtualView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepItemsInView) { _isScrollingForItemsUpdate = true; - // Keeps the first item in the list displayed when new items are added. - PlatformView.StartBringItemIntoView(0, new BringIntoViewOptions() { AnimationDesired = false }); + + // Use dispatcher to ensure the scroll happens after layout is fully complete + VirtualView.Dispatcher.Dispatch(() => + { + if (PlatformView is null) + { + return; + } + + // Keeps the first item in the list displayed when new items are added. + PlatformView.StartBringItemIntoView(0, new BringIntoViewOptions() + { + AnimationDesired = false, + VerticalAlignmentRatio = 0.0, + HorizontalAlignmentRatio = 0.0 + }); + }); } else if (VirtualView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepLastItemInView) { - // Adjusts the scroll offset to keep the last item in the list displayed when new items are added. - PlatformView.StartBringItemIntoView(_itemsSource.Count - 1, new BringIntoViewOptions() { AnimationDesired = false }); + _isScrollingForItemsUpdate = true; + + // Use the view count to ensure we're scrolling to the correct last item + var lastIndex = viewCount - 1; + if (lastIndex >= 0) + { + _pendingScrollToIndex = lastIndex; + + // Use dispatcher to ensure the scroll happens after layout is fully complete + VirtualView.Dispatcher.Dispatch(() => + { + if (PlatformView is null || _pendingScrollToIndex < 0) + { + _pendingScrollToIndex = -1; + return; + } + + var currentViewCount = _collectionViewSource?.View?.Count ?? 0; + var scrollIndex = Math.Min(_pendingScrollToIndex, currentViewCount - 1); + + if (scrollIndex >= 0) + { + PlatformView.StartBringItemIntoView(scrollIndex, new BringIntoViewOptions() + { + AnimationDesired = false, + VerticalAlignmentRatio = 1.0, + HorizontalAlignmentRatio = 1.0 + }); + } + + _pendingScrollToIndex = -1; + }); + } } } From 2f309e1a41b8fd49904c95e63598144d7d86ea10 Mon Sep 17 00:00:00 2001 From: devanathan-vaithiyanathan <114395405+devanathan-vaithiyanathan@users.noreply.github.com> Date: Fri, 6 Feb 2026 13:09:21 +0530 Subject: [PATCH 025/109] Fixed non template grouped scenario --- .../Items2/ItemsViewHandler2.Windows.cs | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index fb731d97002c..0857d1dd82c4 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -217,31 +217,41 @@ CollectionViewSource CreateCollectionViewSource() var itemsSource = Element.ItemsSource; var itemTemplate = Element.ItemTemplate; - if (itemTemplate is not null && itemsSource is not null) + // Handle grouped items first (with or without ItemTemplate) + if (itemsSource is not null && ItemsView is GroupableItemsView groupableItemsView && groupableItemsView.IsGrouped) { - if (ItemsView is GroupableItemsView groupableItemsView && groupableItemsView.IsGrouped) + // When there's no ItemTemplate, use the raw ItemsSource directly (no wrapping) + // This allows default ToString() rendering without ItemTemplateContext2 wrapper + if (itemTemplate is null) { return new CollectionViewSource { - Source = TemplatedItemSourceFactory2.CreateGrouped(itemsSource, itemTemplate, - groupableItemsView.GroupHeaderTemplate, groupableItemsView.GroupFooterTemplate, - Element, mauiContext: MauiContext) - , IsSourceGrouped = false + Source = itemsSource, + IsSourceGrouped = true }; } - else + + return new CollectionViewSource { - var flattenedSource = itemsSource; - if (itemsSource is not null && IsItemsSourceGrouped(itemsSource)) - { - flattenedSource = FlattenGroupedItemsSource(itemsSource); - } - return new CollectionViewSource - { - Source = TemplatedItemSourceFactory2.Create(flattenedSource, itemTemplate, Element, mauiContext: MauiContext), - IsSourceGrouped = false - }; + Source = TemplatedItemSourceFactory2.CreateGrouped(itemsSource, itemTemplate, + groupableItemsView.GroupHeaderTemplate, groupableItemsView.GroupFooterTemplate, + Element, mauiContext: MauiContext) + , IsSourceGrouped = false + }; + } + + if (itemTemplate is not null && itemsSource is not null) + { + var flattenedSource = itemsSource; + if (itemsSource is not null && IsItemsSourceGrouped(itemsSource)) + { + flattenedSource = FlattenGroupedItemsSource(itemsSource); } + return new CollectionViewSource + { + Source = TemplatedItemSourceFactory2.Create(flattenedSource, itemTemplate, Element, mauiContext: MauiContext), + IsSourceGrouped = false + }; } return new CollectionViewSource From 6ffd4c43176abf5759731cfab0bdc667277f29d9 Mon Sep 17 00:00:00 2001 From: devanathan-vaithiyanathan <114395405+devanathan-vaithiyanathan@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:07:54 +0530 Subject: [PATCH 026/109] Fixed h-grid with header --- .../Items2/ItemsViewHandler2.Windows.cs | 28 +- .../Windows/GroupableUniformGridLayout.cs | 312 ++++++++++++++++++ 2 files changed, 329 insertions(+), 11 deletions(-) create mode 100644 src/Controls/src/Core/Handlers/Items2/Windows/GroupableUniformGridLayout.cs diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index 0857d1dd82c4..4eeac0e922f5 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -572,18 +572,24 @@ static UI.Xaml.Controls.StackLayout CreateStackLayout(LinearItemsLayout listItem }; } - static UniformGridLayout CreateGridView(GridItemsLayout gridItemsLayout) + UniformGridLayout CreateGridView(GridItemsLayout gridItemsLayout) { - return new UniformGridLayout() - { - Orientation = gridItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal - ? Orientation.Vertical : Orientation.Horizontal, - MaximumRowsOrColumns = gridItemsLayout.Span, - MinColumnSpacing = gridItemsLayout.HorizontalItemSpacing, - MinRowSpacing = gridItemsLayout.VerticalItemSpacing, - ItemsStretch = UniformGridLayoutItemsStretch.Fill, - ItemsJustification = UniformGridLayoutItemsJustification.Start, - }; + // Use custom layout for grouped items to handle headers/footers spanning full width + bool isGrouped = ItemsView is GroupableItemsView groupableItemsView && groupableItemsView.IsGrouped; + + UniformGridLayout layout = isGrouped + ? new GroupableUniformGridLayout() + : new UniformGridLayout(); + + layout.Orientation = gridItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal + ? Orientation.Vertical : Orientation.Horizontal; + layout.MaximumRowsOrColumns = gridItemsLayout.Span; + layout.MinColumnSpacing = gridItemsLayout.HorizontalItemSpacing; + layout.MinRowSpacing = gridItemsLayout.VerticalItemSpacing; + layout.ItemsStretch = UniformGridLayoutItemsStretch.Fill; + layout.ItemsJustification = UniformGridLayoutItemsJustification.Start; + + return layout; } void FindScrollViewer() diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/GroupableUniformGridLayout.cs b/src/Controls/src/Core/Handlers/Items2/Windows/GroupableUniformGridLayout.cs new file mode 100644 index 000000000000..4d9c2cf6fd5b --- /dev/null +++ b/src/Controls/src/Core/Handlers/Items2/Windows/GroupableUniformGridLayout.cs @@ -0,0 +1,312 @@ +using System; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Windows.Foundation; + +namespace Microsoft.Maui.Controls.Handlers.Items2 +{ + /// + /// Custom UniformGridLayout that handles group headers and footers spanning full width/height. + /// This mimics the behavior of CV1's GridView with GroupStyle support. + /// +#pragma warning disable CsWinRT1028 // Class is not marked partial + internal class GroupableUniformGridLayout : UniformGridLayout +#pragma warning restore CsWinRT1028 // Class is not marked partial + { + protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) + { + if (context.ItemCount == 0) + { + return new Size(0, 0); + } + + var span = MaximumRowsOrColumns; + var minColumnSpacing = MinColumnSpacing; + var minRowSpacing = MinRowSpacing; + var isVerticalOrientation = Orientation == Orientation.Horizontal; + + double totalHeight = 0; + double totalWidth = 0; + double maxItemSize = 0; + int columnIndex = 0; + int rowIndex = 0; + int regularItemCount = 0; + + // Measure all children and calculate dimensions + for (int i = 0; i < context.ItemCount; i++) + { + var element = context.GetOrCreateElementAt(i); + element.Measure(availableSize); + + bool isHeaderOrFooter = IsHeaderOrFooter(element); + + if (isHeaderOrFooter) + { + if (isVerticalOrientation) + { + // Complete current row if needed + if (columnIndex > 0) + { + totalHeight += maxItemSize + minRowSpacing; + columnIndex = 0; + maxItemSize = 0; + } + // Add header height + totalHeight += element.DesiredSize.Height + minRowSpacing; + } + else + { + // Complete current column if needed + if (rowIndex > 0) + { + totalWidth += maxItemSize + minColumnSpacing; + rowIndex = 0; + maxItemSize = 0; + } + // Add header width + totalWidth += element.DesiredSize.Width + minColumnSpacing; + } + } + else + { + regularItemCount++; + + if (isVerticalOrientation) + { + maxItemSize = Math.Max(maxItemSize, element.DesiredSize.Height); + columnIndex++; + + if (columnIndex >= span) + { + totalHeight += maxItemSize + minRowSpacing; + columnIndex = 0; + maxItemSize = 0; + } + } + else + { + maxItemSize = Math.Max(maxItemSize, element.DesiredSize.Width); + rowIndex++; + + if (rowIndex >= span) + { + totalWidth += maxItemSize + minColumnSpacing; + rowIndex = 0; + maxItemSize = 0; + } + } + } + } + + // Add final incomplete row/column + if (isVerticalOrientation) + { + if (columnIndex > 0) + { + totalHeight += maxItemSize; + } + + // Calculate width based on span + double cellWidth = (availableSize.Width - minColumnSpacing * (span - 1)) / span; + totalWidth = availableSize.Width; + } + else + { + if (rowIndex > 0) + { + totalWidth += maxItemSize; + } + + // Calculate height based on span + double cellHeight = (availableSize.Height - minRowSpacing * (span - 1)) / span; + totalHeight = availableSize.Height; + } + + return new Size(totalWidth, totalHeight); + } + + protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) + { + if (context.ItemCount == 0) + { + return finalSize; + } + + var span = MaximumRowsOrColumns; + var minColumnSpacing = MinColumnSpacing; + var minRowSpacing = MinRowSpacing; + var isVerticalOrientation = Orientation == Orientation.Horizontal; // Horizontal orientation means vertical scrolling (items flow left-to-right, stacking vertically) + + double currentX = 0; + double currentY = 0; + double cellWidth = 0; + double cellHeight = 0; + double maxItemSize = 0; // Max width (vertical) or height (horizontal) from regular items + int columnIndex = 0; + int rowIndex = 0; + + // First pass: Calculate cell dimensions from regular (non-header/footer) items + for (int i = 0; i < context.ItemCount; i++) + { + var child = context.GetOrCreateElementAt(i); + bool isHeaderOrFooter = IsHeaderOrFooter(child); + + if (!isHeaderOrFooter) + { + if (isVerticalOrientation) + { + // Vertical grid: items flow left-to-right, wrapping to new rows + // Track max height for uniform row heights + maxItemSize = Math.Max(maxItemSize, child.DesiredSize.Height); + } + else + { + // Horizontal grid: items flow top-to-bottom, wrapping to new columns + // Track max width for uniform column widths + maxItemSize = Math.Max(maxItemSize, child.DesiredSize.Width); + } + } + } + + // Calculate cell dimensions based on span and available space + if (isVerticalOrientation) + { + // Vertical grid: divide width by span for column width + double totalSpacing = minColumnSpacing * (span - 1); + cellWidth = (finalSize.Width - totalSpacing) / span; + cellHeight = maxItemSize > 0 ? maxItemSize : 0; + } + else + { + // Horizontal grid: divide height by span for row height + double totalSpacing = minRowSpacing * (span - 1); + cellHeight = (finalSize.Height - totalSpacing) / span; + cellWidth = maxItemSize > 0 ? maxItemSize : 0; + } + + // Second pass: Arrange items + for (int i = 0; i < context.ItemCount; i++) + { + var child = context.GetOrCreateElementAt(i); + bool isHeaderOrFooter = IsHeaderOrFooter(child); + + if (isHeaderOrFooter) + { + // Header/Footer: Span full width (vertical grid) or full height (horizontal grid) + if (isVerticalOrientation) + { + // If we're in the middle of a row, complete the row first + if (columnIndex > 0) + { + currentY += cellHeight + minRowSpacing; + currentX = 0; + columnIndex = 0; + } + + // Vertical grid: header spans full width + var headerHeight = child.DesiredSize.Height; + var headerRect = new Rect(0, currentY, finalSize.Width, headerHeight); + child.Arrange(headerRect); + + // Move to next row + currentY += headerHeight + minRowSpacing; + } + else + { + // If we're in the middle of a column, complete the column first + if (rowIndex > 0) + { + currentX += cellWidth + minColumnSpacing; + currentY = 0; + rowIndex = 0; + } + + // Horizontal grid: header spans full height + var headerWidth = child.DesiredSize.Width; + var headerRect = new Rect(currentX, 0, headerWidth, finalSize.Height); + child.Arrange(headerRect); + + // Move to next column + currentX += headerWidth + minColumnSpacing; + } + } + else + { + // Regular item: Place in grid cell + if (isVerticalOrientation) + { + // Vertical grid: items flow left-to-right + var itemRect = new Rect(currentX, currentY, cellWidth, cellHeight); + child.Arrange(itemRect); + + columnIndex++; + currentX += cellWidth + minColumnSpacing; + + if (columnIndex >= span) + { + // Move to next row + columnIndex = 0; + currentX = 0; + currentY += cellHeight + minRowSpacing; + } + } + else + { + // Horizontal grid: items flow top-to-bottom + var itemRect = new Rect(currentX, currentY, cellWidth, cellHeight); + child.Arrange(itemRect); + + rowIndex++; + currentY += cellHeight + minRowSpacing; + + if (rowIndex >= span) + { + // Move to next column + rowIndex = 0; + currentY = 0; + currentX += cellWidth + minColumnSpacing; + } + } + } + } + + // Calculate total size + if (isVerticalOrientation) + { + // Add final row height if we have items in the current row + if (columnIndex > 0) + { + currentY += cellHeight; + } + // Return the actual height needed (don't clamp to finalSize.Height) + return new Size(finalSize.Width, currentY); + } + else + { + // Add final column width if we have items in the current column + if (rowIndex > 0) + { + currentX += cellWidth; + } + // Return the actual width needed (don't clamp to finalSize.Width) + return new Size(currentX, finalSize.Height); + } + } + + /// + /// Determines if a child element is a header or footer by checking for ItemTemplateContext2. + /// + bool IsHeaderOrFooter(UIElement child) + { + // Check if the child is an ItemContainer and its content's DataContext is ItemTemplateContext2 + if (child is ItemContainer itemContainer && + itemContainer.Child is FrameworkElement fe && + fe.DataContext is ItemTemplateContext2 context) + { + return context.IsHeader || context.IsFooter; + } + + return false; + } + } +} From fc17ac68827d3fc0b2bbb493fc5853702fb60909 Mon Sep 17 00:00:00 2001 From: devanathan-vaithiyanathan <114395405+devanathan-vaithiyanathan@users.noreply.github.com> Date: Mon, 9 Feb 2026 13:15:41 +0530 Subject: [PATCH 027/109] Fix GridLayout with Span(Non template scenario), text are truncated --- .../src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index 4eeac0e922f5..b9b03d7cc979 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -586,7 +586,12 @@ UniformGridLayout CreateGridView(GridItemsLayout gridItemsLayout) layout.MaximumRowsOrColumns = gridItemsLayout.Span; layout.MinColumnSpacing = gridItemsLayout.HorizontalItemSpacing; layout.MinRowSpacing = gridItemsLayout.VerticalItemSpacing; - layout.ItemsStretch = UniformGridLayoutItemsStretch.Fill; + bool noTemplate = ItemsView.ItemTemplate is null; + bool isMeasureAllItems = ItemsView is CollectionView cv && cv.ItemSizingStrategy == ItemSizingStrategy.MeasureAllItems; + + layout.ItemsStretch = (noTemplate && isMeasureAllItems) + ? UniformGridLayoutItemsStretch.Uniform + : UniformGridLayoutItemsStretch.Fill; layout.ItemsJustification = UniformGridLayoutItemsJustification.Start; return layout; From e0dac4195ac6cfcd07749670b1d8812e788521ee Mon Sep 17 00:00:00 2001 From: devanathan-vaithiyanathan <114395405+devanathan-vaithiyanathan@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:56:07 +0530 Subject: [PATCH 028/109] Fix MeasureFirstItem with URL image return wrong size --- .../Handlers/Items2/Windows/ItemFactory.cs | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs index b0be3a156643..77f2bfa6a3cf 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Microsoft.Maui.Controls.Platform; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -157,24 +158,39 @@ view.Parent is ItemsView itemsView && // Check if we should use cached first item size var cachedSize = handler?.GetCachedFirstItemSize() ?? global::Windows.Foundation.Size.Empty; + // Always measure to allow content to load and render + var measuredSize = base.MeasureOverride(availableSize); + if (!cachedSize.IsEmpty) { - // Use cached size for MeasureFirstItem strategy - base.MeasureOverride(cachedSize); + // For MeasureFirstItem: Return cached size for uniform layout return cachedSize; } - // Measure normally - var measuredSize = base.MeasureOverride(availableSize); - - // Cache the size if this is the first item being measured + // Cache the first item's size for MeasureFirstItem optimization if (handler != null && !IsHeaderOrFooter) { - var currentCached = handler.GetCachedFirstItemSize(); - if (currentCached.IsEmpty) + // For first item with images: Hook into content's SizeChanged to update cache when images load + if (VirtualView is View firstView && Content is FrameworkElement content) { - handler.SetCachedFirstItemSize(measuredSize); + void OnContentSizeChanged(object? sender, Microsoft.UI.Xaml.SizeChangedEventArgs e) + { + // Update cached size with actual loaded size + var currentCache = handler.GetCachedFirstItemSize(); + if (!currentCache.IsEmpty && (e.NewSize.Width > currentCache.Width || e.NewSize.Height > currentCache.Height)) + { + handler.SetCachedFirstItemSize(e.NewSize); + // Force layout update + InvalidateMeasure(); + } + } + + // Subscribe once + content.SizeChanged -= OnContentSizeChanged; + content.SizeChanged += OnContentSizeChanged; } + + handler.SetCachedFirstItemSize(measuredSize); } return measuredSize; From 205a6602371ee2efd7b3ba9c40d778a596af3ebd Mon Sep 17 00:00:00 2001 From: devanathan-vaithiyanathan <114395405+devanathan-vaithiyanathan@users.noreply.github.com> Date: Mon, 9 Feb 2026 19:41:38 +0530 Subject: [PATCH 029/109] Fix for CollectionView Selection maintain issue --- .../Windows/CollectionViewHandler2.Windows.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs index a8ad8882f5df..0e3b89699227 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs @@ -35,6 +35,7 @@ public CollectionViewHandler2(PropertyMapper? mapper = null) : base(mapper ?? Ma public partial class CollectionViewHandler2 : ItemsViewHandler2 { bool _ignorePlatformSelectionChange; + Page? _parentPage; protected override IItemsLayout Layout { get => ItemsView.ItemsLayout; } @@ -121,6 +122,14 @@ protected override void ConnectHandler(WItemsView platformView) base.ConnectHandler(platformView); ItemsView.SelectionChanged += VirtualSelectionChanged; + + // Subscribe to parent page lifecycle events to clear selection on navigation + _parentPage = ItemsView.FindParentOfType(); + if (_parentPage is not null) + { + _parentPage.Disappearing += OnPageDisappearing; + } + if (PlatformView is not null) { PlatformView.SetBinding(WItemsView.SelectionModeProperty, @@ -150,10 +159,37 @@ protected override void DisconnectHandler(WItemsView platformView) { ItemsView.SelectionChanged -= VirtualSelectionChanged; } + + // Unsubscribe from page lifecycle events + if (_parentPage is not null) + { + _parentPage.Disappearing -= OnPageDisappearing; + _parentPage = null; + } base.DisconnectHandler(platformView); } + void OnPageDisappearing(object? sender, EventArgs e) + { + // Clear selection when navigating away from the page + // This allows re-selecting the same item when returning to the page + if (ItemsView is null || PlatformView is null) + return; + + _ignorePlatformSelectionChange = true; + + // Clear platform selection + PlatformView.DeselectAll(); + + // Clear virtual selection without firing SelectionChanged event + ItemsView.SelectionChanged -= VirtualSelectionChanged; + ItemsView.SelectedItem = null; + ItemsView.SelectionChanged += VirtualSelectionChanged; + + _ignorePlatformSelectionChange = false; + } + protected override void UpdateItemsSource() { _ignorePlatformSelectionChange = true; From 0b642faca6c34f0f0eb887d93d5aad753fda0761 Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Tue, 10 Feb 2026 18:58:14 +0530 Subject: [PATCH 030/109] Fix for flickering on horizontal scroll --- .../Core/Platform/Windows/CollectionView/ItemsViewStyles.xaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Controls/src/Core/Platform/Windows/CollectionView/ItemsViewStyles.xaml b/src/Controls/src/Core/Platform/Windows/CollectionView/ItemsViewStyles.xaml index 3915cd8032a8..440b5c88a868 100644 --- a/src/Controls/src/Core/Platform/Windows/CollectionView/ItemsViewStyles.xaml +++ b/src/Controls/src/Core/Platform/Windows/CollectionView/ItemsViewStyles.xaml @@ -278,7 +278,8 @@ Date: Wed, 11 Feb 2026 21:42:01 +0530 Subject: [PATCH 031/109] Proper cleanup code changes --- .../Items2/ItemsViewHandler2.Windows.cs | 183 +++++++++++++----- .../Windows/CollectionViewHandler2.Windows.cs | 8 +- .../Windows/GroupedItemTemplateCollection2.cs | 16 ++ .../Handlers/Items2/Windows/ItemFactory.cs | 35 +++- 4 files changed, 185 insertions(+), 57 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index fb731d97002c..5e940d399926 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -25,6 +25,7 @@ public abstract class ItemsViewHandler2 : ViewHandler : ViewHandler VirtualView; protected TItemsView Element => VirtualView; @@ -96,6 +98,8 @@ public static void MapItemsSource(ItemsViewHandler2 handler, ItemsVi handler.UpdateItemsSource(); } + // Intentionally empty: ItemsUpdatingScrollMode is handled during scroll events + // via ApplyItemsUpdatingScrollMode, not as a direct property map. public static void MapItemsUpdatingScrollMode(ItemsViewHandler2 handler, ItemsView itemsView) { } @@ -189,12 +193,25 @@ protected override void ConnectHandler(WItemsView platformView) protected override void DisconnectHandler(WItemsView platformView) { - if(_collectionViewSource?.Source is INotifyCollectionChanged incc) + // Phase 1: Unsubscribe ALL events first to prevent callbacks during cleanup + if (_collectionViewSource?.Source is INotifyCollectionChanged incc) { incc.CollectionChanged -= ItemsChanged; } - // Unsubscribe from LayoutUpdated before disconnecting to prevent exceptions + if (platformView.ScrollView is not null) + { + platformView.ScrollView.ViewChanged -= ScrollViewChanged; + platformView.ScrollView.PointerWheelChanged -= PointerScrollChanged; + } + + // Unsubscribe pending Loaded handler if ScrollView wasn't found yet + if (_pendingLoadedHandler is not null) + { + platformView.Loaded -= _pendingLoadedHandler; + _pendingLoadedHandler = null; + } + if (_scrollUpdatePending) { platformView.LayoutUpdated -= OnLayoutUpdated; @@ -209,6 +226,46 @@ protected override void DisconnectHandler(WItemsView platformView) VirtualView.ScrollToRequested -= ScrollToRequested; } + // Safe subscription cleanup only — do NOT call CleanUpCollectionViewSource() here + // because it sets _collectionViewSource.Source = null and accesses PlatformView.GetChildren, + // which triggers WinUI side effects (element recycling, collection changes) during teardown. + if (_collectionViewSource?.Source is ObservableItemTemplateCollection2 observableCollection) + { + observableCollection.CleanUp(); + } + else if (_collectionViewSource?.Source is GroupedItemTemplateCollection2 groupedCollection) + { + groupedCollection.CleanUp(); + } + + _itemFactory?.CleanUp(); + _itemFactory = null; + + // Clean up logical children for empty view, header, and footer to prevent memory leaks + if (_mauiEmptyView is not null && _emptyViewDisplayed) + { + ItemsView?.RemoveLogicalChild(_mauiEmptyView); + } + _mauiEmptyView = null; + _emptyView = null; + _emptyViewDisplayed = false; + + if (_mauiHeader is not null && _headerDisplayed) + { + ItemsView?.RemoveLogicalChild(_mauiHeader); + } + _mauiHeader = null; + _header = null; + _headerDisplayed = false; + + if (_mauiFooter is not null && _footerDisplayed) + { + ItemsView?.RemoveLogicalChild(_mauiFooter); + } + _mauiFooter = null; + _footer = null; + _footerDisplayed = false; + base.DisconnectHandler(platformView); } @@ -226,7 +283,8 @@ CollectionViewSource CreateCollectionViewSource() Source = TemplatedItemSourceFactory2.CreateGrouped(itemsSource, itemTemplate, groupableItemsView.GroupHeaderTemplate, groupableItemsView.GroupFooterTemplate, Element, mauiContext: MauiContext) - , IsSourceGrouped = false + , + IsSourceGrouped = false }; } else @@ -284,8 +342,6 @@ protected virtual void UpdateItemsSource() CleanUpCollectionViewSource(); - - _collectionViewSource = CreateCollectionViewSource(); _itemsSource = _collectionViewSource?.Source as IList; @@ -294,13 +350,14 @@ protected virtual void UpdateItemsSource() incc.CollectionChanged += ItemsChanged; } - PlatformView.ItemsSource = null; - PlatformView.ItemsSource = _collectionViewSource?.View; - + PlatformView.ItemsSource = null; + PlatformView.ItemsSource = _collectionViewSource?.View; + if (VirtualView.ItemTemplate is not null) { - PlatformView.ItemTemplate = new ItemFactory(Element); + _itemFactory = new ItemFactory(Element); + PlatformView.ItemTemplate = _itemFactory; } else if (PlatformView.ItemTemplate is not null) { @@ -312,12 +369,20 @@ protected virtual void UpdateItemsSource() void CleanUpCollectionViewSource() { + // Clean up the recycle pool in the old ItemFactory to release pooled elements + _itemFactory?.CleanUp(); + _itemFactory = null; + if (_collectionViewSource is not null) { if (_collectionViewSource.Source is ObservableItemTemplateCollection2 observableItemTemplateCollection) { observableItemTemplateCollection.CleanUp(); } + else if (_collectionViewSource.Source is GroupedItemTemplateCollection2 groupedItemTemplateCollection) + { + groupedItemTemplateCollection.CleanUp(); + } if (_collectionViewSource.Source is INotifyCollectionChanged incc) { @@ -329,7 +394,7 @@ void CleanUpCollectionViewSource() } // Remove all children inside the ItemsSource - if (VirtualView is not null) + if (VirtualView is not null && PlatformView is not null) { foreach (var item in PlatformView.GetChildren()) { @@ -357,7 +422,7 @@ void ItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) UpdateEmptyViewVisibility(); - if(e.Action == NotifyCollectionChangedAction.Add || e.Action == NotifyCollectionChangedAction.Reset ) + if (e.Action == NotifyCollectionChangedAction.Add || e.Action == NotifyCollectionChangedAction.Reset) { _lastRemainingItemsThresholdIndex = -1; } @@ -365,7 +430,7 @@ void ItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) if (!_scrollUpdatePending && PlatformView is not null) { _scrollUpdatePending = true; - + PlatformView.LayoutUpdated += OnLayoutUpdated; } } @@ -407,7 +472,7 @@ void ApplyItemsUpdatingScrollMode() if (VirtualView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepItemsInView) { _isScrollingForItemsUpdate = true; - + // Use dispatcher to ensure the scroll happens after layout is fully complete VirtualView.Dispatcher.Dispatch(() => { @@ -417,8 +482,8 @@ void ApplyItemsUpdatingScrollMode() } // Keeps the first item in the list displayed when new items are added. - PlatformView.StartBringItemIntoView(0, new BringIntoViewOptions() - { + PlatformView.StartBringItemIntoView(0, new BringIntoViewOptions() + { AnimationDesired = false, VerticalAlignmentRatio = 0.0, HorizontalAlignmentRatio = 0.0 @@ -428,13 +493,13 @@ void ApplyItemsUpdatingScrollMode() else if (VirtualView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepLastItemInView) { _isScrollingForItemsUpdate = true; - + // Use the view count to ensure we're scrolling to the correct last item var lastIndex = viewCount - 1; if (lastIndex >= 0) { _pendingScrollToIndex = lastIndex; - + // Use dispatcher to ensure the scroll happens after layout is fully complete VirtualView.Dispatcher.Dispatch(() => { @@ -446,17 +511,17 @@ void ApplyItemsUpdatingScrollMode() var currentViewCount = _collectionViewSource?.View?.Count ?? 0; var scrollIndex = Math.Min(_pendingScrollToIndex, currentViewCount - 1); - + if (scrollIndex >= 0) { - PlatformView.StartBringItemIntoView(scrollIndex, new BringIntoViewOptions() - { + PlatformView.StartBringItemIntoView(scrollIndex, new BringIntoViewOptions() + { AnimationDesired = false, VerticalAlignmentRatio = 1.0, HorizontalAlignmentRatio = 1.0 }); } - + _pendingScrollToIndex = -1; }); } @@ -589,14 +654,21 @@ void FindScrollViewer() return; } - void ListViewLoaded(object sender, RoutedEventArgs e) + // Unsubscribe any previous pending Loaded handler + if (_pendingLoadedHandler is not null) + { + PlatformView.Loaded -= _pendingLoadedHandler; + } + + _pendingLoadedHandler = (sender, e) => { var lv = (WItemsView)sender; - lv.Loaded -= ListViewLoaded; + lv.Loaded -= _pendingLoadedHandler; + _pendingLoadedHandler = null; FindScrollViewer(); - } + }; - PlatformView.Loaded += ListViewLoaded; + PlatformView.Loaded += _pendingLoadedHandler; } void OnScrollViewerFound() @@ -695,6 +767,7 @@ void UpdateEmptyView() } // Resolve empty view + var oldMauiEmptyView = _mauiEmptyView; _emptyView = emptyViewTemplate != null ? ItemsViewExtensions.RealizeEmptyViewTemplate(emptyView, emptyViewTemplate, MauiContext!, ref _mauiEmptyView) : emptyView switch @@ -709,6 +782,13 @@ void UpdateEmptyView() _ => ItemsViewExtensions.RealizeEmptyViewTemplate(emptyView, null, MauiContext!, ref _mauiEmptyView) }; + // Remove old logical child before adding the new one to prevent leak + if (oldMauiEmptyView is not null && _emptyViewDisplayed) + { + ItemsView.RemoveLogicalChild(oldMauiEmptyView); + _emptyViewDisplayed = false; + } + (PlatformView as IEmptyView)?.SetEmptyView(_emptyView, _mauiEmptyView); UpdateEmptyViewVisibility(); } @@ -726,13 +806,8 @@ void UpdateEmptyViewVisibility() if (isEmpty) { - if (_mauiEmptyView is not null) + if (_mauiEmptyView is not null && !_emptyViewDisplayed) { - if (_emptyViewDisplayed) - { - ItemsView.RemoveLogicalChild(_mauiEmptyView); - } - if (ItemsView.EmptyViewTemplate is null) { ItemsView.AddLogicalChild(_mauiEmptyView); @@ -791,6 +866,9 @@ void UpdateHeader() return; } + // Save old logical child reference before realization overwrites _mauiHeader via ref + var oldMauiHeader = _mauiHeader; + // If HeaderTemplate is set, use it regardless of header value if (headerTemplate is not null) { @@ -826,7 +904,12 @@ void UpdateHeader() platformItemsView.HeaderVisibility = WVisibility.Visible; } - // Add logical child for View + // Remove old logical child before adding the new one to prevent leak + if (oldMauiHeader is not null && _headerDisplayed) + { + ItemsView.RemoveLogicalChild(oldMauiHeader); + } + if (_mauiHeader is not null) { ItemsView.AddLogicalChild(_mauiHeader); @@ -864,6 +947,9 @@ void UpdateFooter() return; } + // Save old logical child reference before realization overwrites _mauiFooter via ref + var oldMauiFooter = _mauiFooter; + // If FooterTemplate is set, use it regardless of footer value if (footerTemplate is not null) { @@ -899,7 +985,12 @@ void UpdateFooter() platformItemsView.FooterVisibility = WVisibility.Visible; } - // Add logical child for View + // Remove old logical child before adding the new one to prevent leak + if (oldMauiFooter is not null && _footerDisplayed) + { + ItemsView.RemoveLogicalChild(oldMauiFooter); + } + if (_mauiFooter is not null) { ItemsView.AddLogicalChild(_mauiFooter); @@ -941,7 +1032,7 @@ void UpdateVerticalScrollBarVisibility() scrollView.VerticalScrollBarVisibility = ScrollingScrollBarVisibility.Hidden; break; case ScrollBarVisibility.Default: - scrollView.VerticalScrollBarVisibility =_defaultVerticalScrollVisibility.Value; + scrollView.VerticalScrollBarVisibility = _defaultVerticalScrollVisibility.Value; break; } @@ -975,7 +1066,7 @@ void UpdateHorizontalScrollBarVisibility() { case ScrollBarVisibility.Always: scrollView.HorizontalScrollBarVisibility = ScrollingScrollBarVisibility.Visible; - break; + break; case ScrollBarVisibility.Never: scrollView.HorizontalScrollBarVisibility = ScrollingScrollBarVisibility.Hidden; break; @@ -1007,7 +1098,7 @@ void ScrollViewChanged(UI.Xaml.Controls.ScrollView sender, object args) void HandleScroll(WScrollPresenter scrollViewer) { - if(_isScrollingForItemsUpdate) + if (_isScrollingForItemsUpdate) { _isScrollingForItemsUpdate = false; return; @@ -1028,10 +1119,14 @@ void HandleScroll(WScrollPresenter scrollViewer) switch (Layout) { case LinearItemsLayout linearItemsLayout: - advancing = itemsViewScrolledEventArgs.HorizontalDelta > 0; + advancing = linearItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal + ? itemsViewScrolledEventArgs.HorizontalDelta > 0 + : itemsViewScrolledEventArgs.VerticalDelta > 0; break; case GridItemsLayout gridItemsLayout: - advancing = itemsViewScrolledEventArgs.VerticalDelta > 0; + advancing = gridItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal + ? itemsViewScrolledEventArgs.HorizontalDelta > 0 + : itemsViewScrolledEventArgs.VerticalDelta > 0; break; default: break; @@ -1045,18 +1140,16 @@ void HandleScroll(WScrollPresenter scrollViewer) if (_collectionViewSource != null && remainingItemsThreshold > -1) { var itemsRemaining = _collectionViewSource.View.Count - 1 - itemsViewScrolledEventArgs.LastVisibleItemIndex; - - if(itemsRemaining<= remainingItemsThreshold) + + if (itemsRemaining <= remainingItemsThreshold) { - if (itemsViewScrolledEventArgs.LastVisibleItemIndex > _lastRemainingItemsThresholdIndex) + if (itemsViewScrolledEventArgs.LastVisibleItemIndex > _lastRemainingItemsThresholdIndex) { _lastRemainingItemsThresholdIndex = itemsViewScrolledEventArgs.LastVisibleItemIndex; - Element.SendRemainingItemsThresholdReached(); + Element.SendRemainingItemsThresholdReached(); } - } - - } + } } ItemsViewScrolledEventArgs ComputeVisibleIndexes(ItemsViewScrolledEventArgs args, bool advancing) diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs index a8ad8882f5df..1e70e9af16e5 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs @@ -29,7 +29,7 @@ public CollectionViewHandler2(PropertyMapper? mapper = null) : base(mapper ?? Ma [SelectableItemsView.SelectedItemsProperty.PropertyName] = MapSelectedItems, [SelectableItemsView.SelectionModeProperty.PropertyName] = MapSelectionMode, [StructuredItemsView.ItemSizingStrategyProperty.PropertyName] = MapItemSizingStrategy, - + }; } public partial class CollectionViewHandler2 : ItemsViewHandler2 @@ -291,8 +291,6 @@ void UpdatePlatformSelection() return; } - _ignorePlatformSelectionChange = true; - var itemList = PlatformView.ItemsSource as ICollectionView; if (itemList is null) @@ -300,6 +298,8 @@ void UpdatePlatformSelection() return; } + _ignorePlatformSelectionChange = true; + switch (PlatformView.SelectionMode) { case ItemsViewSelectionMode.Single: @@ -333,7 +333,7 @@ void UpdatePlatformSelection() break; case ItemsViewSelectionMode.Multiple: PlatformView.DeselectAll(); - + // Use safe enumeration to avoid ArgumentOutOfRangeException during collection updates int index = 0; foreach (var nativeItem in itemList) diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs index fedb19cd67ee..ff14774ab173 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs @@ -261,11 +261,13 @@ void HandleGroupItemsRemove(NotifyCollectionChangedEventArgs e, int flatIndex) int removeIndex = flatIndex + (e.OldStartingIndex >= 0 ? e.OldStartingIndex : 0); var removedItems = new List(e.OldItems.Count); + _suppressNotifications = true; for (int i = 0; i < e.OldItems.Count; i++) { removedItems.Add(Items[removeIndex]); Items.RemoveAt(removeIndex); } + _suppressNotifications = false; OnCollectionChanged(new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Remove, removedItems, removeIndex)); @@ -341,5 +343,19 @@ public void Reset() SubscribeToGroups(_itemsSource); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } + + /// + /// Unsubscribes from all group and top-level collection changed events. + /// Must be called when the collection is being replaced or the handler disconnects. + /// + public void CleanUp() + { + UnsubscribeFromAllGroups(); + + if (_itemsSource is INotifyCollectionChanged incc) + { + incc.CollectionChanged -= GroupsChanged; + } + } } } diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs index b0be3a156643..fc28cb9c5a8f 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs @@ -58,13 +58,9 @@ internal partial class ItemFactory(ItemsView view) : IElementFactory wrapper.VerticalAlignment = VerticalAlignment.Stretch; wrapper.VerticalContentAlignment = VerticalAlignment.Stretch; wrapper.SetContent(viewContent); + wrapper.IsHeaderOrFooter = templateContext.IsHeader || templateContext.IsFooter; - if(wrapper is not null) - { - wrapper.IsHeaderOrFooter = templateContext.IsHeader || templateContext.IsFooter; - } - - if (wrapper?.VirtualView is View virtualView) + if (wrapper.VirtualView is View virtualView) { virtualView.SetValue(OriginTemplateProperty, template); } @@ -124,15 +120,38 @@ public void RecycleElement(ElementFactoryRecycleArgs args) _view.RemoveLogicalChild(wrapperView); } + + /// + /// Clears the recycle pool and removes logical children held by pooled elements. + /// Must be called when the items source changes or when the handler disconnects + /// to prevent memory leaks from pooled ItemContainers holding strong references. + /// + internal void CleanUp() + { + foreach (var kvp in _recyclePool) + { + foreach (var container in kvp.Value) + { + var wrapper = container?.Child as ElementWrapper; + var wrapperView = wrapper?.VirtualView as View; + if (wrapperView is not null) + { + _view.RemoveLogicalChild(wrapperView); + } + } + } + + _recyclePool.Clear(); + } } internal partial class ElementWrapper(IMauiContext context) : ContentControl { public IView? VirtualView { get; private set; } - + private IMauiContext _context = context; - public bool IsHeaderOrFooter { get; set; } + public bool IsHeaderOrFooter { get; set; } public void SetContent(IView view) { From be82cebbbf9d2f8ac2bd23e4f535c70d2f152921 Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:38:14 +0530 Subject: [PATCH 032/109] Fix orientation and KeepScrollOffset --- .../Items2/ItemsViewHandler2.Windows.cs | 15 ++++++++++---- .../Handlers/Items2/Windows/MauiItemsView.cs | 20 ++++++++++++++++--- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index 5e940d399926..87cccd76ea46 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -454,6 +454,11 @@ void ApplyItemsUpdatingScrollMode() return; } + if (VirtualView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepScrollOffset) + { + return; + } + if (_itemsSource is null || _itemsSource.Count == 0) { return; @@ -574,12 +579,8 @@ protected void UpdateItemsLayout() _layoutPropertyChangedProxy = new WeakNotifyPropertyChangedProxy(Layout, _layoutPropertyChanged); } - UpdateItemsSource(); - PlatformView.Layout = CreateItemsLayout(); - bool isHorizontal = IsLayoutHorizontal; - // Update header/footer orientation if (PlatformView is MauiItemsView mauiItemsView) { @@ -1148,6 +1149,12 @@ void HandleScroll(WScrollPresenter scrollViewer) _lastRemainingItemsThresholdIndex = itemsViewScrolledEventArgs.LastVisibleItemIndex; Element.SendRemainingItemsThresholdReached(); } + else + { + //Reset when scrolling back up away from the threshold zone so the + //event can fire again if the user scrolls back down + _lastRemainingItemsThresholdIndex = -1; + } } } } diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs b/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs index 166794ea4f94..b6d668526646 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs @@ -181,11 +181,18 @@ void ApplyLayoutOrientation() if (_isHorizontalLayout) { _containerPanel.Orientation = Orientation.Horizontal; - _itemsRepeater.VerticalAlignment = UI.Xaml.VerticalAlignment.Top; + // For horizontal layout, the container panel should stretch vertically + _containerPanel.VerticalAlignment = UI.Xaml.VerticalAlignment.Stretch; + _containerPanel.HorizontalAlignment = UI.Xaml.HorizontalAlignment.Left; + + // Items should stretch vertically (cross-axis) + _itemsRepeater.VerticalAlignment = UI.Xaml.VerticalAlignment.Stretch; _itemsRepeater.HorizontalAlignment = UI.Xaml.HorizontalAlignment.Left; - _headerContentControl.VerticalContentAlignment = UI.Xaml.VerticalAlignment.Top; + _headerContentControl.VerticalAlignment = UI.Xaml.VerticalAlignment.Stretch; + _headerContentControl.VerticalContentAlignment = UI.Xaml.VerticalAlignment.Stretch; _headerContentControl.HorizontalContentAlignment = UI.Xaml.HorizontalAlignment.Left; - _footerContentControl.VerticalContentAlignment = UI.Xaml.VerticalAlignment.Top; + _footerContentControl.VerticalAlignment = UI.Xaml.VerticalAlignment.Stretch; + _footerContentControl.VerticalContentAlignment = UI.Xaml.VerticalAlignment.Stretch; _footerContentControl.HorizontalContentAlignment = UI.Xaml.HorizontalAlignment.Left; if (_scrollView is not null) @@ -196,10 +203,17 @@ void ApplyLayoutOrientation() else { _containerPanel.Orientation = Orientation.Vertical; + // For vertical layout, the container panel should stretch horizontally + _containerPanel.VerticalAlignment = UI.Xaml.VerticalAlignment.Top; + _containerPanel.HorizontalAlignment = UI.Xaml.HorizontalAlignment.Stretch; + + // Items should stretch horizontally (cross-axis) _itemsRepeater.VerticalAlignment = UI.Xaml.VerticalAlignment.Top; _itemsRepeater.HorizontalAlignment = UI.Xaml.HorizontalAlignment.Stretch; + _headerContentControl.VerticalAlignment = UI.Xaml.VerticalAlignment.Top; _headerContentControl.VerticalContentAlignment = UI.Xaml.VerticalAlignment.Top; _headerContentControl.HorizontalContentAlignment = UI.Xaml.HorizontalAlignment.Stretch; + _footerContentControl.VerticalAlignment = UI.Xaml.VerticalAlignment.Top; _footerContentControl.VerticalContentAlignment = UI.Xaml.VerticalAlignment.Top; _footerContentControl.HorizontalContentAlignment = UI.Xaml.HorizontalAlignment.Stretch; From 29d317be477485b0b907c03251529f656abcc27d Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Thu, 12 Feb 2026 15:52:53 +0530 Subject: [PATCH 033/109] SnapPoints --- .../Items2/ItemsViewHandler2.Windows.cs | 171 +++++++++++++++++- 1 file changed, 165 insertions(+), 6 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index 87cccd76ea46..590b7eb4773b 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -17,6 +17,7 @@ using WASDKScrollBarVisibility = Microsoft.UI.Xaml.Controls.ScrollBarVisibility; using WItemsView = Microsoft.UI.Xaml.Controls.ItemsView; using WScrollPresenter = Microsoft.UI.Xaml.Controls.Primitives.ScrollPresenter; +using WScrollSnapPointsAlignment = Microsoft.UI.Xaml.Controls.Primitives.ScrollSnapPointsAlignment; using WVisibility = Microsoft.UI.Xaml.Visibility; namespace Microsoft.Maui.Controls.Handlers.Items2 @@ -203,6 +204,7 @@ protected override void DisconnectHandler(WItemsView platformView) { platformView.ScrollView.ViewChanged -= ScrollViewChanged; platformView.ScrollView.PointerWheelChanged -= PointerScrollChanged; + platformView.ScrollView.ExtentChanged -= ScrollViewExtentChanged; } // Unsubscribe pending Loaded handler if ScrollView wasn't found yet @@ -266,6 +268,9 @@ protected override void DisconnectHandler(WItemsView platformView) _footer = null; _footerDisplayed = false; + _cachedSnapPointOffsets = null; + _lastSnapPointExtent = 0; + base.DisconnectHandler(platformView); } @@ -681,12 +686,15 @@ void OnScrollViewerFound() PlatformView.ScrollView.ViewChanged -= ScrollViewChanged; PlatformView.ScrollView.PointerWheelChanged -= PointerScrollChanged; + PlatformView.ScrollView.ExtentChanged -= ScrollViewExtentChanged; PlatformView.ScrollView.ViewChanged += ScrollViewChanged; PlatformView.ScrollView.PointerWheelChanged += PointerScrollChanged; + PlatformView.ScrollView.ExtentChanged += ScrollViewExtentChanged; UpdateVerticalScrollBarVisibility(); UpdateHorizontalScrollBarVisibility(); + UpdateSnapPoints(); } void LayoutPropertyChanged(object? sender, PropertyChangedEventArgs e) @@ -703,6 +711,11 @@ void LayoutPropertyChanged(object? sender, PropertyChangedEventArgs e) { UpdateItemsLayoutItemSpacing(); } + else if (e.PropertyName == nameof(ItemsLayout.SnapPointsType) || + e.PropertyName == nameof(ItemsLayout.SnapPointsAlignment)) + { + UpdateSnapPoints(); + } } void UpdateItemsLayoutSpan() @@ -729,6 +742,148 @@ void UpdateItemsLayoutItemSpacing() } } + double _lastSnapPointExtent; + List? _cachedSnapPointOffsets; + SnapPointsType _cachedSnapPointsType; + SnapPointsAlignment _cachedSnapPointsAlignment; + + void ScrollViewExtentChanged(Microsoft.UI.Xaml.Controls.ScrollView sender, object args) + { + // Only recalculate snap points when the extent actually changes + // to avoid redundant work during intermediate layout passes. + var currentExtent = IsLayoutHorizontal ? sender.ExtentWidth : sender.ExtentHeight; + if (currentExtent != _lastSnapPointExtent) + { + _lastSnapPointExtent = currentExtent; + UpdateSnapPoints(); + } + } + + void UpdateSnapPoints() + { + if (PlatformView?.ScrollView?.ScrollPresenter is not WScrollPresenter scrollPresenter) + { + return; + } + + if (Layout is not ItemsLayout itemsLayout) + { + return; + } + + var snapPointsType = itemsLayout.SnapPointsType; + + if (snapPointsType == SnapPointsType.None) + { + scrollPresenter.HorizontalSnapPoints.Clear(); + scrollPresenter.VerticalSnapPoints.Clear(); + _cachedSnapPointOffsets = null; + _cachedSnapPointsType = SnapPointsType.None; + return; + } + + // NOTE: MandatorySingle is treated identically to Mandatory. + // WinUI's ScrollPresenter snap point object model does not natively support + // "single item per gesture" limiting. Both Mandatory and MandatorySingle + // will snap to the nearest snap point, but a fling gesture can skip past + // multiple items. A true MandatorySingle implementation would require + // intercepting scroll inertia to limit travel distance, which is not + // supported by the current ScrollPresenter API. + + var itemCount = _collectionViewSource?.View?.Count ?? 0; + if (itemCount == 0) + { + scrollPresenter.HorizontalSnapPoints.Clear(); + scrollPresenter.VerticalSnapPoints.Clear(); + _cachedSnapPointOffsets = null; + return; + } + + var snapAlignment = itemsLayout.SnapPointsAlignment; + + // Compute offsets from realized item containers. + // Uses actual measured sizes to handle variable-size items, + // complex templates, and async image loading. + double runningOffset = 0; + double spacing = 0; + var newOffsets = new List(); + + if (Layout is LinearItemsLayout linearLayout) + { + spacing = linearLayout.ItemSpacing; + } + else if (Layout is GridItemsLayout gridLayout) + { + spacing = IsLayoutHorizontal ? gridLayout.HorizontalItemSpacing : gridLayout.VerticalItemSpacing; + } + + foreach (var container in PlatformView.GetChildren()) + { + if (container is null) + { + continue; + } + + double itemExtent = IsLayoutHorizontal ? container.ActualWidth : container.ActualHeight; + if (itemExtent <= 0) + { + continue; + } + + newOffsets.Add(runningOffset); + runningOffset += itemExtent + spacing; + } + + if (newOffsets.Count == 0) + { + // Items not yet realized; snap points will be applied + // when ExtentChanged fires after items are laid out. + return; + } + + // Skip rebuild if snap points haven't changed — avoids redundant + // WinUI collection clears and allocations on every ExtentChanged. + if (_cachedSnapPointOffsets is not null && + _cachedSnapPointsType == snapPointsType && + _cachedSnapPointsAlignment == snapAlignment && + _cachedSnapPointOffsets.Count == newOffsets.Count && + _cachedSnapPointOffsets.SequenceEqual(newOffsets)) + { + return; + } + + // Rebuild snap points — clear both axes to handle orientation changes + var alignment = GetScrollSnapPointsAlignment(snapAlignment); + + scrollPresenter.HorizontalSnapPoints.Clear(); + scrollPresenter.VerticalSnapPoints.Clear(); + + var snapPoints = IsLayoutHorizontal + ? scrollPresenter.HorizontalSnapPoints + : scrollPresenter.VerticalSnapPoints; + + foreach (var offset in newOffsets) + { + snapPoints.Add(new Microsoft.UI.Xaml.Controls.Primitives.ScrollSnapPoint( + offset, alignment)); + } + + _cachedSnapPointOffsets = newOffsets; + _cachedSnapPointsType = snapPointsType; + _cachedSnapPointsAlignment = snapAlignment; + } + + static WScrollSnapPointsAlignment GetScrollSnapPointsAlignment(SnapPointsAlignment alignment) + { + return alignment switch + { + SnapPointsAlignment.Start => WScrollSnapPointsAlignment.Near, + SnapPointsAlignment.Center => WScrollSnapPointsAlignment.Center, + SnapPointsAlignment.End => WScrollSnapPointsAlignment.Far, + _ => WScrollSnapPointsAlignment.Near, + }; + } + void UpdateEmptyView() { if (Element is null || PlatformView is null) @@ -1149,12 +1304,16 @@ void HandleScroll(WScrollPresenter scrollViewer) _lastRemainingItemsThresholdIndex = itemsViewScrolledEventArgs.LastVisibleItemIndex; Element.SendRemainingItemsThresholdReached(); } - else - { - //Reset when scrolling back up away from the threshold zone so the - //event can fire again if the user scrolls back down - _lastRemainingItemsThresholdIndex = -1; - } + // When scrolling backward within the threshold zone, keep the + // high-water mark — don't reset, to avoid duplicate event fires. + // The reset happens in the outer else (when leaving the zone entirely) + // or in ItemsChanged (when new items are added). + } + else + { + // Reset when scrolling away from the threshold zone so the + // event can re-fire when the user scrolls back. + _lastRemainingItemsThresholdIndex = -1; } } } From c23a2e21190f062fa2792a42727e30dac4231a66 Mon Sep 17 00:00:00 2001 From: devanathan-vaithiyanathan <114395405+devanathan-vaithiyanathan@users.noreply.github.com> Date: Mon, 16 Feb 2026 17:08:21 +0530 Subject: [PATCH 034/109] fixed 33487 --- .../Windows/CollectionViewHandler2.Windows.cs | 24 +++++++++---------- .../Handlers/Items2/Windows/ItemFactory.cs | 16 ++++++------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs index 0e3b89699227..a1610f93dade 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs @@ -246,7 +246,7 @@ void UpdateVisualStates() if (itemcontainer?.Child is ElementWrapper wrapper && wrapper.VirtualView is VisualElement visualElement) { var actualItem = visualElement.BindingContext; - bool isSelected = ItemsView.SelectedItem == actualItem || ItemsView.SelectedItems.Contains(actualItem); + bool isSelected = object.Equals(ItemsView.SelectedItem, actualItem) || ItemsView.SelectedItems.Contains(actualItem); VisualStateManager.GoToState(visualElement, isSelected ? VisualStateManager.CommonStates.Selected : VisualStateManager.CommonStates.Normal); } } @@ -347,19 +347,17 @@ void UpdatePlatformSelection() } else { - var selectedItem = itemList.FirstOrDefault(item => + var selectedItem = itemList.FirstOrDefault(item => + { + if (item is ItemTemplateContext2 itemPair) + { + return object.Equals(itemPair.Item, ItemsView.SelectedItem); + } + else { - if (item is ItemTemplateContext2 itemPair) - { - return itemPair.Item == ItemsView.SelectedItem; - } - else - { - return item == ItemsView.SelectedItem; - } - }); - - if (selectedItem is not null) + return object.Equals(item, ItemsView.SelectedItem); + } + }); if (selectedItem is not null) { PlatformView.Select(itemList.IndexOf(selectedItem)); } diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs index 77f2bfa6a3cf..5b55f692e1e5 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs @@ -76,15 +76,13 @@ internal partial class ItemFactory(ItemsView view) : IElementFactory { view.BindingContext = templateContext.Item ?? _view.BindingContext; _view.AddLogicalChild(view); - if (_view is SelectableItemsView selectableItemsView && selectableItemsView.SelectionMode != SelectionMode.None) - { - bool isSelected = false; - if (selectableItemsView.SelectionMode == SelectionMode.Single) - isSelected = selectableItemsView.SelectedItem == templateContext.Item; - else - isSelected = selectableItemsView.SelectedItems.Contains(templateContext.Item); - - if (isSelected && view is VisualElement visualElement) + if (_view is SelectableItemsView selectableItemsView && selectableItemsView.SelectionMode != SelectionMode.None) + { + bool isSelected = false; + if (selectableItemsView.SelectionMode == SelectionMode.Single) + isSelected = object.Equals(selectableItemsView.SelectedItem, templateContext.Item); + else + isSelected = selectableItemsView.SelectedItems.Contains(templateContext.Item); if (isSelected && view is VisualElement visualElement) { VisualStateManager.GoToState(visualElement, VisualStateManager.CommonStates.Selected); } From 2640311766ea9b71d881c5c2f5749dd018a88f93 Mon Sep 17 00:00:00 2001 From: devanathan-vaithiyanathan <114395405+devanathan-vaithiyanathan@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:20:09 +0530 Subject: [PATCH 035/109] Fixed MeasureFirstItem with Template changes --- .../Core/Handlers/Items2/ItemsViewHandler2.Windows.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index b9b03d7cc979..5257e0cdf5d0 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -112,6 +112,15 @@ public static void MapVerticalScrollBarVisibility(ItemsViewHandler2 public static void MapItemTemplate(ItemsViewHandler2 handler, ItemsView itemsView) { + // Only invalidate cache if: + // 1. ItemSizingStrategy is MeasureFirstItem + // 2. Control is already loaded (runtime template change, not initial load) + if (handler is CollectionViewHandler2 cvHandler && itemsView is CollectionView cv && + cv.ItemSizingStrategy == ItemSizingStrategy.MeasureFirstItem && itemsView.IsLoadedOnPlatform()) + { + cvHandler.InvalidateFirstItemSize(); + } + handler.UpdateItemsSource(); } From 6216ead10e2f38918b9f16bceafcddd6b9a64b37 Mon Sep 17 00:00:00 2001 From: devanathan-vaithiyanathan <114395405+devanathan-vaithiyanathan@users.noreply.github.com> Date: Fri, 20 Feb 2026 15:22:40 +0530 Subject: [PATCH 036/109] Fix for Unloaded CV with ScrollTo Operation --- src/Controls/src/Core/Items/ItemsView.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Controls/src/Core/Items/ItemsView.cs b/src/Controls/src/Core/Items/ItemsView.cs index 5d2aee9be013..0e7f7c70edf4 100644 --- a/src/Controls/src/Core/Items/ItemsView.cs +++ b/src/Controls/src/Core/Items/ItemsView.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; +using System.Linq; using System.Windows.Input; using Microsoft.Maui.Controls.Internals; using Microsoft.Maui.Controls.Xaml.Diagnostics; @@ -223,6 +224,9 @@ public ItemsUpdatingScrollMode ItemsUpdatingScrollMode public void ScrollTo(int index, int groupIndex = -1, ScrollToPosition position = ScrollToPosition.MakeVisible, bool animate = true) { + if (DismissScroll()) + return; + OnScrollToRequested(new ScrollToRequestEventArgs(index, groupIndex, position, animate)); } @@ -236,6 +240,9 @@ public void ScrollTo(int index, int groupIndex = -1, public void ScrollTo(object item, object group = null, ScrollToPosition position = ScrollToPosition.MakeVisible, bool animate = true) { + if (DismissScroll()) + return; + OnScrollToRequested(new ScrollToRequestEventArgs(item, group, position, animate)); } @@ -312,5 +319,10 @@ private protected override string GetDebuggerDisplay() var itemsSourceText = DebuggerDisplayHelpers.GetDebugText(nameof(ItemsSource), ItemsSource?.GetType()); return $"{base.GetDebuggerDisplay()}, {itemsSourceText}"; } + + private bool DismissScroll() + { + return ItemsSource is null || (ItemsSource is IEnumerable items && !items.Any()); + } } } From 744fe754c56552c492da2f3fd0d56df2a55799a9 Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Tue, 24 Feb 2026 10:47:50 +0530 Subject: [PATCH 037/109] Update CollectionViewHandler2.Windows.cs --- .../Items2/Windows/CollectionViewHandler2.Windows.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs index 1e70e9af16e5..309bfa20468e 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs @@ -73,15 +73,17 @@ public static void MapItemsSource(CollectionViewHandler2 handler, SelectableItem public static void MapSelectedItem(CollectionViewHandler2 handler, SelectableItemsView itemsView) { - + handler.UpdatePlatformSelection(); } public static void MapSelectedItems(CollectionViewHandler2 handler, SelectableItemsView itemsView) { + handler.UpdatePlatformSelection(); } public static void MapSelectionMode(CollectionViewHandler2 handler, SelectableItemsView itemsView) { + handler.UpdatePlatformSelection(); } /// @@ -210,7 +212,7 @@ void UpdateVisualStates() if (itemcontainer?.Child is ElementWrapper wrapper && wrapper.VirtualView is VisualElement visualElement) { var actualItem = visualElement.BindingContext; - bool isSelected = ItemsView.SelectedItem == actualItem || ItemsView.SelectedItems.Contains(actualItem); + bool isSelected = object.Equals(ItemsView.SelectedItem, actualItem) || ItemsView.SelectedItems.Contains(actualItem); VisualStateManager.GoToState(visualElement, isSelected ? VisualStateManager.CommonStates.Selected : VisualStateManager.CommonStates.Normal); } } @@ -315,11 +317,11 @@ void UpdatePlatformSelection() { if (item is ItemTemplateContext2 itemPair) { - return itemPair.Item == ItemsView.SelectedItem; + return object.Equals(itemPair.Item, ItemsView.SelectedItem); } else { - return item == ItemsView.SelectedItem; + return object.Equals(item, ItemsView.SelectedItem); } }); From 77ea32c937f6f80ba9a497c458123baafc8f0b48 Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Tue, 24 Feb 2026 18:52:24 +0530 Subject: [PATCH 038/109] commit for review concern --- .../Items2/ItemsViewHandler2.Windows.cs | 51 +++++-- .../Windows/CollectionViewHandler2.Windows.cs | 128 +++++++++++------- .../Windows/GroupedItemTemplateCollection2.cs | 4 + .../Handlers/Items2/Windows/ItemFactory.cs | 20 +++ .../Items2/Windows/ItemTemplateContext2.cs | 21 +++ .../Windows/ItemTemplateContextEnumerable2.cs | 5 + .../Windows/ItemTemplateContextList2.cs | 6 + .../Handlers/Items2/Windows/MauiItemsView.cs | 17 ++- .../ObservableItemTemplateCollection2.cs | 11 +- .../Windows/TemplatedItemSourceFactory2.cs | 12 ++ 10 files changed, 210 insertions(+), 65 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index 590b7eb4773b..705454537f97 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -22,8 +22,27 @@ namespace Microsoft.Maui.Controls.Handlers.Items2 { + /// + /// Base handler for ItemsView controls on Windows, providing item source management, + /// layout, scrolling, snap points, empty views, headers, and footers. + /// public abstract class ItemsViewHandler2 : ViewHandler where TItemsView : ItemsView { + /// + /// Alignment ratio that positions the item at the start (top/left) of the viewport. + /// + const double AlignToStart = 0.0; + + /// + /// Alignment ratio that positions the item at the center of the viewport. + /// + const double AlignToCenter = 0.5; + + /// + /// Alignment ratio that positions the item at the end (bottom/right) of the viewport. + /// + const double AlignToEnd = 1.0; + CollectionViewSource? _collectionViewSource; IList? _itemsSource; ItemFactory? _itemFactory; @@ -92,7 +111,7 @@ public ItemsViewHandler2(PropertyMapper? mapper = null) : base(mapper ?? ItemsVi [Controls.StructuredItemsView.FooterTemplateProperty.PropertyName] = MapFooterTemplate, }; - private bool _scrollUpdatePending; + bool _scrollUpdatePending; public static void MapItemsSource(ItemsViewHandler2 handler, ItemsView itemsView) { @@ -228,9 +247,17 @@ protected override void DisconnectHandler(WItemsView platformView) VirtualView.ScrollToRequested -= ScrollToRequested; } - // Safe subscription cleanup only — do NOT call CleanUpCollectionViewSource() here - // because it sets _collectionViewSource.Source = null and accesses PlatformView.GetChildren, - // which triggers WinUI side effects (element recycling, collection changes) during teardown. + // Safe subscription cleanup only — do NOT call CleanUpCollectionViewSource() here. + // + // CleanUpCollectionViewSource() performs two operations that trigger WinUI side effects + // during teardown: + // 1. Sets _collectionViewSource.Source = null → causes WinUI to recycle/release elements + // 2. Calls PlatformView.GetChildren() → triggers element enumeration + // while the ItemsRepeater is tearing down, leading to collection change notifications + // and potential InvalidOperationException. + // + // The CleanUp() methods below are safe because they ONLY unsubscribe event handlers + // (CollectionChanged, group change notifications) without touching WinUI state. if (_collectionViewSource?.Source is ObservableItemTemplateCollection2 observableCollection) { observableCollection.CleanUp(); @@ -495,8 +522,8 @@ void ApplyItemsUpdatingScrollMode() PlatformView.StartBringItemIntoView(0, new BringIntoViewOptions() { AnimationDesired = false, - VerticalAlignmentRatio = 0.0, - HorizontalAlignmentRatio = 0.0 + VerticalAlignmentRatio = AlignToStart, + HorizontalAlignmentRatio = AlignToStart }); }); } @@ -527,8 +554,8 @@ void ApplyItemsUpdatingScrollMode() PlatformView.StartBringItemIntoView(scrollIndex, new BringIntoViewOptions() { AnimationDesired = false, - VerticalAlignmentRatio = 1.0, - HorizontalAlignmentRatio = 1.0 + VerticalAlignmentRatio = AlignToEnd, + HorizontalAlignmentRatio = AlignToEnd }); } @@ -1383,17 +1410,17 @@ ItemsView is GroupableItemsView groupableItemsView && return; } - float offset = 0.0f; + double offset = AlignToStart; switch (args.ScrollToPosition) { case ScrollToPosition.Start: - offset = 0.0f; + offset = AlignToStart; break; case ScrollToPosition.Center: - offset = 0.5f; + offset = AlignToCenter; break; case ScrollToPosition.End: - offset = 1.0f; + offset = AlignToEnd; break; } diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs index 309bfa20468e..7dc4724daf3d 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Microsoft.Maui.Controls.Platform; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Data; @@ -166,11 +165,17 @@ protected override void UpdateItemsSource() _ignorePlatformSelectionChange = false; } + /// + /// Handles changes to the virtual (MAUI) selection and synchronizes them to the platform. + /// void VirtualSelectionChanged(object? sender, SelectionChangedEventArgs? e) { UpdatePlatformSelection(); } + /// + /// Handles changes to the platform (WinUI) selection and synchronizes them to the virtual view. + /// void PlatformSelectionChanged(WItemsView sender, ItemsViewSelectionChangedEventArgs args) { if (PlatformView is null) @@ -179,6 +184,9 @@ void PlatformSelectionChanged(WItemsView sender, ItemsViewSelectionChangedEventA UpdateVirtualSelection(); } + /// + /// Reads the current platform selection state and updates the virtual (MAUI) selection accordingly. + /// void UpdateVirtualSelection() { if (_ignorePlatformSelectionChange || ItemsView is null || PlatformView is null) @@ -240,29 +248,24 @@ void UpdateVirtualMultipleSelection() ItemsView.SelectionChanged -= VirtualSelectionChanged; - // Get current platform selection - var currentPlatformSelection = new HashSet(); - foreach (var item in PlatformView.SelectedItems) - { - var selectedItem = item is ItemTemplateContext2 itc ? itc.Item : item; - if (selectedItem is not null) - currentPlatformSelection.Add(selectedItem); - } - - // Get previous virtual selection - var previousSelection = new HashSet(ItemsView.SelectedItems); + var newSelection = ComputeNewMultipleSelection(); + ItemsView.UpdateSelectedItems(newSelection); - // Find newly selected items (in platform but not in virtual) - var newlySelected = new List(); - foreach (var item in PlatformView.SelectedItems) - { - var selectedItem = item is ItemTemplateContext2 itc ? itc.Item : item; - if (selectedItem is not null && !previousSelection.Contains(selectedItem)) - newlySelected.Add(selectedItem); - } + ItemsView.SelectionChanged += VirtualSelectionChanged; + } - // Build new selection: existing items still selected + newly selected items + /// + /// Computes the new multiple selection list by merging the current platform selection + /// with the existing virtual selection, preserving the order of previously selected items + /// and appending newly selected items at the end. + /// + List ComputeNewMultipleSelection() + { + // Extract actual items from platform selection (unwrapping ItemTemplateContext2) + var currentPlatformSelection = ExtractPlatformSelectedItems(); + var previousSelection = new HashSet(ItemsView.SelectedItems); var newSelection = new List(); + var addedToSelection = new HashSet(); // Keep existing items that are still selected (maintains their order) foreach (var existingItem in ItemsView.SelectedItems) @@ -270,22 +273,41 @@ void UpdateVirtualMultipleSelection() if (currentPlatformSelection.Contains(existingItem)) { newSelection.Add(existingItem); + addedToSelection.Add(existingItem); } } - // Add newly selected items at the end (in selection order) - foreach (var item in newlySelected) + // Append newly selected items (in platform but not in previous virtual selection) + foreach (var item in currentPlatformSelection) { - if (!newSelection.Contains(item)) + if (!previousSelection.Contains(item) && !addedToSelection.Contains(item)) { newSelection.Add(item); } } - ItemsView.UpdateSelectedItems(newSelection); - ItemsView.SelectionChanged += VirtualSelectionChanged; + return newSelection; } + /// + /// Extracts the actual data items from the platform's selected items, + /// unwrapping wrappers when present. + /// + HashSet ExtractPlatformSelectedItems() + { + var result = new HashSet(); + foreach (var item in PlatformView.SelectedItems) + { + var selectedItem = item is ItemTemplateContext2 itc ? itc.Item : item; + if (selectedItem is not null) + result.Add(selectedItem); + } + return result; + } + + /// + /// Reads the current virtual (MAUI) selection state and updates the platform (WinUI) selection accordingly. + /// void UpdatePlatformSelection() { if (PlatformView is null || ItemsView is null) @@ -305,30 +327,16 @@ void UpdatePlatformSelection() switch (PlatformView.SelectionMode) { case ItemsViewSelectionMode.Single: - if (ItemsView is not null) + if (ItemsView.SelectedItem is null) { - if (ItemsView.SelectedItem is null) - { - PlatformView.DeselectAll(); - } - else + PlatformView.DeselectAll(); + } + else + { + var selectedIndex = FindItemIndexInSource(itemList, ItemsView.SelectedItem); + if (selectedIndex >= 0) { - var selectedItem = itemList.FirstOrDefault(item => - { - if (item is ItemTemplateContext2 itemPair) - { - return object.Equals(itemPair.Item, ItemsView.SelectedItem); - } - else - { - return object.Equals(item, ItemsView.SelectedItem); - } - }); - - if (selectedItem is not null) - { - PlatformView.Select(itemList.IndexOf(selectedItem)); - } + PlatformView.Select(selectedIndex); } } @@ -360,10 +368,33 @@ void UpdatePlatformSelection() _ignorePlatformSelectionChange = false; UpdateVisualStates(); } + + /// + /// Finds the index of the specified item in the collection view source, using a single-pass + /// iteration. Returns -1 if the item is not found. + /// + static int FindItemIndexInSource(ICollectionView itemList, object targetItem) + { + int index = 0; + foreach (var nativeItem in itemList) + { + var actualItem = nativeItem is ItemTemplateContext2 itc ? itc.Item : nativeItem; + if (object.Equals(actualItem, targetItem)) + { + return index; + } + index++; + } + return -1; + } } +/// +/// Converts between MAUI and WinUI values. +/// partial class SelectionModeConvert : UI.Xaml.Data.IValueConverter { + /// public object Convert(object value, Type targetType, object parameter, string language) { var formSelectionMode = (SelectionMode)value; @@ -378,6 +409,7 @@ public object Convert(object value, Type targetType, object parameter, string la } } + /// public object ConvertBack(object value, Type targetType, object parameter, string language) { var uwpListViewSelectionMode = (ItemsViewSelectionMode)value; diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs index ff14774ab173..0fb8e63c643f 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs @@ -5,6 +5,10 @@ namespace Microsoft.Maui.Controls.Handlers.Items2 { + /// + /// An observable collection that flattens grouped items into a single list with + /// header/footer contexts per group, keeping in sync with the source via INCC. + /// internal class GroupedItemTemplateCollection2 : ObservableCollection { readonly IEnumerable _itemsSource; diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs index fc28cb9c5a8f..4dd4c64097d5 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs @@ -5,6 +5,10 @@ namespace Microsoft.Maui.Controls.Handlers.Items2 { + /// + /// Element factory that creates, recycles, and manages elements + /// for the WinUI ItemsView/ItemsRepeater, using a template-keyed recycle pool. + /// internal partial class ItemFactory(ItemsView view) : IElementFactory { private readonly ItemsView _view = view; @@ -14,6 +18,9 @@ internal partial class ItemFactory(ItemsView view) : IElementFactory BindableProperty.CreateAttached( "OriginTemplate", typeof(DataTemplate), typeof(ItemFactory), null); + /// + /// Creates or retrieves a recycled for the given data context. + /// public UIElement? GetElement(ElementFactoryGetArgs args) { // NOTE: 1.6: replace w/ RecyclePool @@ -100,6 +107,9 @@ internal partial class ItemFactory(ItemsView view) : IElementFactory return null; } + /// + /// Returns an element to the recycle pool, keyed by its original . + /// public void RecycleElement(ElementFactoryRecycleArgs args) { var item = args.Element as ItemContainer; @@ -145,14 +155,24 @@ internal void CleanUp() } } + /// + /// A wrapper that hosts a MAUI inside a WinUI element tree. + /// Handles MeasureFirstItem optimization by caching the first measured size. + /// internal partial class ElementWrapper(IMauiContext context) : ContentControl { + /// The MAUI virtual view hosted by this wrapper. public IView? VirtualView { get; private set; } private IMauiContext _context = context; + /// Whether this wrapper hosts a group header or footer (excluded from size caching). public bool IsHeaderOrFooter { get; set; } + /// + /// Sets the MAUI view content, converting it to a platform element. + /// Only sets content if not already initialized. + /// public void SetContent(IView view) { if (VirtualView is null || VirtualView.Handler is null) diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs index b9e26488a25b..dc62a6b0f819 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs @@ -2,18 +2,39 @@ namespace Microsoft.Maui.Controls.Handlers.Items2; +/// +/// Pairs a data item with its and layout metadata. +/// Used by the WinUI ItemsView/ItemsRepeater as the data context for each realized element. +/// internal class ItemTemplateContext2 { readonly WeakReference _container; + + /// The data template used to create the visual element for this item. public DataTemplate? MauiDataTemplate { get; } + + /// The MAUI context used for platform view creation. public IMauiContext? MauiContext { get; } + + /// The actual data item from the items source. public object? Item { get; } + + /// The parent container (e.g., the ItemsView). Held via a weak reference to avoid leaks. public BindableObject? Container => _container.TryGetTarget(out var c) ? c : null; + + /// The desired item height for uniform sizing strategies. public double ItemHeight { get; } + + /// The desired item width for uniform sizing strategies. public double ItemWidth { get; } + + /// The spacing between items. public Thickness ItemSpacing { get; } + /// Whether this context represents a group header. public bool IsHeader { get; } + + /// Whether this context represents a group footer. public bool IsFooter { get; } public ItemTemplateContext2(DataTemplate mauiDataTemplate, object item, BindableObject container, diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextEnumerable2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextEnumerable2.cs index 7529ef21aecb..60f9ad435580 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextEnumerable2.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextEnumerable2.cs @@ -2,6 +2,11 @@ namespace Microsoft.Maui.Controls.Handlers.Items2 { + /// + /// Wraps a non-list items source, yielding an + /// for each item on enumeration. + /// Used when the source does not implement . + /// internal class ItemTemplateContextEnumerable2 : IEnumerable { readonly IEnumerable _itemsSource; diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextList2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextList2.cs index 5ad6dd7f8fc5..262fda6f2452 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextList2.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextList2.cs @@ -4,6 +4,11 @@ namespace Microsoft.Maui.Controls.Handlers.Items2 { + /// + /// A lazily-populated read-only list that wraps an items source, + /// creating instances on demand and caching them by index. + /// Used for non-observable list sources. + /// internal class ItemTemplateContextList2 : IReadOnlyList { readonly IList _itemsSource; @@ -52,6 +57,7 @@ public ItemTemplateContextList2(IList itemsSource, DataTemplate itemTemplate, Bi if (itemSpacing.HasValue) _itemSpacing = itemSpacing.Value; + // Cap initial dictionary capacity at 64 to avoid over-allocation for small collections _itemTemplateContexts = new(capacity: Math.Min(64, _itemsSource.Count)); } diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs b/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs index b6d668526646..4ed698e3af14 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs @@ -10,6 +10,10 @@ namespace Microsoft.Maui.Controls.Handlers.Items2 { + /// + /// Custom subclass that adds support for + /// empty views, headers, footers, and layout orientation for the CollectionView Handler 2. + /// internal partial class MauiItemsView : UI.Xaml.Controls.ItemsView, IEmptyView { ContentControl? _emptyViewContentControl; @@ -48,6 +52,7 @@ static void EmptyViewVisibilityChanged(DependencyObject d, DependencyPropertyCha } } + /// Gets or sets the visibility of the empty view overlay. public WVisibility EmptyViewVisibility { get @@ -72,6 +77,7 @@ static void HeaderVisibilityChanged(DependencyObject d, DependencyPropertyChange } } + /// Gets or sets the visibility of the header element. public WVisibility HeaderVisibility { get => (WVisibility)GetValue(HeaderVisibilityProperty); @@ -90,12 +96,14 @@ static void FooterVisibilityChanged(DependencyObject d, DependencyPropertyChange } } + /// Gets or sets the visibility of the footer element. public WVisibility FooterVisibility { get => (WVisibility)GetValue(FooterVisibilityProperty); set => SetValue(FooterVisibilityProperty, value); } + /// Sets the empty view content and its MAUI view counterpart. public void SetEmptyView(FrameworkElement emptyView, View mauiEmptyView) { _emptyView = emptyView; @@ -108,6 +116,7 @@ public void SetEmptyView(FrameworkElement emptyView, View mauiEmptyView) } } + /// Sets the header content and its MAUI view counterpart. public void SetHeader(FrameworkElement header, View? mauiHeader) { _header = header; @@ -120,6 +129,7 @@ public void SetHeader(FrameworkElement header, View? mauiHeader) } } + /// Sets the footer content and its MAUI view counterpart. public void SetFooter(FrameworkElement footer, View? mauiFooter) { _footer = footer; @@ -165,6 +175,7 @@ protected override void OnApplyTemplate() ApplyLayoutOrientation(); } + /// Sets the layout orientation and updates the visual tree accordingly. public void SetLayoutOrientation(bool isHorizontal) { _isHorizontalLayout = isHorizontal; @@ -199,14 +210,14 @@ void ApplyLayoutOrientation() { _scrollView.ContentOrientation = UI.Xaml.Controls.ScrollingContentOrientation.Horizontal; } - } + } else { _containerPanel.Orientation = Orientation.Vertical; // For vertical layout, the container panel should stretch horizontally _containerPanel.VerticalAlignment = UI.Xaml.VerticalAlignment.Top; _containerPanel.HorizontalAlignment = UI.Xaml.HorizontalAlignment.Stretch; - + // Items should stretch horizontally (cross-axis) _itemsRepeater.VerticalAlignment = UI.Xaml.VerticalAlignment.Top; _itemsRepeater.HorizontalAlignment = UI.Xaml.HorizontalAlignment.Stretch; @@ -217,7 +228,7 @@ void ApplyLayoutOrientation() _footerContentControl.VerticalContentAlignment = UI.Xaml.VerticalAlignment.Top; _footerContentControl.HorizontalContentAlignment = UI.Xaml.HorizontalAlignment.Stretch; - if(_scrollView is not null) + if (_scrollView is not null) { _scrollView.ContentOrientation = UI.Xaml.Controls.ScrollingContentOrientation.Vertical; } diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ObservableItemTemplateCollection2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ObservableItemTemplateCollection2.cs index 6ae24beffc7d..7abe9122804d 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/ObservableItemTemplateCollection2.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ObservableItemTemplateCollection2.cs @@ -5,6 +5,11 @@ namespace Microsoft.Maui.Controls.Handlers.Items2 { + /// + /// An observable collection of that mirrors an + /// items source, keeping the template collection + /// synchronized with the underlying data and supporting drag/drop reordering. + /// internal class ObservableItemTemplateCollection2 : ObservableCollection { readonly IList _itemsSource; @@ -25,8 +30,6 @@ internal class ObservableItemTemplateCollection2 : ObservableCollection + /// Unsubscribes from collection change events. Must be called when replacing + /// the items source or when the handler disconnects. + /// public void CleanUp() { CollectionChanged -= TemplateCollectionChanged; diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/TemplatedItemSourceFactory2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/TemplatedItemSourceFactory2.cs index 1eabd82c930a..45808ae9e4f7 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/TemplatedItemSourceFactory2.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/TemplatedItemSourceFactory2.cs @@ -3,8 +3,16 @@ namespace Microsoft.Maui.Controls.Handlers.Items2 { + /// + /// Factory that creates the appropriate templated item source collection + /// based on the capabilities of the items source (observable, list, or enumerable). + /// internal static class TemplatedItemSourceFactory2 { + /// + /// Creates a templated item source for the given items source, choosing the best + /// collection type: observable (for INCC sources), indexed list, or enumerable fallback. + /// internal static object Create(IEnumerable itemsSource, DataTemplate itemTemplate, BindableObject container, double? itemHeight = null, double? itemWidth = null, Thickness? itemSpacing = null, IMauiContext? mauiContext = null) { @@ -19,6 +27,10 @@ internal static object Create(IEnumerable itemsSource, DataTemplate itemTemplate return new ItemTemplateContextEnumerable2(itemsSource, itemTemplate, container, itemHeight, itemWidth, itemSpacing, mauiContext); } + /// + /// Creates a grouped templated item source that flattens grouped data into a single + /// list with header and footer contexts for each group. + /// internal static object CreateGrouped(IEnumerable itemsSource, DataTemplate itemTemplate, DataTemplate groupHeaderTemplate, DataTemplate groupFooterTemplate, BindableObject container, IMauiContext? mauiContext = null) { From 14022d7374fa792a469285d698271f305fe9f779 Mon Sep 17 00:00:00 2001 From: devanathan-vaithiyanathan <114395405+devanathan-vaithiyanathan@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:17:05 +0530 Subject: [PATCH 039/109] Update CollectionViewHandler2.Windows.cs --- .../Windows/CollectionViewHandler2.Windows.cs | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs index 63cf3334327b..2c1cc374e578 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs @@ -372,24 +372,7 @@ void UpdatePlatformSelection() var selectedIndex = FindItemIndexInSource(itemList, ItemsView.SelectedItem); if (selectedIndex >= 0) { - PlatformView.DeselectAll(); - } - else - { - var selectedItem = itemList.FirstOrDefault(item => - { - if (item is ItemTemplateContext2 itemPair) - { - return object.Equals(itemPair.Item, ItemsView.SelectedItem); - } - else - { - return object.Equals(item, ItemsView.SelectedItem); - } - }); if (selectedItem is not null) - { - PlatformView.Select(itemList.IndexOf(selectedItem)); - } + PlatformView.Select(selectedIndex); } } From a6e5e726c6b6d04c7248fefa37ffb0459729fb16 Mon Sep 17 00:00:00 2001 From: devanathan-vaithiyanathan <114395405+devanathan-vaithiyanathan@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:46:55 +0530 Subject: [PATCH 040/109] Fix 32543 --- .../Core/Handlers/Items2/Windows/ItemFactory.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs index 9f15c5267198..fd7d95751c16 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs @@ -61,9 +61,21 @@ internal partial class ItemFactory(ItemsView view) : IElementFactory if (_view.Handler?.MauiContext is not null && viewContent is not null) { wrapper = new ElementWrapper(_view.Handler.MauiContext); - wrapper.HorizontalAlignment = HorizontalAlignment.Stretch; + wrapper.HorizontalAlignment = viewContent.HorizontalOptions.Alignment switch + { + LayoutAlignment.Start => HorizontalAlignment.Left, + LayoutAlignment.Center => HorizontalAlignment.Center, + LayoutAlignment.End => HorizontalAlignment.Right, + _ => HorizontalAlignment.Stretch + }; + wrapper.VerticalAlignment = viewContent.VerticalOptions.Alignment switch + { + LayoutAlignment.Start => VerticalAlignment.Top, + LayoutAlignment.Center => VerticalAlignment.Center, + LayoutAlignment.End => VerticalAlignment.Bottom, + _ => VerticalAlignment.Stretch + }; wrapper.HorizontalContentAlignment = HorizontalAlignment.Stretch; - wrapper.VerticalAlignment = VerticalAlignment.Stretch; wrapper.VerticalContentAlignment = VerticalAlignment.Stretch; wrapper.SetContent(viewContent); wrapper.IsHeaderOrFooter = templateContext.IsHeader || templateContext.IsFooter; From f8e67fe74539cbbe844cc6baf79d8ab835841cca Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Thu, 26 Feb 2026 08:00:41 +0530 Subject: [PATCH 041/109] Review code changes --- .../Items/ItemsViewHandler.Windows.cs | 29 - .../Items2/ItemsViewHandler2.Windows.cs | 2328 ++++++++--------- .../Windows/CollectionViewHandler2.Windows.cs | 12 +- .../Windows/GroupedItemTemplateCollection2.cs | 544 ++-- .../Handlers/Items2/Windows/ItemFactory.cs | 320 ++- .../Items2/Windows/ItemTemplateContext2.cs | 2 +- .../Windows/ItemTemplateContextEnumerable2.cs | 67 +- .../Windows/ItemTemplateContextList2.cs | 151 +- .../Handlers/Items2/Windows/MauiItemsView.cs | 369 ++- .../ObservableItemTemplateCollection2.cs | 458 ++-- .../Windows/TemplatedItemSourceFactory2.cs | 54 +- .../CollectionView/ItemsViewStyles.xaml | 388 +-- 12 files changed, 2343 insertions(+), 2379 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.Windows.cs b/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.Windows.cs index cf2514890fa8..cdf7a4132f62 100644 --- a/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.Windows.cs @@ -451,35 +451,6 @@ void ScrollViewChanged(object sender, ScrollViewerViewChangedEventArgs e) HandleScroll(_scrollViewer); } - FrameworkElement RealizeEmptyViewTemplate(object bindingContext, DataTemplate emptyViewTemplate) - { - if (emptyViewTemplate == null) - { - return new TextBlock - { - HorizontalAlignment = Microsoft.UI.Xaml.HorizontalAlignment.Center, - VerticalAlignment = Microsoft.UI.Xaml.VerticalAlignment.Center, - Text = bindingContext.ToString() - }; - } - - var template = emptyViewTemplate.SelectDataTemplate(bindingContext, null); - var view = template.CreateContent() as View; - view.BindingContext = bindingContext; - - return RealizeEmptyView(view); - } - - FrameworkElement RealizeEmptyView(View view) - { - _formsEmptyView = view ?? throw new ArgumentNullException(nameof(view)); - - var handler = view.ToHandler(MauiContext); - var platformView = handler.ContainerView ?? handler.PlatformView; - - return platformView as FrameworkElement; - } - internal void HandleScroll(ScrollViewer scrollViewer) { var itemsViewScrolledEventArgs = new ItemsViewScrolledEventArgs diff --git a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs index 705454537f97..766b99b480b7 100644 --- a/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/ItemsViewHandler2.Windows.cs @@ -20,1526 +20,1524 @@ using WScrollSnapPointsAlignment = Microsoft.UI.Xaml.Controls.Primitives.ScrollSnapPointsAlignment; using WVisibility = Microsoft.UI.Xaml.Visibility; -namespace Microsoft.Maui.Controls.Handlers.Items2 +namespace Microsoft.Maui.Controls.Handlers.Items2; +/// +/// Base handler for ItemsView controls on Windows, providing item source management, +/// layout, scrolling, snap points, empty views, headers, and footers. +/// +public abstract class ItemsViewHandler2 : ViewHandler where TItemsView : ItemsView { /// - /// Base handler for ItemsView controls on Windows, providing item source management, - /// layout, scrolling, snap points, empty views, headers, and footers. + /// Alignment ratio that positions the item at the start (top/left) of the viewport. /// - public abstract class ItemsViewHandler2 : ViewHandler where TItemsView : ItemsView + const double AlignToStart = 0.0; + + /// + /// Alignment ratio that positions the item at the center of the viewport. + /// + const double AlignToCenter = 0.5; + + /// + /// Alignment ratio that positions the item at the end (bottom/right) of the viewport. + /// + const double AlignToEnd = 1.0; + + CollectionViewSource? _collectionViewSource; + IList? _itemsSource; + ItemFactory? _itemFactory; + + FrameworkElement? _emptyView; + View? _mauiEmptyView; + bool _emptyViewDisplayed; + double _previousHorizontalOffset; + double _previousVerticalOffset; + + FrameworkElement? _footer; + View? _mauiFooter; + bool _footerDisplayed; + + FrameworkElement? _header; + View? _mauiHeader; + bool _headerDisplayed; + + ScrollingScrollBarVisibility? _defaultHorizontalScrollVisibility; + ScrollingScrollBarVisibility? _defaultVerticalScrollVisibility; + + int _lastRemainingItemsThresholdIndex = -1; + + WeakNotifyPropertyChangedProxy? _layoutPropertyChangedProxy; + PropertyChangedEventHandler? _layoutPropertyChanged; + bool _isScrollingForItemsUpdate; + int _pendingScrollToIndex = -1; + RoutedEventHandler? _pendingLoadedHandler; + protected TItemsView ItemsView => VirtualView; + protected TItemsView Element => VirtualView; + + protected abstract IItemsLayout Layout { get; } + + bool IsLayoutHorizontal => Layout switch { - /// - /// Alignment ratio that positions the item at the start (top/left) of the viewport. - /// - const double AlignToStart = 0.0; - - /// - /// Alignment ratio that positions the item at the center of the viewport. - /// - const double AlignToCenter = 0.5; - - /// - /// Alignment ratio that positions the item at the end (bottom/right) of the viewport. - /// - const double AlignToEnd = 1.0; - - CollectionViewSource? _collectionViewSource; - IList? _itemsSource; - ItemFactory? _itemFactory; - - FrameworkElement? _emptyView; - View? _mauiEmptyView; - bool _emptyViewDisplayed; - double _previousHorizontalOffset; - double _previousVerticalOffset; - - FrameworkElement? _footer; - View? _mauiFooter; - bool _footerDisplayed; - - FrameworkElement? _header; - View? _mauiHeader; - bool _headerDisplayed; - - ScrollingScrollBarVisibility? _defaultHorizontalScrollVisibility; - ScrollingScrollBarVisibility? _defaultVerticalScrollVisibility; - - int _lastRemainingItemsThresholdIndex = -1; - - WeakNotifyPropertyChangedProxy? _layoutPropertyChangedProxy; - PropertyChangedEventHandler? _layoutPropertyChanged; - bool _isScrollingForItemsUpdate; - int _pendingScrollToIndex = -1; - RoutedEventHandler? _pendingLoadedHandler; - protected TItemsView ItemsView => VirtualView; - protected TItemsView Element => VirtualView; - - protected abstract IItemsLayout Layout { get; } - - bool IsLayoutHorizontal => Layout switch - { - LinearItemsLayout linearLayout => linearLayout.Orientation == ItemsLayoutOrientation.Horizontal, - GridItemsLayout gridLayout => gridLayout.Orientation == ItemsLayoutOrientation.Horizontal, - _ => false - }; + LinearItemsLayout linearLayout => linearLayout.Orientation == ItemsLayoutOrientation.Horizontal, + GridItemsLayout gridLayout => gridLayout.Orientation == ItemsLayoutOrientation.Horizontal, + _ => false + }; - public ItemsViewHandler2() : base(ItemsViewMapper) - { + public ItemsViewHandler2() : base(ItemsViewMapper) + { - } + } - public ItemsViewHandler2(PropertyMapper? mapper = null) : base(mapper ?? ItemsViewMapper) - { + public ItemsViewHandler2(PropertyMapper? mapper = null) : base(mapper ?? ItemsViewMapper) + { - } + } - public static PropertyMapper> ItemsViewMapper = new(ViewMapper) - { - [Controls.ItemsView.ItemsSourceProperty.PropertyName] = MapItemsSource, - [Controls.ItemsView.HorizontalScrollBarVisibilityProperty.PropertyName] = MapHorizontalScrollBarVisibility, - [Controls.ItemsView.VerticalScrollBarVisibilityProperty.PropertyName] = MapVerticalScrollBarVisibility, - [Controls.ItemsView.ItemTemplateProperty.PropertyName] = MapItemTemplate, - [Controls.ItemsView.EmptyViewProperty.PropertyName] = MapEmptyView, - [Controls.ItemsView.EmptyViewTemplateProperty.PropertyName] = MapEmptyViewTemplate, - [Controls.ItemsView.FlowDirectionProperty.PropertyName] = MapFlowDirection, - [Controls.ItemsView.IsVisibleProperty.PropertyName] = MapIsVisible, - [Controls.ItemsView.ItemsUpdatingScrollModeProperty.PropertyName] = MapItemsUpdatingScrollMode, - [Controls.StructuredItemsView.ItemsLayoutProperty.PropertyName] = MapItemsLayout, - [Controls.StructuredItemsView.HeaderProperty.PropertyName] = MapHeader, - [Controls.StructuredItemsView.HeaderTemplateProperty.PropertyName] = MapHeaderTemplate, - [Controls.StructuredItemsView.FooterProperty.PropertyName] = MapFooter, - [Controls.StructuredItemsView.FooterTemplateProperty.PropertyName] = MapFooterTemplate, - }; + public static PropertyMapper> ItemsViewMapper = new(ViewMapper) + { + [Controls.ItemsView.ItemsSourceProperty.PropertyName] = MapItemsSource, + [Controls.ItemsView.HorizontalScrollBarVisibilityProperty.PropertyName] = MapHorizontalScrollBarVisibility, + [Controls.ItemsView.VerticalScrollBarVisibilityProperty.PropertyName] = MapVerticalScrollBarVisibility, + [Controls.ItemsView.ItemTemplateProperty.PropertyName] = MapItemTemplate, + [Controls.ItemsView.EmptyViewProperty.PropertyName] = MapEmptyView, + [Controls.ItemsView.EmptyViewTemplateProperty.PropertyName] = MapEmptyViewTemplate, + [Controls.ItemsView.FlowDirectionProperty.PropertyName] = MapFlowDirection, + [Controls.ItemsView.IsVisibleProperty.PropertyName] = MapIsVisible, + [Controls.ItemsView.ItemsUpdatingScrollModeProperty.PropertyName] = MapItemsUpdatingScrollMode, + [Controls.StructuredItemsView.ItemsLayoutProperty.PropertyName] = MapItemsLayout, + [Controls.StructuredItemsView.HeaderProperty.PropertyName] = MapHeader, + [Controls.StructuredItemsView.HeaderTemplateProperty.PropertyName] = MapHeaderTemplate, + [Controls.StructuredItemsView.FooterProperty.PropertyName] = MapFooter, + [Controls.StructuredItemsView.FooterTemplateProperty.PropertyName] = MapFooterTemplate, + }; + + bool _scrollUpdatePending; + + public static void MapItemsSource(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.UpdateItemsSource(); + } - bool _scrollUpdatePending; + // Intentionally empty: ItemsUpdatingScrollMode is handled during scroll events + // via ApplyItemsUpdatingScrollMode, not as a direct property map. + public static void MapItemsUpdatingScrollMode(ItemsViewHandler2 handler, ItemsView itemsView) + { + } - public static void MapItemsSource(ItemsViewHandler2 handler, ItemsView itemsView) - { - handler.UpdateItemsSource(); - } + public static void MapHorizontalScrollBarVisibility(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.UpdateHorizontalScrollBarVisibility(); + } - // Intentionally empty: ItemsUpdatingScrollMode is handled during scroll events - // via ApplyItemsUpdatingScrollMode, not as a direct property map. - public static void MapItemsUpdatingScrollMode(ItemsViewHandler2 handler, ItemsView itemsView) - { - } + public static void MapVerticalScrollBarVisibility(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.UpdateVerticalScrollBarVisibility(); + } + + public static void MapItemTemplate(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.UpdateItemsSource(); + } + + public static void MapEmptyView(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.UpdateEmptyView(); + } + + public static void MapEmptyViewTemplate(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.UpdateEmptyView(); + } + + public static void MapFlowDirection(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.PlatformView.UpdateFlowDirection(itemsView); + } + + public static void MapIsVisible(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.PlatformView.UpdateVisibility(itemsView); + } + + public static void MapItemsLayout(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.UpdateItemsLayout(); + } + + public static void MapHeader(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.UpdateHeader(); + } + + public static void MapHeaderTemplate(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.UpdateHeader(); + } + + public static void MapFooter(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.UpdateFooter(); + } + + public static void MapFooterTemplate(ItemsViewHandler2 handler, ItemsView itemsView) + { + handler.UpdateFooter(); + } + + protected override WItemsView CreatePlatformView() + { + var itemsView = SelectListViewBase(); + + return itemsView; + } + + protected override void ConnectHandler(WItemsView platformView) + { + base.ConnectHandler(platformView); - public static void MapHorizontalScrollBarVisibility(ItemsViewHandler2 handler, ItemsView itemsView) + if (Layout is not null) { - handler.UpdateHorizontalScrollBarVisibility(); + _layoutPropertyChanged ??= LayoutPropertyChanged; + _layoutPropertyChangedProxy = new WeakNotifyPropertyChangedProxy(Layout, _layoutPropertyChanged); } - - public static void MapVerticalScrollBarVisibility(ItemsViewHandler2 handler, ItemsView itemsView) + else { - handler.UpdateVerticalScrollBarVisibility(); + _layoutPropertyChangedProxy?.Unsubscribe(); + _layoutPropertyChangedProxy = null; } - public static void MapItemTemplate(ItemsViewHandler2 handler, ItemsView itemsView) + VirtualView.ScrollToRequested += ScrollToRequested; + FindScrollViewer(); + } + + + protected override void DisconnectHandler(WItemsView platformView) + { + // Phase 1: Unsubscribe ALL events first to prevent callbacks during cleanup + if (_collectionViewSource?.Source is INotifyCollectionChanged incc) { - handler.UpdateItemsSource(); + incc.CollectionChanged -= ItemsChanged; } - public static void MapEmptyView(ItemsViewHandler2 handler, ItemsView itemsView) + if (platformView.ScrollView is not null) { - handler.UpdateEmptyView(); + platformView.ScrollView.ViewChanged -= ScrollViewChanged; + platformView.ScrollView.PointerWheelChanged -= PointerScrollChanged; + platformView.ScrollView.ExtentChanged -= ScrollViewExtentChanged; } - public static void MapEmptyViewTemplate(ItemsViewHandler2 handler, ItemsView itemsView) + // Unsubscribe pending Loaded handler if ScrollView wasn't found yet + if (_pendingLoadedHandler is not null) { - handler.UpdateEmptyView(); + platformView.Loaded -= _pendingLoadedHandler; + _pendingLoadedHandler = null; } - public static void MapFlowDirection(ItemsViewHandler2 handler, ItemsView itemsView) + if (_scrollUpdatePending) { - handler.PlatformView.UpdateFlowDirection(itemsView); + platformView.LayoutUpdated -= OnLayoutUpdated; + _scrollUpdatePending = false; } - public static void MapIsVisible(ItemsViewHandler2 handler, ItemsView itemsView) + _layoutPropertyChangedProxy?.Unsubscribe(); + _layoutPropertyChangedProxy = null; + + if (VirtualView is not null) { - handler.PlatformView.UpdateVisibility(itemsView); + VirtualView.ScrollToRequested -= ScrollToRequested; } - public static void MapItemsLayout(ItemsViewHandler2 handler, ItemsView itemsView) + // Safe subscription cleanup only — do NOT call CleanUpCollectionViewSource() here. + // + // CleanUpCollectionViewSource() performs two operations that trigger WinUI side effects + // during teardown: + // 1. Sets _collectionViewSource.Source = null → causes WinUI to recycle/release elements + // 2. Calls PlatformView.GetChildren() → triggers element enumeration + // while the ItemsRepeater is tearing down, leading to collection change notifications + // and potential InvalidOperationException. + // + // The CleanUp() methods below are safe because they ONLY unsubscribe event handlers + // (CollectionChanged, group change notifications) without touching WinUI state. + if (_collectionViewSource?.Source is ObservableItemTemplateCollection2 observableCollection) { - handler.UpdateItemsLayout(); + observableCollection.CleanUp(); } - - public static void MapHeader(ItemsViewHandler2 handler, ItemsView itemsView) + else if (_collectionViewSource?.Source is GroupedItemTemplateCollection2 groupedCollection) { - handler.UpdateHeader(); + groupedCollection.CleanUp(); } - public static void MapHeaderTemplate(ItemsViewHandler2 handler, ItemsView itemsView) + _itemFactory?.CleanUp(); + _itemFactory = null; + + // Clean up logical children for empty view, header, and footer to prevent memory leaks + if (_mauiEmptyView is not null && _emptyViewDisplayed) { - handler.UpdateHeader(); + ItemsView?.RemoveLogicalChild(_mauiEmptyView); } + _mauiEmptyView = null; + _emptyView = null; + _emptyViewDisplayed = false; - public static void MapFooter(ItemsViewHandler2 handler, ItemsView itemsView) + if (_mauiHeader is not null && _headerDisplayed) { - handler.UpdateFooter(); + ItemsView?.RemoveLogicalChild(_mauiHeader); } + _mauiHeader = null; + _header = null; + _headerDisplayed = false; - public static void MapFooterTemplate(ItemsViewHandler2 handler, ItemsView itemsView) + if (_mauiFooter is not null && _footerDisplayed) { - handler.UpdateFooter(); + ItemsView?.RemoveLogicalChild(_mauiFooter); } + _mauiFooter = null; + _footer = null; + _footerDisplayed = false; - protected override WItemsView CreatePlatformView() - { - var itemsView = SelectListViewBase(); + _cachedSnapPointOffsets = null; + _lastSnapPointExtent = 0; - return itemsView; - } + base.DisconnectHandler(platformView); + } - protected override void ConnectHandler(WItemsView platformView) - { - base.ConnectHandler(platformView); + CollectionViewSource CreateCollectionViewSource() + { + var itemsSource = Element.ItemsSource; + var itemTemplate = Element.ItemTemplate; - if (Layout is not null) + if (itemTemplate is not null && itemsSource is not null) + { + if (ItemsView is GroupableItemsView groupableItemsView && groupableItemsView.IsGrouped) { - _layoutPropertyChanged ??= LayoutPropertyChanged; - _layoutPropertyChangedProxy = new WeakNotifyPropertyChangedProxy(Layout, _layoutPropertyChanged); + return new CollectionViewSource + { + Source = TemplatedItemSourceFactory2.CreateGrouped(itemsSource, itemTemplate, + groupableItemsView.GroupHeaderTemplate, groupableItemsView.GroupFooterTemplate, + Element, mauiContext: MauiContext) + , + IsSourceGrouped = false + }; } else { - _layoutPropertyChangedProxy?.Unsubscribe(); - _layoutPropertyChangedProxy = null; + var flattenedSource = itemsSource; + if (itemsSource is not null && IsItemsSourceGrouped(itemsSource)) + { + flattenedSource = FlattenGroupedItemsSource(itemsSource); + } + return new CollectionViewSource + { + Source = TemplatedItemSourceFactory2.Create(flattenedSource, itemTemplate, Element, mauiContext: MauiContext), + IsSourceGrouped = false + }; } - - VirtualView.ScrollToRequested += ScrollToRequested; - FindScrollViewer(); } + return new CollectionViewSource + { + Source = itemsSource, + IsSourceGrouped = false + }; + } - protected override void DisconnectHandler(WItemsView platformView) + bool IsItemsSourceGrouped(object itemsSource) + { + if (itemsSource is IEnumerable enumerable) { - // Phase 1: Unsubscribe ALL events first to prevent callbacks during cleanup - if (_collectionViewSource?.Source is INotifyCollectionChanged incc) + foreach (var item in enumerable) { - incc.CollectionChanged -= ItemsChanged; + if (item is IEnumerable && item is not string) + { + return true; + } + break; } + } + return false; + } - if (platformView.ScrollView is not null) - { - platformView.ScrollView.ViewChanged -= ScrollViewChanged; - platformView.ScrollView.PointerWheelChanged -= PointerScrollChanged; - platformView.ScrollView.ExtentChanged -= ScrollViewExtentChanged; - } + IEnumerable FlattenGroupedItemsSource(IEnumerable groupedSource) + { + return groupedSource.Cast(). + Where(group => group is IEnumerable && group is not string). + SelectMany(group => ((IEnumerable)group).Cast()); + } - // Unsubscribe pending Loaded handler if ScrollView wasn't found yet - if (_pendingLoadedHandler is not null) - { - platformView.Loaded -= _pendingLoadedHandler; - _pendingLoadedHandler = null; - } - if (_scrollUpdatePending) - { - platformView.LayoutUpdated -= OnLayoutUpdated; - _scrollUpdatePending = false; - } + protected virtual void UpdateItemsSource() + { + if (PlatformView is null) + { + return; + } - _layoutPropertyChangedProxy?.Unsubscribe(); - _layoutPropertyChangedProxy = null; + CleanUpCollectionViewSource(); - if (VirtualView is not null) - { - VirtualView.ScrollToRequested -= ScrollToRequested; - } + _collectionViewSource = CreateCollectionViewSource(); + _itemsSource = _collectionViewSource?.Source as IList; - // Safe subscription cleanup only — do NOT call CleanUpCollectionViewSource() here. - // - // CleanUpCollectionViewSource() performs two operations that trigger WinUI side effects - // during teardown: - // 1. Sets _collectionViewSource.Source = null → causes WinUI to recycle/release elements - // 2. Calls PlatformView.GetChildren() → triggers element enumeration - // while the ItemsRepeater is tearing down, leading to collection change notifications - // and potential InvalidOperationException. - // - // The CleanUp() methods below are safe because they ONLY unsubscribe event handlers - // (CollectionChanged, group change notifications) without touching WinUI state. - if (_collectionViewSource?.Source is ObservableItemTemplateCollection2 observableCollection) - { - observableCollection.CleanUp(); - } - else if (_collectionViewSource?.Source is GroupedItemTemplateCollection2 groupedCollection) - { - groupedCollection.CleanUp(); - } + if (_collectionViewSource?.Source is INotifyCollectionChanged incc) + { + incc.CollectionChanged += ItemsChanged; + } + + PlatformView.ItemsSource = null; + PlatformView.ItemsSource = _collectionViewSource?.View; + + + if (VirtualView.ItemTemplate is not null) + { + _itemFactory = new ItemFactory(Element); + PlatformView.ItemTemplate = _itemFactory; + } + else if (PlatformView.ItemTemplate is not null) + { + PlatformView.ItemTemplate = null; + } + + UpdateEmptyViewVisibility(); + } - _itemFactory?.CleanUp(); - _itemFactory = null; + void CleanUpCollectionViewSource() + { + // Clean up the recycle pool in the old ItemFactory to release pooled elements + _itemFactory?.CleanUp(); + _itemFactory = null; - // Clean up logical children for empty view, header, and footer to prevent memory leaks - if (_mauiEmptyView is not null && _emptyViewDisplayed) + if (_collectionViewSource is not null) + { + if (_collectionViewSource.Source is ObservableItemTemplateCollection2 observableItemTemplateCollection) { - ItemsView?.RemoveLogicalChild(_mauiEmptyView); + observableItemTemplateCollection.CleanUp(); } - _mauiEmptyView = null; - _emptyView = null; - _emptyViewDisplayed = false; - - if (_mauiHeader is not null && _headerDisplayed) + else if (_collectionViewSource.Source is GroupedItemTemplateCollection2 groupedItemTemplateCollection) { - ItemsView?.RemoveLogicalChild(_mauiHeader); + groupedItemTemplateCollection.CleanUp(); } - _mauiHeader = null; - _header = null; - _headerDisplayed = false; - if (_mauiFooter is not null && _footerDisplayed) + if (_collectionViewSource.Source is INotifyCollectionChanged incc) { - ItemsView?.RemoveLogicalChild(_mauiFooter); + incc.CollectionChanged -= ItemsChanged; } - _mauiFooter = null; - _footer = null; - _footerDisplayed = false; - - _cachedSnapPointOffsets = null; - _lastSnapPointExtent = 0; - base.DisconnectHandler(platformView); + _collectionViewSource.Source = null; + _collectionViewSource = null; } - CollectionViewSource CreateCollectionViewSource() + // Remove all children inside the ItemsSource + if (VirtualView is not null && PlatformView is not null) { - var itemsSource = Element.ItemsSource; - var itemTemplate = Element.ItemTemplate; - - if (itemTemplate is not null && itemsSource is not null) + foreach (var item in PlatformView.GetChildren()) { - if (ItemsView is GroupableItemsView groupableItemsView && groupableItemsView.IsGrouped) - { - return new CollectionViewSource - { - Source = TemplatedItemSourceFactory2.CreateGrouped(itemsSource, itemTemplate, - groupableItemsView.GroupHeaderTemplate, groupableItemsView.GroupFooterTemplate, - Element, mauiContext: MauiContext) - , - IsSourceGrouped = false - }; - } - else + if (item is not null) { - var flattenedSource = itemsSource; - if (itemsSource is not null && IsItemsSourceGrouped(itemsSource)) - { - flattenedSource = FlattenGroupedItemsSource(itemsSource); - } - return new CollectionViewSource - { - Source = TemplatedItemSourceFactory2.Create(flattenedSource, itemTemplate, Element, mauiContext: MauiContext), - IsSourceGrouped = false - }; + var element = item.GetVisualElement(); + VirtualView.RemoveLogicalChild(element); } } - - return new CollectionViewSource - { - Source = itemsSource, - IsSourceGrouped = false - }; } - bool IsItemsSourceGrouped(object itemsSource) + if (VirtualView?.ItemsSource is null && PlatformView is not null) { - if (itemsSource is IEnumerable enumerable) - { - foreach (var item in enumerable) - { - if (item is IEnumerable && item is not string) - { - return true; - } - break; - } - } - return false; + PlatformView.ItemsSource = null; } + } - IEnumerable FlattenGroupedItemsSource(IEnumerable groupedSource) + void ItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + // Skip if handler is disconnected + if (PlatformView is null || VirtualView is null) { - return groupedSource.Cast(). - Where(group => group is IEnumerable && group is not string). - SelectMany(group => ((IEnumerable)group).Cast()); + return; } + UpdateEmptyViewVisibility(); - protected virtual void UpdateItemsSource() + if (e.Action == NotifyCollectionChangedAction.Add || e.Action == NotifyCollectionChangedAction.Reset) { - if (PlatformView is null) - { - return; - } + _lastRemainingItemsThresholdIndex = -1; + } - CleanUpCollectionViewSource(); + if (!_scrollUpdatePending && PlatformView is not null) + { + _scrollUpdatePending = true; - _collectionViewSource = CreateCollectionViewSource(); - _itemsSource = _collectionViewSource?.Source as IList; + PlatformView.LayoutUpdated += OnLayoutUpdated; + } + } + void OnLayoutUpdated(object? s, object args) + { + if (PlatformView is null) + { + _scrollUpdatePending = false; + return; + } - if (_collectionViewSource?.Source is INotifyCollectionChanged incc) - { - incc.CollectionChanged += ItemsChanged; - } + PlatformView.LayoutUpdated -= OnLayoutUpdated; + _scrollUpdatePending = false; + ApplyItemsUpdatingScrollMode(); + } - PlatformView.ItemsSource = null; - PlatformView.ItemsSource = _collectionViewSource?.View; + void ApplyItemsUpdatingScrollMode() + { + if (PlatformView is null || VirtualView is null) + { + return; + } + if (VirtualView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepScrollOffset) + { + return; + } - if (VirtualView.ItemTemplate is not null) - { - _itemFactory = new ItemFactory(Element); - PlatformView.ItemTemplate = _itemFactory; - } - else if (PlatformView.ItemTemplate is not null) - { - PlatformView.ItemTemplate = null; - } + if (_itemsSource is null || _itemsSource.Count == 0) + { + return; + } - UpdateEmptyViewVisibility(); + // Get the actual count from the CollectionView's view + var viewCount = _collectionViewSource?.View?.Count ?? 0; + if (viewCount == 0) + { + return; } - void CleanUpCollectionViewSource() + // Force layout update to ensure items are properly positioned + PlatformView.UpdateLayout(); + + if (VirtualView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepItemsInView) { - // Clean up the recycle pool in the old ItemFactory to release pooled elements - _itemFactory?.CleanUp(); - _itemFactory = null; + _isScrollingForItemsUpdate = true; - if (_collectionViewSource is not null) + // Use dispatcher to ensure the scroll happens after layout is fully complete + VirtualView.Dispatcher.Dispatch(() => { - if (_collectionViewSource.Source is ObservableItemTemplateCollection2 observableItemTemplateCollection) - { - observableItemTemplateCollection.CleanUp(); - } - else if (_collectionViewSource.Source is GroupedItemTemplateCollection2 groupedItemTemplateCollection) + if (PlatformView is null) { - groupedItemTemplateCollection.CleanUp(); + return; } - if (_collectionViewSource.Source is INotifyCollectionChanged incc) + // Keeps the first item in the list displayed when new items are added. + PlatformView.StartBringItemIntoView(0, new BringIntoViewOptions() { - incc.CollectionChanged -= ItemsChanged; - } - - _collectionViewSource.Source = null; - _collectionViewSource = null; - } + AnimationDesired = false, + VerticalAlignmentRatio = AlignToStart, + HorizontalAlignmentRatio = AlignToStart + }); + }); + } + else if (VirtualView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepLastItemInView) + { + _isScrollingForItemsUpdate = true; - // Remove all children inside the ItemsSource - if (VirtualView is not null && PlatformView is not null) + // Use the view count to ensure we're scrolling to the correct last item + var lastIndex = viewCount - 1; + if (lastIndex >= 0) { - foreach (var item in PlatformView.GetChildren()) + _pendingScrollToIndex = lastIndex; + + // Use dispatcher to ensure the scroll happens after layout is fully complete + VirtualView.Dispatcher.Dispatch(() => { - if (item is not null) + if (PlatformView is null || _pendingScrollToIndex < 0) + { + _pendingScrollToIndex = -1; + return; + } + + var currentViewCount = _collectionViewSource?.View?.Count ?? 0; + var scrollIndex = Math.Min(_pendingScrollToIndex, currentViewCount - 1); + + if (scrollIndex >= 0) { - var element = item.GetVisualElement(); - VirtualView.RemoveLogicalChild(element); + PlatformView.StartBringItemIntoView(scrollIndex, new BringIntoViewOptions() + { + AnimationDesired = false, + VerticalAlignmentRatio = AlignToEnd, + HorizontalAlignmentRatio = AlignToEnd + }); } - } - } - if (VirtualView?.ItemsSource is null && PlatformView is not null) - { - PlatformView.ItemsSource = null; + _pendingScrollToIndex = -1; + }); } } + } - void ItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) + MauiItemsView SelectListViewBase() + { + var itemsView = new MauiItemsView() { - // Skip if handler is disconnected - if (PlatformView is null || VirtualView is null) - { - return; - } - - UpdateEmptyViewVisibility(); + Layout = CreateItemsLayout() + }; - if (e.Action == NotifyCollectionChangedAction.Add || e.Action == NotifyCollectionChangedAction.Reset) - { - _lastRemainingItemsThresholdIndex = -1; - } + if (IsLayoutHorizontal) + { + ScrollViewer.SetHorizontalScrollMode(itemsView, UI.Xaml.Controls.ScrollMode.Enabled); + ScrollViewer.SetHorizontalScrollBarVisibility(itemsView, WASDKScrollBarVisibility.Visible); - if (!_scrollUpdatePending && PlatformView is not null) - { - _scrollUpdatePending = true; + ScrollViewer.SetVerticalScrollMode(itemsView, UI.Xaml.Controls.ScrollMode.Disabled); + ScrollViewer.SetVerticalScrollBarVisibility(itemsView, WASDKScrollBarVisibility.Disabled); - PlatformView.LayoutUpdated += OnLayoutUpdated; - } } - void OnLayoutUpdated(object? s, object args) + else { - if (PlatformView is null) - { - _scrollUpdatePending = false; - return; - } + ScrollViewer.SetHorizontalScrollMode(itemsView, UI.Xaml.Controls.ScrollMode.Disabled); + ScrollViewer.SetHorizontalScrollBarVisibility(itemsView, WASDKScrollBarVisibility.Disabled); - PlatformView.LayoutUpdated -= OnLayoutUpdated; - _scrollUpdatePending = false; - ApplyItemsUpdatingScrollMode(); + ScrollViewer.SetVerticalScrollMode(itemsView, UI.Xaml.Controls.ScrollMode.Enabled); + ScrollViewer.SetVerticalScrollBarVisibility(itemsView, WASDKScrollBarVisibility.Visible); } - void ApplyItemsUpdatingScrollMode() + itemsView.SetLayoutOrientation(IsLayoutHorizontal); + return itemsView; + } + + protected void UpdateItemsLayout() + { + FindScrollViewer(); + + _defaultHorizontalScrollVisibility = null; + _defaultVerticalScrollVisibility = null; + + // Unsubscribe from the old layout's property changes and subscribe to the new layout + _layoutPropertyChangedProxy?.Unsubscribe(); + _layoutPropertyChangedProxy = null; + + if (Layout is not null) { - if (PlatformView is null || VirtualView is null) - { - return; - } + _layoutPropertyChanged ??= LayoutPropertyChanged; + _layoutPropertyChangedProxy = new WeakNotifyPropertyChangedProxy(Layout, _layoutPropertyChanged); + } - if (VirtualView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepScrollOffset) - { - return; - } + PlatformView.Layout = CreateItemsLayout(); - if (_itemsSource is null || _itemsSource.Count == 0) + // Update header/footer orientation + if (PlatformView is MauiItemsView mauiItemsView) + { + if (IsLayoutHorizontal) { - return; + ScrollViewer.SetHorizontalScrollMode(mauiItemsView, UI.Xaml.Controls.ScrollMode.Enabled); + ScrollViewer.SetVerticalScrollMode(mauiItemsView, UI.Xaml.Controls.ScrollMode.Disabled); } - - // Get the actual count from the CollectionView's view - var viewCount = _collectionViewSource?.View?.Count ?? 0; - if (viewCount == 0) + else { - return; + ScrollViewer.SetHorizontalScrollMode(mauiItemsView, UI.Xaml.Controls.ScrollMode.Disabled); + ScrollViewer.SetVerticalScrollMode(mauiItemsView, UI.Xaml.Controls.ScrollMode.Enabled); } - // Force layout update to ensure items are properly positioned - PlatformView.UpdateLayout(); + mauiItemsView.SetLayoutOrientation(IsLayoutHorizontal); + } - if (VirtualView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepItemsInView) - { - _isScrollingForItemsUpdate = true; + UpdateVerticalScrollBarVisibility(); + UpdateHorizontalScrollBarVisibility(); + UpdateEmptyView(); + } - // Use dispatcher to ensure the scroll happens after layout is fully complete - VirtualView.Dispatcher.Dispatch(() => - { - if (PlatformView is null) - { - return; - } - - // Keeps the first item in the list displayed when new items are added. - PlatformView.StartBringItemIntoView(0, new BringIntoViewOptions() - { - AnimationDesired = false, - VerticalAlignmentRatio = AlignToStart, - HorizontalAlignmentRatio = AlignToStart - }); - }); - } - else if (VirtualView.ItemsUpdatingScrollMode == ItemsUpdatingScrollMode.KeepLastItemInView) - { - _isScrollingForItemsUpdate = true; - - // Use the view count to ensure we're scrolling to the correct last item - var lastIndex = viewCount - 1; - if (lastIndex >= 0) - { - _pendingScrollToIndex = lastIndex; + UI.Xaml.Controls.Layout CreateItemsLayout() + { + switch (Layout) + { + case GridItemsLayout gridItemsLayout: + return CreateGridView(gridItemsLayout); + case LinearItemsLayout listItemsLayout: + return CreateStackLayout(listItemsLayout); + default: + break; + } - // Use dispatcher to ensure the scroll happens after layout is fully complete - VirtualView.Dispatcher.Dispatch(() => - { - if (PlatformView is null || _pendingScrollToIndex < 0) - { - _pendingScrollToIndex = -1; - return; - } + throw new NotImplementedException("The layout is not implemented"); + } - var currentViewCount = _collectionViewSource?.View?.Count ?? 0; - var scrollIndex = Math.Min(_pendingScrollToIndex, currentViewCount - 1); + static UI.Xaml.Controls.StackLayout CreateStackLayout(LinearItemsLayout listItemsLayout) + { + return new UI.Xaml.Controls.StackLayout() + { + Orientation = listItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal + ? Orientation.Horizontal : Orientation.Vertical, + Spacing = listItemsLayout.ItemSpacing + }; + } - if (scrollIndex >= 0) - { - PlatformView.StartBringItemIntoView(scrollIndex, new BringIntoViewOptions() - { - AnimationDesired = false, - VerticalAlignmentRatio = AlignToEnd, - HorizontalAlignmentRatio = AlignToEnd - }); - } + static UniformGridLayout CreateGridView(GridItemsLayout gridItemsLayout) + { + return new UniformGridLayout() + { + Orientation = gridItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal + ? Orientation.Vertical : Orientation.Horizontal, + MaximumRowsOrColumns = gridItemsLayout.Span, + MinColumnSpacing = gridItemsLayout.HorizontalItemSpacing, + MinRowSpacing = gridItemsLayout.VerticalItemSpacing, + ItemsStretch = UniformGridLayoutItemsStretch.Fill, + ItemsJustification = UniformGridLayoutItemsJustification.Start, + }; + } - _pendingScrollToIndex = -1; - }); - } - } + void FindScrollViewer() + { + if (PlatformView is null) + { + return; } - MauiItemsView SelectListViewBase() + if (PlatformView.ScrollView is not null) { - var itemsView = new MauiItemsView() - { - Layout = CreateItemsLayout() - }; - - if (IsLayoutHorizontal) - { - ScrollViewer.SetHorizontalScrollMode(itemsView, UI.Xaml.Controls.ScrollMode.Enabled); - ScrollViewer.SetHorizontalScrollBarVisibility(itemsView, WASDKScrollBarVisibility.Visible); - - ScrollViewer.SetVerticalScrollMode(itemsView, UI.Xaml.Controls.ScrollMode.Disabled); - ScrollViewer.SetVerticalScrollBarVisibility(itemsView, WASDKScrollBarVisibility.Disabled); - - } - else - { - ScrollViewer.SetHorizontalScrollMode(itemsView, UI.Xaml.Controls.ScrollMode.Disabled); - ScrollViewer.SetHorizontalScrollBarVisibility(itemsView, WASDKScrollBarVisibility.Disabled); - - ScrollViewer.SetVerticalScrollMode(itemsView, UI.Xaml.Controls.ScrollMode.Enabled); - ScrollViewer.SetVerticalScrollBarVisibility(itemsView, WASDKScrollBarVisibility.Visible); - } + OnScrollViewerFound(); + return; + } - itemsView.SetLayoutOrientation(IsLayoutHorizontal); - return itemsView; + // Unsubscribe any previous pending Loaded handler + if (_pendingLoadedHandler is not null) + { + PlatformView.Loaded -= _pendingLoadedHandler; } - protected void UpdateItemsLayout() + _pendingLoadedHandler = (sender, e) => { + var lv = (WItemsView)sender; + lv.Loaded -= _pendingLoadedHandler; + _pendingLoadedHandler = null; FindScrollViewer(); + }; - _defaultHorizontalScrollVisibility = null; - _defaultVerticalScrollVisibility = null; - - // Unsubscribe from the old layout's property changes and subscribe to the new layout - _layoutPropertyChangedProxy?.Unsubscribe(); - _layoutPropertyChangedProxy = null; + PlatformView.Loaded += _pendingLoadedHandler; + } - if (Layout is not null) - { - _layoutPropertyChanged ??= LayoutPropertyChanged; - _layoutPropertyChangedProxy = new WeakNotifyPropertyChangedProxy(Layout, _layoutPropertyChanged); - } + void OnScrollViewerFound() + { + if (PlatformView is null || PlatformView.ScrollView is null) + { + return; + } - PlatformView.Layout = CreateItemsLayout(); + PlatformView.ScrollView.ViewChanged -= ScrollViewChanged; + PlatformView.ScrollView.PointerWheelChanged -= PointerScrollChanged; + PlatformView.ScrollView.ExtentChanged -= ScrollViewExtentChanged; - // Update header/footer orientation - if (PlatformView is MauiItemsView mauiItemsView) - { - if (IsLayoutHorizontal) - { - ScrollViewer.SetHorizontalScrollMode(mauiItemsView, UI.Xaml.Controls.ScrollMode.Enabled); - ScrollViewer.SetVerticalScrollMode(mauiItemsView, UI.Xaml.Controls.ScrollMode.Disabled); - } - else - { - ScrollViewer.SetHorizontalScrollMode(mauiItemsView, UI.Xaml.Controls.ScrollMode.Disabled); - ScrollViewer.SetVerticalScrollMode(mauiItemsView, UI.Xaml.Controls.ScrollMode.Enabled); - } + PlatformView.ScrollView.ViewChanged += ScrollViewChanged; + PlatformView.ScrollView.PointerWheelChanged += PointerScrollChanged; + PlatformView.ScrollView.ExtentChanged += ScrollViewExtentChanged; - mauiItemsView.SetLayoutOrientation(IsLayoutHorizontal); - } + UpdateVerticalScrollBarVisibility(); + UpdateHorizontalScrollBarVisibility(); + UpdateSnapPoints(); + } - UpdateVerticalScrollBarVisibility(); - UpdateHorizontalScrollBarVisibility(); - UpdateEmptyView(); + void LayoutPropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == GridItemsLayout.SpanProperty.PropertyName) + { + UpdateItemsLayoutSpan(); } - - UI.Xaml.Controls.Layout CreateItemsLayout() + else if (e.PropertyName == GridItemsLayout.HorizontalItemSpacingProperty.PropertyName || e.PropertyName == GridItemsLayout.VerticalItemSpacingProperty.PropertyName) { - switch (Layout) - { - case GridItemsLayout gridItemsLayout: - return CreateGridView(gridItemsLayout); - case LinearItemsLayout listItemsLayout: - return CreateStackLayout(listItemsLayout); - default: - break; - } - - throw new NotImplementedException("The layout is not implemented"); + UpdateItemsLayoutItemSpacing(); } - - static UI.Xaml.Controls.StackLayout CreateStackLayout(LinearItemsLayout listItemsLayout) + else if (e.PropertyName == LinearItemsLayout.ItemSpacingProperty.PropertyName) { - return new UI.Xaml.Controls.StackLayout() - { - Orientation = listItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal - ? Orientation.Horizontal : Orientation.Vertical, - Spacing = listItemsLayout.ItemSpacing - }; + UpdateItemsLayoutItemSpacing(); } - - static UniformGridLayout CreateGridView(GridItemsLayout gridItemsLayout) + else if (e.PropertyName == nameof(ItemsLayout.SnapPointsType) || + e.PropertyName == nameof(ItemsLayout.SnapPointsAlignment)) { - return new UniformGridLayout() - { - Orientation = gridItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal - ? Orientation.Vertical : Orientation.Horizontal, - MaximumRowsOrColumns = gridItemsLayout.Span, - MinColumnSpacing = gridItemsLayout.HorizontalItemSpacing, - MinRowSpacing = gridItemsLayout.VerticalItemSpacing, - ItemsStretch = UniformGridLayoutItemsStretch.Fill, - ItemsJustification = UniformGridLayoutItemsJustification.Start, - }; + UpdateSnapPoints(); } + } - void FindScrollViewer() + void UpdateItemsLayoutSpan() + { + if (PlatformView.Layout is UniformGridLayout listViewLayout && + Layout is GridItemsLayout gridItemsLayout) { - if (PlatformView is null) - { - return; - } - - if (PlatformView.ScrollView is not null) - { - OnScrollViewerFound(); - return; - } - - // Unsubscribe any previous pending Loaded handler - if (_pendingLoadedHandler is not null) - { - PlatformView.Loaded -= _pendingLoadedHandler; - } - - _pendingLoadedHandler = (sender, e) => - { - var lv = (WItemsView)sender; - lv.Loaded -= _pendingLoadedHandler; - _pendingLoadedHandler = null; - FindScrollViewer(); - }; - - PlatformView.Loaded += _pendingLoadedHandler; + listViewLayout.MaximumRowsOrColumns = gridItemsLayout.Span; } + } - void OnScrollViewerFound() + void UpdateItemsLayoutItemSpacing() + { + if (PlatformView.Layout is UniformGridLayout listViewLayout && + Layout is GridItemsLayout gridItemsLayout) { - if (PlatformView is null || PlatformView.ScrollView is null) - { - return; - } - - PlatformView.ScrollView.ViewChanged -= ScrollViewChanged; - PlatformView.ScrollView.PointerWheelChanged -= PointerScrollChanged; - PlatformView.ScrollView.ExtentChanged -= ScrollViewExtentChanged; + listViewLayout.MinColumnSpacing = gridItemsLayout.HorizontalItemSpacing; + listViewLayout.MinRowSpacing = gridItemsLayout.VerticalItemSpacing; + } + else if (PlatformView.Layout is UI.Xaml.Controls.StackLayout stackLayout && + Layout is LinearItemsLayout linearItemsLayout) + { + stackLayout.Spacing = linearItemsLayout.ItemSpacing; + } + } - PlatformView.ScrollView.ViewChanged += ScrollViewChanged; - PlatformView.ScrollView.PointerWheelChanged += PointerScrollChanged; - PlatformView.ScrollView.ExtentChanged += ScrollViewExtentChanged; + double _lastSnapPointExtent; + List? _cachedSnapPointOffsets; + SnapPointsType _cachedSnapPointsType; + SnapPointsAlignment _cachedSnapPointsAlignment; - UpdateVerticalScrollBarVisibility(); - UpdateHorizontalScrollBarVisibility(); + void ScrollViewExtentChanged(Microsoft.UI.Xaml.Controls.ScrollView sender, object args) + { + // Only recalculate snap points when the extent actually changes + // to avoid redundant work during intermediate layout passes. + var currentExtent = IsLayoutHorizontal ? sender.ExtentWidth : sender.ExtentHeight; + if (currentExtent != _lastSnapPointExtent) + { + _lastSnapPointExtent = currentExtent; UpdateSnapPoints(); } + } - void LayoutPropertyChanged(object? sender, PropertyChangedEventArgs e) + void UpdateSnapPoints() + { + if (PlatformView?.ScrollView?.ScrollPresenter is not WScrollPresenter scrollPresenter) { - if (e.PropertyName == GridItemsLayout.SpanProperty.PropertyName) - { - UpdateItemsLayoutSpan(); - } - else if (e.PropertyName == GridItemsLayout.HorizontalItemSpacingProperty.PropertyName || e.PropertyName == GridItemsLayout.VerticalItemSpacingProperty.PropertyName) - { - UpdateItemsLayoutItemSpacing(); - } - else if (e.PropertyName == LinearItemsLayout.ItemSpacingProperty.PropertyName) - { - UpdateItemsLayoutItemSpacing(); - } - else if (e.PropertyName == nameof(ItemsLayout.SnapPointsType) || - e.PropertyName == nameof(ItemsLayout.SnapPointsAlignment)) - { - UpdateSnapPoints(); - } + return; } - void UpdateItemsLayoutSpan() + if (Layout is not ItemsLayout itemsLayout) { - if (PlatformView.Layout is UniformGridLayout listViewLayout && - Layout is GridItemsLayout gridItemsLayout) - { - listViewLayout.MaximumRowsOrColumns = gridItemsLayout.Span; - } + return; } - void UpdateItemsLayoutItemSpacing() + var snapPointsType = itemsLayout.SnapPointsType; + + if (snapPointsType == SnapPointsType.None) { - if (PlatformView.Layout is UniformGridLayout listViewLayout && - Layout is GridItemsLayout gridItemsLayout) - { - listViewLayout.MinColumnSpacing = gridItemsLayout.HorizontalItemSpacing; - listViewLayout.MinRowSpacing = gridItemsLayout.VerticalItemSpacing; - } - else if (PlatformView.Layout is UI.Xaml.Controls.StackLayout stackLayout && - Layout is LinearItemsLayout linearItemsLayout) - { - stackLayout.Spacing = linearItemsLayout.ItemSpacing; - } + scrollPresenter.HorizontalSnapPoints.Clear(); + scrollPresenter.VerticalSnapPoints.Clear(); + _cachedSnapPointOffsets = null; + _cachedSnapPointsType = SnapPointsType.None; + return; } - double _lastSnapPointExtent; - List? _cachedSnapPointOffsets; - SnapPointsType _cachedSnapPointsType; - SnapPointsAlignment _cachedSnapPointsAlignment; + // NOTE: MandatorySingle is treated identically to Mandatory. + // WinUI's ScrollPresenter snap point object model does not natively support + // "single item per gesture" limiting. Both Mandatory and MandatorySingle + // will snap to the nearest snap point, but a fling gesture can skip past + // multiple items. A true MandatorySingle implementation would require + // intercepting scroll inertia to limit travel distance, which is not + // supported by the current ScrollPresenter API. - void ScrollViewExtentChanged(Microsoft.UI.Xaml.Controls.ScrollView sender, object args) + var itemCount = _collectionViewSource?.View?.Count ?? 0; + if (itemCount == 0) { - // Only recalculate snap points when the extent actually changes - // to avoid redundant work during intermediate layout passes. - var currentExtent = IsLayoutHorizontal ? sender.ExtentWidth : sender.ExtentHeight; - if (currentExtent != _lastSnapPointExtent) - { - _lastSnapPointExtent = currentExtent; - UpdateSnapPoints(); - } + scrollPresenter.HorizontalSnapPoints.Clear(); + scrollPresenter.VerticalSnapPoints.Clear(); + _cachedSnapPointOffsets = null; + return; } - void UpdateSnapPoints() - { - if (PlatformView?.ScrollView?.ScrollPresenter is not WScrollPresenter scrollPresenter) - { - return; - } + var snapAlignment = itemsLayout.SnapPointsAlignment; - if (Layout is not ItemsLayout itemsLayout) - { - return; - } + // Compute offsets from realized item containers. + // Uses actual measured sizes to handle variable-size items, + // complex templates, and async image loading. + double runningOffset = 0; + double spacing = 0; + var newOffsets = new List(); - var snapPointsType = itemsLayout.SnapPointsType; + if (Layout is LinearItemsLayout linearLayout) + { + spacing = linearLayout.ItemSpacing; + } + else if (Layout is GridItemsLayout gridLayout) + { + spacing = IsLayoutHorizontal ? gridLayout.HorizontalItemSpacing : gridLayout.VerticalItemSpacing; + } - if (snapPointsType == SnapPointsType.None) + foreach (var container in PlatformView.GetChildren()) + { + if (container is null) { - scrollPresenter.HorizontalSnapPoints.Clear(); - scrollPresenter.VerticalSnapPoints.Clear(); - _cachedSnapPointOffsets = null; - _cachedSnapPointsType = SnapPointsType.None; - return; + continue; } - // NOTE: MandatorySingle is treated identically to Mandatory. - // WinUI's ScrollPresenter snap point object model does not natively support - // "single item per gesture" limiting. Both Mandatory and MandatorySingle - // will snap to the nearest snap point, but a fling gesture can skip past - // multiple items. A true MandatorySingle implementation would require - // intercepting scroll inertia to limit travel distance, which is not - // supported by the current ScrollPresenter API. - - var itemCount = _collectionViewSource?.View?.Count ?? 0; - if (itemCount == 0) + double itemExtent = IsLayoutHorizontal ? container.ActualWidth : container.ActualHeight; + if (itemExtent <= 0) { - scrollPresenter.HorizontalSnapPoints.Clear(); - scrollPresenter.VerticalSnapPoints.Clear(); - _cachedSnapPointOffsets = null; - return; + continue; } - var snapAlignment = itemsLayout.SnapPointsAlignment; + newOffsets.Add(runningOffset); + runningOffset += itemExtent + spacing; + } - // Compute offsets from realized item containers. - // Uses actual measured sizes to handle variable-size items, - // complex templates, and async image loading. - double runningOffset = 0; - double spacing = 0; - var newOffsets = new List(); + if (newOffsets.Count == 0) + { + // Items not yet realized; snap points will be applied + // when ExtentChanged fires after items are laid out. + return; + } - if (Layout is LinearItemsLayout linearLayout) - { - spacing = linearLayout.ItemSpacing; - } - else if (Layout is GridItemsLayout gridLayout) - { - spacing = IsLayoutHorizontal ? gridLayout.HorizontalItemSpacing : gridLayout.VerticalItemSpacing; - } + // Skip rebuild if snap points haven't changed — avoids redundant + // WinUI collection clears and allocations on every ExtentChanged. + if (_cachedSnapPointOffsets is not null && + _cachedSnapPointsType == snapPointsType && + _cachedSnapPointsAlignment == snapAlignment && + _cachedSnapPointOffsets.Count == newOffsets.Count && + _cachedSnapPointOffsets.SequenceEqual(newOffsets)) + { + return; + } - foreach (var container in PlatformView.GetChildren()) - { - if (container is null) - { - continue; - } + // Rebuild snap points — clear both axes to handle orientation changes + var alignment = GetScrollSnapPointsAlignment(snapAlignment); - double itemExtent = IsLayoutHorizontal ? container.ActualWidth : container.ActualHeight; - if (itemExtent <= 0) - { - continue; - } + scrollPresenter.HorizontalSnapPoints.Clear(); + scrollPresenter.VerticalSnapPoints.Clear(); - newOffsets.Add(runningOffset); - runningOffset += itemExtent + spacing; - } + var snapPoints = IsLayoutHorizontal + ? scrollPresenter.HorizontalSnapPoints + : scrollPresenter.VerticalSnapPoints; - if (newOffsets.Count == 0) - { - // Items not yet realized; snap points will be applied - // when ExtentChanged fires after items are laid out. - return; - } + foreach (var offset in newOffsets) + { + snapPoints.Add(new Microsoft.UI.Xaml.Controls.Primitives.ScrollSnapPoint( + offset, alignment)); + } - // Skip rebuild if snap points haven't changed — avoids redundant - // WinUI collection clears and allocations on every ExtentChanged. - if (_cachedSnapPointOffsets is not null && - _cachedSnapPointsType == snapPointsType && - _cachedSnapPointsAlignment == snapAlignment && - _cachedSnapPointOffsets.Count == newOffsets.Count && - _cachedSnapPointOffsets.SequenceEqual(newOffsets)) - { - return; - } + _cachedSnapPointOffsets = newOffsets; + _cachedSnapPointsType = snapPointsType; + _cachedSnapPointsAlignment = snapAlignment; + } - // Rebuild snap points — clear both axes to handle orientation changes - var alignment = GetScrollSnapPointsAlignment(snapAlignment); + static WScrollSnapPointsAlignment GetScrollSnapPointsAlignment(SnapPointsAlignment alignment) + { + return alignment switch + { + SnapPointsAlignment.Start => WScrollSnapPointsAlignment.Near, + SnapPointsAlignment.Center => WScrollSnapPointsAlignment.Center, + SnapPointsAlignment.End => WScrollSnapPointsAlignment.Far, + _ => WScrollSnapPointsAlignment.Near, + }; + } - scrollPresenter.HorizontalSnapPoints.Clear(); - scrollPresenter.VerticalSnapPoints.Clear(); + void UpdateEmptyView() + { + if (Element is null || PlatformView is null) + { + return; + } - var snapPoints = IsLayoutHorizontal - ? scrollPresenter.HorizontalSnapPoints - : scrollPresenter.VerticalSnapPoints; + var emptyView = Element.EmptyView; + var emptyViewTemplate = Element.EmptyViewTemplate; - foreach (var offset in newOffsets) + // Clear empty view + if (emptyView is null && emptyViewTemplate is null) + { + if (_emptyViewDisplayed) { - snapPoints.Add(new Microsoft.UI.Xaml.Controls.Primitives.ScrollSnapPoint( - offset, alignment)); + if (_emptyView != null && PlatformView is IEmptyView ev) + ev.EmptyViewVisibility = WVisibility.Collapsed; + + if (_mauiEmptyView != null) + ItemsView.RemoveLogicalChild(_mauiEmptyView); + + _emptyViewDisplayed = false; } - _cachedSnapPointOffsets = newOffsets; - _cachedSnapPointsType = snapPointsType; - _cachedSnapPointsAlignment = snapAlignment; + _emptyView = null; + _mauiEmptyView = null; + (PlatformView as IEmptyView)?.SetEmptyView(null, null); + return; } - static WScrollSnapPointsAlignment GetScrollSnapPointsAlignment(SnapPointsAlignment alignment) + if (emptyViewTemplate is DataTemplateSelector && emptyView is null) { - return alignment switch + _emptyView = null; + _mauiEmptyView = null; + (PlatformView as IEmptyView)?.SetEmptyView(null, null); + return; + } + + // Resolve empty view + var oldMauiEmptyView = _mauiEmptyView; + _emptyView = emptyViewTemplate != null + ? ItemsViewExtensions.RealizeEmptyViewTemplate(emptyView, emptyViewTemplate, MauiContext!, ref _mauiEmptyView) + : emptyView switch { - SnapPointsAlignment.Start => WScrollSnapPointsAlignment.Near, - SnapPointsAlignment.Center => WScrollSnapPointsAlignment.Center, - SnapPointsAlignment.End => WScrollSnapPointsAlignment.Far, - _ => WScrollSnapPointsAlignment.Near, + string text => new TextBlock + { + HorizontalAlignment = Microsoft.UI.Xaml.HorizontalAlignment.Center, + VerticalAlignment = Microsoft.UI.Xaml.VerticalAlignment.Center, + Text = text + }, + View view => ItemsViewExtensions.RealizeEmptyView(view, MauiContext!, ref _mauiEmptyView), + _ => ItemsViewExtensions.RealizeEmptyViewTemplate(emptyView, null, MauiContext!, ref _mauiEmptyView) }; + + // Remove old logical child before adding the new one to prevent leak + if (oldMauiEmptyView is not null && _emptyViewDisplayed) + { + ItemsView.RemoveLogicalChild(oldMauiEmptyView); + _emptyViewDisplayed = false; } - void UpdateEmptyView() + (PlatformView as IEmptyView)?.SetEmptyView(_emptyView, _mauiEmptyView); + UpdateEmptyViewVisibility(); + } + + void UpdateEmptyViewVisibility() + { + if (PlatformView is null || VirtualView is null) { - if (Element is null || PlatformView is null) - { - return; - } + return; + } - var emptyView = Element.EmptyView; - var emptyViewTemplate = Element.EmptyViewTemplate; + // Check both CollectionViewSource.View and the underlying _itemsSource + // After a Reset action, CollectionViewSource.View.Count might not be immediately updated + bool isEmpty = (_collectionViewSource?.View?.Count ?? 0) == 0 && (_itemsSource?.Count ?? 0) == 0; - // Clear empty view - if (emptyView is null && emptyViewTemplate is null) + if (isEmpty) + { + if (_mauiEmptyView is not null && !_emptyViewDisplayed) { - if (_emptyViewDisplayed) + if (ItemsView.EmptyViewTemplate is null) { - if (_emptyView != null && PlatformView is IEmptyView ev) - ev.EmptyViewVisibility = WVisibility.Collapsed; - - if (_mauiEmptyView != null) - ItemsView.RemoveLogicalChild(_mauiEmptyView); - - _emptyViewDisplayed = false; + ItemsView.AddLogicalChild(_mauiEmptyView); } - - _emptyView = null; - _mauiEmptyView = null; - (PlatformView as IEmptyView)?.SetEmptyView(null, null); - return; } - if (emptyViewTemplate is DataTemplateSelector && emptyView is null) + if (_emptyView is not null && PlatformView is IEmptyView emptyView) { - _emptyView = null; - _mauiEmptyView = null; - (PlatformView as IEmptyView)?.SetEmptyView(null, null); - return; + emptyView.EmptyViewVisibility = WVisibility.Visible; } - // Resolve empty view - var oldMauiEmptyView = _mauiEmptyView; - _emptyView = emptyViewTemplate != null - ? ItemsViewExtensions.RealizeEmptyViewTemplate(emptyView, emptyViewTemplate, MauiContext!, ref _mauiEmptyView) - : emptyView switch - { - string text => new TextBlock - { - HorizontalAlignment = Microsoft.UI.Xaml.HorizontalAlignment.Center, - VerticalAlignment = Microsoft.UI.Xaml.VerticalAlignment.Center, - Text = text - }, - View view => ItemsViewExtensions.RealizeEmptyView(view, MauiContext!, ref _mauiEmptyView), - _ => ItemsViewExtensions.RealizeEmptyViewTemplate(emptyView, null, MauiContext!, ref _mauiEmptyView) - }; - - // Remove old logical child before adding the new one to prevent leak - if (oldMauiEmptyView is not null && _emptyViewDisplayed) - { - ItemsView.RemoveLogicalChild(oldMauiEmptyView); - _emptyViewDisplayed = false; - } - - (PlatformView as IEmptyView)?.SetEmptyView(_emptyView, _mauiEmptyView); - UpdateEmptyViewVisibility(); + _emptyViewDisplayed = true; } - - void UpdateEmptyViewVisibility() + else { - if (PlatformView is null || VirtualView is null) + if (_emptyViewDisplayed) { - return; - } - - // Check both CollectionViewSource.View and the underlying _itemsSource - // After a Reset action, CollectionViewSource.View.Count might not be immediately updated - bool isEmpty = (_collectionViewSource?.View?.Count ?? 0) == 0 && (_itemsSource?.Count ?? 0) == 0; - - if (isEmpty) - { - if (_mauiEmptyView is not null && !_emptyViewDisplayed) - { - if (ItemsView.EmptyViewTemplate is null) - { - ItemsView.AddLogicalChild(_mauiEmptyView); - } - } - if (_emptyView is not null && PlatformView is IEmptyView emptyView) { - emptyView.EmptyViewVisibility = WVisibility.Visible; + emptyView.EmptyViewVisibility = WVisibility.Collapsed; } - _emptyViewDisplayed = true; + ItemsView.RemoveLogicalChild(_mauiEmptyView); } - else - { - if (_emptyViewDisplayed) - { - if (_emptyView is not null && PlatformView is IEmptyView emptyView) - { - emptyView.EmptyViewVisibility = WVisibility.Collapsed; - } - ItemsView.RemoveLogicalChild(_mauiEmptyView); - } - - _emptyViewDisplayed = false; - } + _emptyViewDisplayed = false; } + } - void UpdateHeader() + void UpdateHeader() + { + if (Element is not StructuredItemsView structuredItemsView || PlatformView is null) { - if (Element is not StructuredItemsView structuredItemsView || PlatformView is null) - { - return; - } + return; + } - var header = structuredItemsView.Header; - var headerTemplate = structuredItemsView.HeaderTemplate; + var header = structuredItemsView.Header; + var headerTemplate = structuredItemsView.HeaderTemplate; - // Hide header only if both Header and HeaderTemplate are null - if (header is null && headerTemplate is null) + // Hide header only if both Header and HeaderTemplate are null + if (header is null && headerTemplate is null) + { + if (_headerDisplayed) { - if (_headerDisplayed) + if (PlatformView is MauiItemsView mauiItemsView) { - if (PlatformView is MauiItemsView mauiItemsView) - { - mauiItemsView.HeaderVisibility = WVisibility.Collapsed; - } + mauiItemsView.HeaderVisibility = WVisibility.Collapsed; + } - if (_mauiHeader is not null) - { - ItemsView.RemoveLogicalChild(_mauiHeader); - } - _headerDisplayed = false; + if (_mauiHeader is not null) + { + ItemsView.RemoveLogicalChild(_mauiHeader); } - return; + _headerDisplayed = false; } + return; + } - // Save old logical child reference before realization overwrites _mauiHeader via ref - var oldMauiHeader = _mauiHeader; + // Save old logical child reference before realization overwrites _mauiHeader via ref + var oldMauiHeader = _mauiHeader; - // If HeaderTemplate is set, use it regardless of header value - if (headerTemplate is not null) - { - var bindingContext = header ?? (object)string.Empty; - _header = ItemsViewExtensions.RealizeHeaderFooterTemplate(bindingContext, headerTemplate, MauiContext!, ref _mauiHeader); - } - else if (header is not null) + // If HeaderTemplate is set, use it regardless of header value + if (headerTemplate is not null) + { + var bindingContext = header ?? (object)string.Empty; + _header = ItemsViewExtensions.RealizeHeaderFooterTemplate(bindingContext, headerTemplate, MauiContext!, ref _mauiHeader); + } + else if (header is not null) + { + _header = header switch { - _header = header switch + string text => new TextBlock { - string text => new TextBlock - { - Text = text, - Margin = new Microsoft.UI.Xaml.Thickness(0, 0, 0, 10) - }, - View view => ItemsViewExtensions.RealizeHeaderFooterView(view, MauiContext!, ref _mauiHeader), - _ => new TextBlock - { - Text = header.ToString(), - Margin = new Microsoft.UI.Xaml.Thickness(0, 0, 0, 10) - } - }; - } - else - { - // This shouldn't happen due to null check above, but handle it safely - return; - } - - if (PlatformView is MauiItemsView platformItemsView && _header is not null) - { - platformItemsView.SetHeader(_header, _mauiHeader); - platformItemsView.HeaderVisibility = WVisibility.Visible; - } + Text = text, + Margin = new Microsoft.UI.Xaml.Thickness(0, 0, 0, 10) + }, + View view => ItemsViewExtensions.RealizeHeaderFooterView(view, MauiContext!, ref _mauiHeader), + _ => new TextBlock + { + Text = header.ToString(), + Margin = new Microsoft.UI.Xaml.Thickness(0, 0, 0, 10) + } + }; + } + else + { + // This shouldn't happen due to null check above, but handle it safely + return; + } - // Remove old logical child before adding the new one to prevent leak - if (oldMauiHeader is not null && _headerDisplayed) - { - ItemsView.RemoveLogicalChild(oldMauiHeader); - } + if (PlatformView is MauiItemsView platformItemsView && _header is not null) + { + platformItemsView.SetHeader(_header, _mauiHeader); + platformItemsView.HeaderVisibility = WVisibility.Visible; + } - if (_mauiHeader is not null) - { - ItemsView.AddLogicalChild(_mauiHeader); - } + // Remove old logical child before adding the new one to prevent leak + if (oldMauiHeader is not null && _headerDisplayed) + { + ItemsView.RemoveLogicalChild(oldMauiHeader); + } - _headerDisplayed = true; + if (_mauiHeader is not null) + { + ItemsView.AddLogicalChild(_mauiHeader); } - void UpdateFooter() + _headerDisplayed = true; + } + + void UpdateFooter() + { + if (Element is not StructuredItemsView structuredItemsView || PlatformView is null) { - if (Element is not StructuredItemsView structuredItemsView || PlatformView is null) - { - return; - } + return; + } - var footer = structuredItemsView.Footer; - var footerTemplate = structuredItemsView.FooterTemplate; + var footer = structuredItemsView.Footer; + var footerTemplate = structuredItemsView.FooterTemplate; - // Hide footer only if both Footer and FooterTemplate are null - if (footer is null && footerTemplate is null) + // Hide footer only if both Footer and FooterTemplate are null + if (footer is null && footerTemplate is null) + { + if (_footerDisplayed) { - if (_footerDisplayed) + if (PlatformView is MauiItemsView mauiItemsView) { - if (PlatformView is MauiItemsView mauiItemsView) - { - mauiItemsView.FooterVisibility = WVisibility.Collapsed; - } - - if (_mauiFooter is not null) - { - ItemsView.RemoveLogicalChild(_mauiFooter); - } - _footerDisplayed = false; + mauiItemsView.FooterVisibility = WVisibility.Collapsed; } - return; - } - - // Save old logical child reference before realization overwrites _mauiFooter via ref - var oldMauiFooter = _mauiFooter; - // If FooterTemplate is set, use it regardless of footer value - if (footerTemplate is not null) - { - var bindingContext = footer ?? (object)string.Empty; - _footer = ItemsViewExtensions.RealizeHeaderFooterTemplate(bindingContext, footerTemplate, MauiContext!, ref _mauiFooter); - } - else if (footer is not null) - { - _footer = footer switch + if (_mauiFooter is not null) { - string text => new TextBlock - { - Text = text, - Margin = new Microsoft.UI.Xaml.Thickness(0, 10, 0, 0) - }, - View view => ItemsViewExtensions.RealizeHeaderFooterView(view, MauiContext!, ref _mauiFooter), - _ => new TextBlock - { - Text = footer.ToString(), - Margin = new Microsoft.UI.Xaml.Thickness(0, 10, 0, 0) - } - }; - } - else - { - // This shouldn't happen due to null check above, but handle it safely - return; + ItemsView.RemoveLogicalChild(_mauiFooter); + } + _footerDisplayed = false; } + return; + } - if (PlatformView is MauiItemsView platformItemsView && _footer is not null) - { - platformItemsView.SetFooter(_footer, _mauiFooter); - platformItemsView.FooterVisibility = WVisibility.Visible; - } + // Save old logical child reference before realization overwrites _mauiFooter via ref + var oldMauiFooter = _mauiFooter; - // Remove old logical child before adding the new one to prevent leak - if (oldMauiFooter is not null && _footerDisplayed) + // If FooterTemplate is set, use it regardless of footer value + if (footerTemplate is not null) + { + var bindingContext = footer ?? (object)string.Empty; + _footer = ItemsViewExtensions.RealizeHeaderFooterTemplate(bindingContext, footerTemplate, MauiContext!, ref _mauiFooter); + } + else if (footer is not null) + { + _footer = footer switch { - ItemsView.RemoveLogicalChild(oldMauiFooter); - } + string text => new TextBlock + { + Text = text, + Margin = new Microsoft.UI.Xaml.Thickness(0, 10, 0, 0) + }, + View view => ItemsViewExtensions.RealizeHeaderFooterView(view, MauiContext!, ref _mauiFooter), + _ => new TextBlock + { + Text = footer.ToString(), + Margin = new Microsoft.UI.Xaml.Thickness(0, 10, 0, 0) + } + }; + } + else + { + // This shouldn't happen due to null check above, but handle it safely + return; + } - if (_mauiFooter is not null) - { - ItemsView.AddLogicalChild(_mauiFooter); - } + if (PlatformView is MauiItemsView platformItemsView && _footer is not null) + { + platformItemsView.SetFooter(_footer, _mauiFooter); + platformItemsView.FooterVisibility = WVisibility.Visible; + } - _footerDisplayed = true; + // Remove old logical child before adding the new one to prevent leak + if (oldMauiFooter is not null && _footerDisplayed) + { + ItemsView.RemoveLogicalChild(oldMauiFooter); } - void UpdateVerticalScrollBarVisibility() + if (_mauiFooter is not null) { - var scrollBarVisibility = Element.VerticalScrollBarVisibility; - var scrollView = PlatformView.ScrollView; + ItemsView.AddLogicalChild(_mauiFooter); + } - if (scrollView is null) - return; + _footerDisplayed = true; + } - if (scrollBarVisibility != ScrollBarVisibility.Default) - { - // If the value is changing to anything other than the default, record the default - if (_defaultVerticalScrollVisibility is null) - { - _defaultVerticalScrollVisibility = scrollView.VerticalScrollBarVisibility; - } - } + void UpdateVerticalScrollBarVisibility() + { + var scrollBarVisibility = Element.VerticalScrollBarVisibility; + var scrollView = PlatformView.ScrollView; - if (_defaultVerticalScrollVisibility is null) - { - // If the default has never been recorded, then this has never been set to anything but the - // default value; there's nothing to do. - return; - } + if (scrollView is null) + return; - switch (scrollBarVisibility) + if (scrollBarVisibility != ScrollBarVisibility.Default) + { + // If the value is changing to anything other than the default, record the default + if (_defaultVerticalScrollVisibility is null) { - case ScrollBarVisibility.Always: - scrollView.VerticalScrollBarVisibility = ScrollingScrollBarVisibility.Visible; - break; - case ScrollBarVisibility.Never: - scrollView.VerticalScrollBarVisibility = ScrollingScrollBarVisibility.Hidden; - break; - case ScrollBarVisibility.Default: - scrollView.VerticalScrollBarVisibility = _defaultVerticalScrollVisibility.Value; - break; + _defaultVerticalScrollVisibility = scrollView.VerticalScrollBarVisibility; } + } + if (_defaultVerticalScrollVisibility is null) + { + // If the default has never been recorded, then this has never been set to anything but the + // default value; there's nothing to do. + return; } - void UpdateHorizontalScrollBarVisibility() + switch (scrollBarVisibility) { - var scrollBarVisibility = Element.HorizontalScrollBarVisibility; - var scrollView = PlatformView.ScrollView; + case ScrollBarVisibility.Always: + scrollView.VerticalScrollBarVisibility = ScrollingScrollBarVisibility.Visible; + break; + case ScrollBarVisibility.Never: + scrollView.VerticalScrollBarVisibility = ScrollingScrollBarVisibility.Hidden; + break; + case ScrollBarVisibility.Default: + scrollView.VerticalScrollBarVisibility = _defaultVerticalScrollVisibility.Value; + break; + } - if (scrollView is null) - return; + } - if (scrollBarVisibility != ScrollBarVisibility.Default) - { - // If the value is changing to anything other than the default, record the default - if (_defaultHorizontalScrollVisibility is null) - { - _defaultHorizontalScrollVisibility = scrollView.HorizontalScrollBarVisibility; - } - } + void UpdateHorizontalScrollBarVisibility() + { + var scrollBarVisibility = Element.HorizontalScrollBarVisibility; + var scrollView = PlatformView.ScrollView; - if (_defaultHorizontalScrollVisibility is null) - { - // If the default has never been recorded, then this has never been set to anything but the - // default value; there's nothing to do. - return; - } + if (scrollView is null) + return; - switch (scrollBarVisibility) + if (scrollBarVisibility != ScrollBarVisibility.Default) + { + // If the value is changing to anything other than the default, record the default + if (_defaultHorizontalScrollVisibility is null) { - case ScrollBarVisibility.Always: - scrollView.HorizontalScrollBarVisibility = ScrollingScrollBarVisibility.Visible; - break; - case ScrollBarVisibility.Never: - scrollView.HorizontalScrollBarVisibility = ScrollingScrollBarVisibility.Hidden; - break; - case ScrollBarVisibility.Default: - scrollView.HorizontalScrollBarVisibility = _defaultHorizontalScrollVisibility.Value; - break; + _defaultHorizontalScrollVisibility = scrollView.HorizontalScrollBarVisibility; } - } - void PointerScrollChanged(object sender, PointerRoutedEventArgs e) + if (_defaultHorizontalScrollVisibility is null) { - if (PlatformView?.ScrollView is null) - return; + // If the default has never been recorded, then this has never been set to anything but the + // default value; there's nothing to do. + return; + } - if (PlatformView.ScrollView.ComputedHorizontalScrollMode == ScrollingScrollMode.Enabled) - { - PlatformView.ScrollView.AddScrollVelocity(new(e.GetCurrentPoint(PlatformView.ScrollView).Properties.MouseWheelDelta, 0), null); - } + switch (scrollBarVisibility) + { + case ScrollBarVisibility.Always: + scrollView.HorizontalScrollBarVisibility = ScrollingScrollBarVisibility.Visible; + break; + case ScrollBarVisibility.Never: + scrollView.HorizontalScrollBarVisibility = ScrollingScrollBarVisibility.Hidden; + break; + case ScrollBarVisibility.Default: + scrollView.HorizontalScrollBarVisibility = _defaultHorizontalScrollVisibility.Value; + break; } - void ScrollViewChanged(UI.Xaml.Controls.ScrollView sender, object args) + } + + void PointerScrollChanged(object sender, PointerRoutedEventArgs e) + { + if (PlatformView?.ScrollView is null) + return; + + if (PlatformView.ScrollView.ComputedHorizontalScrollMode == ScrollingScrollMode.Enabled) { - if (PlatformView?.ScrollView?.ScrollPresenter is null) - return; + PlatformView.ScrollView.AddScrollVelocity(new(e.GetCurrentPoint(PlatformView.ScrollView).Properties.MouseWheelDelta, 0), null); + } + } + + void ScrollViewChanged(UI.Xaml.Controls.ScrollView sender, object args) + { + if (PlatformView?.ScrollView?.ScrollPresenter is null) + return; - HandleScroll(PlatformView.ScrollView.ScrollPresenter); + HandleScroll(PlatformView.ScrollView.ScrollPresenter); + } + + void HandleScroll(WScrollPresenter scrollViewer) + { + if (_isScrollingForItemsUpdate) + { + _isScrollingForItemsUpdate = false; + return; } - void HandleScroll(WScrollPresenter scrollViewer) + var itemsViewScrolledEventArgs = new ItemsViewScrolledEventArgs { - if (_isScrollingForItemsUpdate) - { - _isScrollingForItemsUpdate = false; - return; - } + HorizontalOffset = scrollViewer.HorizontalOffset, + HorizontalDelta = scrollViewer.HorizontalOffset - _previousHorizontalOffset, + VerticalOffset = scrollViewer.VerticalOffset, + VerticalDelta = scrollViewer.VerticalOffset - _previousVerticalOffset, + }; - var itemsViewScrolledEventArgs = new ItemsViewScrolledEventArgs - { - HorizontalOffset = scrollViewer.HorizontalOffset, - HorizontalDelta = scrollViewer.HorizontalOffset - _previousHorizontalOffset, - VerticalOffset = scrollViewer.VerticalOffset, - VerticalDelta = scrollViewer.VerticalOffset - _previousVerticalOffset, - }; + _previousHorizontalOffset = scrollViewer.HorizontalOffset; + _previousVerticalOffset = scrollViewer.VerticalOffset; - _previousHorizontalOffset = scrollViewer.HorizontalOffset; - _previousVerticalOffset = scrollViewer.VerticalOffset; + bool advancing = true; + switch (Layout) + { + case LinearItemsLayout linearItemsLayout: + advancing = linearItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal + ? itemsViewScrolledEventArgs.HorizontalDelta > 0 + : itemsViewScrolledEventArgs.VerticalDelta > 0; + break; + case GridItemsLayout gridItemsLayout: + advancing = gridItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal + ? itemsViewScrolledEventArgs.HorizontalDelta > 0 + : itemsViewScrolledEventArgs.VerticalDelta > 0; + break; + default: + break; + } - bool advancing = true; - switch (Layout) - { - case LinearItemsLayout linearItemsLayout: - advancing = linearItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal - ? itemsViewScrolledEventArgs.HorizontalDelta > 0 - : itemsViewScrolledEventArgs.VerticalDelta > 0; - break; - case GridItemsLayout gridItemsLayout: - advancing = gridItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal - ? itemsViewScrolledEventArgs.HorizontalDelta > 0 - : itemsViewScrolledEventArgs.VerticalDelta > 0; - break; - default: - break; - } + itemsViewScrolledEventArgs = ComputeVisibleIndexes(itemsViewScrolledEventArgs, advancing); - itemsViewScrolledEventArgs = ComputeVisibleIndexes(itemsViewScrolledEventArgs, advancing); + Element.SendScrolled(itemsViewScrolledEventArgs); - Element.SendScrolled(itemsViewScrolledEventArgs); + var remainingItemsThreshold = Element.RemainingItemsThreshold; + if (_collectionViewSource != null && remainingItemsThreshold > -1) + { + var itemsRemaining = _collectionViewSource.View.Count - 1 - itemsViewScrolledEventArgs.LastVisibleItemIndex; - var remainingItemsThreshold = Element.RemainingItemsThreshold; - if (_collectionViewSource != null && remainingItemsThreshold > -1) + if (itemsRemaining <= remainingItemsThreshold) { - var itemsRemaining = _collectionViewSource.View.Count - 1 - itemsViewScrolledEventArgs.LastVisibleItemIndex; - - if (itemsRemaining <= remainingItemsThreshold) - { - if (itemsViewScrolledEventArgs.LastVisibleItemIndex > _lastRemainingItemsThresholdIndex) - { - _lastRemainingItemsThresholdIndex = itemsViewScrolledEventArgs.LastVisibleItemIndex; - Element.SendRemainingItemsThresholdReached(); - } - // When scrolling backward within the threshold zone, keep the - // high-water mark — don't reset, to avoid duplicate event fires. - // The reset happens in the outer else (when leaving the zone entirely) - // or in ItemsChanged (when new items are added). - } - else + if (itemsViewScrolledEventArgs.LastVisibleItemIndex > _lastRemainingItemsThresholdIndex) { - // Reset when scrolling away from the threshold zone so the - // event can re-fire when the user scrolls back. - _lastRemainingItemsThresholdIndex = -1; + _lastRemainingItemsThresholdIndex = itemsViewScrolledEventArgs.LastVisibleItemIndex; + Element.SendRemainingItemsThresholdReached(); } + // When scrolling backward within the threshold zone, keep the + // high-water mark — don't reset, to avoid duplicate event fires. + // The reset happens in the outer else (when leaving the zone entirely) + // or in ItemsChanged (when new items are added). + } + else + { + // Reset when scrolling away from the threshold zone so the + // event can re-fire when the user scrolls back. + _lastRemainingItemsThresholdIndex = -1; } } + } - ItemsViewScrolledEventArgs ComputeVisibleIndexes(ItemsViewScrolledEventArgs args, bool advancing) - { - var (firstVisibleItemIndex, lastVisibleItemIndex, centerItemIndex) = GetVisibleIndexes(advancing); + ItemsViewScrolledEventArgs ComputeVisibleIndexes(ItemsViewScrolledEventArgs args, bool advancing) + { + var (firstVisibleItemIndex, lastVisibleItemIndex, centerItemIndex) = GetVisibleIndexes(advancing); - args.FirstVisibleItemIndex = firstVisibleItemIndex; - args.CenterItemIndex = centerItemIndex; - args.LastVisibleItemIndex = lastVisibleItemIndex; + args.FirstVisibleItemIndex = firstVisibleItemIndex; + args.CenterItemIndex = centerItemIndex; + args.LastVisibleItemIndex = lastVisibleItemIndex; - return args; - } + return args; + } + + (int firstVisibleItemIndex, int lastVisibleItemIndex, int centerItemIndex) GetVisibleIndexes(bool advancing) + { + int firstVisibleItemIndex = -1; + int lastVisibleItemIndex = -1; - (int firstVisibleItemIndex, int lastVisibleItemIndex, int centerItemIndex) GetVisibleIndexes(bool advancing) + if (PlatformView is null) { - int firstVisibleItemIndex = -1; - int lastVisibleItemIndex = -1; + return (firstVisibleItemIndex, lastVisibleItemIndex, -1); + } - if (PlatformView is null) - { - return (firstVisibleItemIndex, lastVisibleItemIndex, -1); - } + PlatformView.TryGetItemIndex(0, 0, out firstVisibleItemIndex); + PlatformView.TryGetItemIndex(1, 1, out lastVisibleItemIndex); - PlatformView.TryGetItemIndex(0, 0, out firstVisibleItemIndex); - PlatformView.TryGetItemIndex(1, 1, out lastVisibleItemIndex); + double center = (lastVisibleItemIndex + firstVisibleItemIndex) / 2.0; + int centerItemIndex = advancing ? (int)Math.Ceiling(center) : (int)Math.Floor(center); - double center = (lastVisibleItemIndex + firstVisibleItemIndex) / 2.0; - int centerItemIndex = advancing ? (int)Math.Ceiling(center) : (int)Math.Floor(center); + return (firstVisibleItemIndex, lastVisibleItemIndex, centerItemIndex); + } - return (firstVisibleItemIndex, lastVisibleItemIndex, centerItemIndex); + void ScrollToRequested(object? sender, ScrollToRequestEventArgs args) + { + // Use base.PlatformView to avoid InvalidOperationException + if (base.PlatformView is not WItemsView platformView) + { + return; } - void ScrollToRequested(object? sender, ScrollToRequestEventArgs args) - { - // Use base.PlatformView to avoid InvalidOperationException - if (base.PlatformView is not WItemsView platformView) - { - return; - } + int index; - int index; + // Handle grouped scrolling by position + if (args.Mode == ScrollToMode.Position && + ItemsView is GroupableItemsView groupableItemsView && + groupableItemsView.IsGrouped && + args.GroupIndex >= 0) + { + index = FindGroupedItemIndex(args.GroupIndex, args.Index); + } + else if (args.Mode == ScrollToMode.Element) + { + index = FindItemIndex(args.Item); + } + else + { + // Non-grouped position-based scroll + index = args.Index; + } - // Handle grouped scrolling by position - if (args.Mode == ScrollToMode.Position && - ItemsView is GroupableItemsView groupableItemsView && - groupableItemsView.IsGrouped && - args.GroupIndex >= 0) - { - index = FindGroupedItemIndex(args.GroupIndex, args.Index); - } - else if (args.Mode == ScrollToMode.Element) - { - index = FindItemIndex(args.Item); - } - else - { - // Non-grouped position-based scroll - index = args.Index; - } + // Validate index is within bounds + var itemCount = _collectionViewSource?.View?.Count ?? 0; + if (index < 0 || index >= itemCount) + { + return; + } - // Validate index is within bounds - var itemCount = _collectionViewSource?.View?.Count ?? 0; - if (index < 0 || index >= itemCount) - { - return; - } + double offset = AlignToStart; + switch (args.ScrollToPosition) + { + case ScrollToPosition.Start: + offset = AlignToStart; + break; + case ScrollToPosition.Center: + offset = AlignToCenter; + break; + case ScrollToPosition.End: + offset = AlignToEnd; + break; + } - double offset = AlignToStart; - switch (args.ScrollToPosition) - { - case ScrollToPosition.Start: - offset = AlignToStart; - break; - case ScrollToPosition.Center: - offset = AlignToCenter; - break; - case ScrollToPosition.End: - offset = AlignToEnd; - break; - } + platformView.StartBringItemIntoView(index, new BringIntoViewOptions() + { + AnimationDesired = args.IsAnimated, + VerticalAlignmentRatio = offset, + HorizontalAlignmentRatio = offset + }); + } - platformView.StartBringItemIntoView(index, new BringIntoViewOptions() - { - AnimationDesired = args.IsAnimated, - VerticalAlignmentRatio = offset, - HorizontalAlignmentRatio = offset - }); + /// + /// Finds the flat index in the flattened grouped collection for the specified group and item index. + /// + int FindGroupedItemIndex(int groupIndex, int itemIndex) + { + if (_collectionViewSource is null) + { + return -1; } - /// - /// Finds the flat index in the flattened grouped collection for the specified group and item index. - /// - int FindGroupedItemIndex(int groupIndex, int itemIndex) + if (ItemsView is not GroupableItemsView groupableItemsView) { - if (_collectionViewSource is null) - { - return -1; - } - - if (ItemsView is not GroupableItemsView groupableItemsView) - { - return -1; - } + return -1; + } - var itemsSource = groupableItemsView.ItemsSource; - if (itemsSource is null) - { - return -1; - } + var itemsSource = groupableItemsView.ItemsSource; + if (itemsSource is null) + { + return -1; + } - var hasGroupHeader = groupableItemsView.GroupHeaderTemplate is not null; - var hasGroupFooter = groupableItemsView.GroupFooterTemplate is not null; + var hasGroupHeader = groupableItemsView.GroupHeaderTemplate is not null; + var hasGroupFooter = groupableItemsView.GroupFooterTemplate is not null; - int flatIndex = 0; - int currentGroupIndex = 0; + int flatIndex = 0; + int currentGroupIndex = 0; - foreach (var group in itemsSource) + foreach (var group in itemsSource) + { + if (group is IList groupList) { - if (group is IList groupList) + if (currentGroupIndex == groupIndex) { - if (currentGroupIndex == groupIndex) - { - // Found the target group - if (hasGroupHeader) - { - flatIndex++; // Skip group header - } - - // Check if itemIndex is within bounds of this group - if (itemIndex < 0 || itemIndex >= groupList.Count) - { - return -1; - } - - // Return the calculated flat index - return flatIndex + itemIndex; - } - - // Count items in this group to move to next group + // Found the target group if (hasGroupHeader) { - flatIndex++; + flatIndex++; // Skip group header } - flatIndex += groupList.Count; - if (hasGroupFooter) + + // Check if itemIndex is within bounds of this group + if (itemIndex < 0 || itemIndex >= groupList.Count) { - flatIndex++; + return -1; } + + // Return the calculated flat index + return flatIndex + itemIndex; } - currentGroupIndex++; + // Count items in this group to move to next group + if (hasGroupHeader) + { + flatIndex++; + } + flatIndex += groupList.Count; + if (hasGroupFooter) + { + flatIndex++; + } } - // Group index not found + currentGroupIndex++; + } + + // Group index not found + return -1; + } + + /// + /// Finds the index of an item in the collection view by searching through the flattened list. + /// Used for non-grouped ScrollToMode.Element requests. + /// + int FindItemIndex(object item) + { + if (_collectionViewSource is null) + { return -1; } - /// - /// Finds the index of an item in the collection view by searching through the flattened list. - /// Used for non-grouped ScrollToMode.Element requests. - /// - int FindItemIndex(object item) + for (int index = 0; index < _collectionViewSource.View.Count; index++) { - if (_collectionViewSource is null) - { - return -1; - } + var viewItem = _collectionViewSource.View[index]; - for (int index = 0; index < _collectionViewSource.View.Count; index++) + // Check for ItemTemplateContext (non-grouped templated items) + if (viewItem is ItemTemplateContext pair) { - var viewItem = _collectionViewSource.View[index]; - - // Check for ItemTemplateContext (non-grouped templated items) - if (viewItem is ItemTemplateContext pair) + if (Equals(pair.Item, item)) { - if (Equals(pair.Item, item)) - { - return index; - } - } - // Check for ItemTemplateContext2 (grouped templated items) - else if (viewItem is ItemTemplateContext2 pair2) - { - // Skip headers and footers, only match actual items - if (!pair2.IsHeader && !pair2.IsFooter && Equals(pair2.Item, item)) - { - return index; - } + return index; } - // Check for non-templated items (direct equality) - else if (Equals(viewItem, item)) + } + // Check for ItemTemplateContext2 (grouped templated items) + else if (viewItem is ItemTemplateContext2 pair2) + { + // Skip headers and footers, only match actual items + if (!pair2.IsHeader && !pair2.IsFooter && Equals(pair2.Item, item)) { return index; } } - - return -1; + // Check for non-templated items (direct equality) + else if (Equals(viewItem, item)) + { + return index; + } } + + return -1; } -} +} \ No newline at end of file diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs index 7dc4724daf3d..f276deac9633 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/CollectionViewHandler2.Windows.cs @@ -129,7 +129,7 @@ protected override void ConnectHandler(WItemsView platformView) { Source = ItemsView, Path = new UI.Xaml.PropertyPath("SelectionMode"), - Converter = new SelectionModeConvert(), + Converter = new SelectionModeConverter(), Mode = UI.Xaml.Data.BindingMode.TwoWay }); @@ -392,13 +392,13 @@ static int FindItemIndexInSource(ICollectionView itemList, object targetItem) /// /// Converts between MAUI and WinUI values. /// -partial class SelectionModeConvert : UI.Xaml.Data.IValueConverter +partial class SelectionModeConverter : UI.Xaml.Data.IValueConverter { /// public object Convert(object value, Type targetType, object parameter, string language) { - var formSelectionMode = (SelectionMode)value; - switch (formSelectionMode) + var selectionMode = (SelectionMode)value; + switch (selectionMode) { case SelectionMode.Single: return ItemsViewSelectionMode.Single; @@ -412,8 +412,8 @@ public object Convert(object value, Type targetType, object parameter, string la /// public object ConvertBack(object value, Type targetType, object parameter, string language) { - var uwpListViewSelectionMode = (ItemsViewSelectionMode)value; - switch (uwpListViewSelectionMode) + var winUISelectionMode = (ItemsViewSelectionMode)value; + switch (winUISelectionMode) { case ItemsViewSelectionMode.None: return SelectionMode.None; diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs index 0fb8e63c643f..d600d31d235f 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs @@ -3,363 +3,367 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; -namespace Microsoft.Maui.Controls.Handlers.Items2 +namespace Microsoft.Maui.Controls.Handlers.Items2; +/// +/// An observable collection that flattens grouped items into a single list with +/// header/footer contexts per group, keeping in sync with the source via INCC. +/// +internal class GroupedItemTemplateCollection2 : ObservableCollection { - /// - /// An observable collection that flattens grouped items into a single list with - /// header/footer contexts per group, keeping in sync with the source via INCC. - /// - internal class GroupedItemTemplateCollection2 : ObservableCollection + readonly IEnumerable _itemsSource; + readonly DataTemplate _itemTemplate; + readonly DataTemplate? _groupHeaderTemplate; + readonly DataTemplate? _groupFooterTemplate; + readonly BindableObject _container; + readonly IMauiContext? _mauiContext; + readonly Dictionary _groupSubscriptions = new(); + bool _suppressNotifications; + + public GroupedItemTemplateCollection2(IEnumerable itemsSource, + DataTemplate itemTemplate, DataTemplate groupHeaderTemplate, DataTemplate groupFooterTemplate, + BindableObject container, IMauiContext? mauiContext = null) { - readonly IEnumerable _itemsSource; - readonly DataTemplate _itemTemplate; - readonly DataTemplate? _groupHeaderTemplate; - readonly DataTemplate? _groupFooterTemplate; - readonly BindableObject _container; - readonly IMauiContext? _mauiContext; - readonly Dictionary _groupSubscriptions = new(); - bool _suppressNotifications; - - public GroupedItemTemplateCollection2(IEnumerable itemsSource, - DataTemplate itemTemplate, DataTemplate groupHeaderTemplate, DataTemplate groupFooterTemplate, - BindableObject container, IMauiContext? mauiContext = null) - { - _itemsSource = itemsSource; - _itemTemplate = itemTemplate; - _groupHeaderTemplate = groupHeaderTemplate; - _groupFooterTemplate = groupFooterTemplate; - _container = container; - _mauiContext = mauiContext; + _itemsSource = itemsSource; + _itemTemplate = itemTemplate; + _groupHeaderTemplate = groupHeaderTemplate; + _groupFooterTemplate = groupFooterTemplate; + _container = container; + _mauiContext = mauiContext; - RebuildFlatList(); - SubscribeToGroups(_itemsSource); + RebuildFlatList(); + SubscribeToGroups(_itemsSource); - if (_itemsSource is INotifyCollectionChanged incc) - { - incc.CollectionChanged += GroupsChanged; - } + if (_itemsSource is INotifyCollectionChanged incc) + { + incc.CollectionChanged += OnGroupsChanged; } + } - protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + if (!_suppressNotifications) { - if (!_suppressNotifications) - { - base.OnCollectionChanged(e); - } + base.OnCollectionChanged(e); } + } - ItemTemplateContext2 CreateItemContext(object item) => - new(_itemTemplate, item, _container, mauiContext: _mauiContext); + ItemTemplateContext2 CreateItemContext(object item) => + new(_itemTemplate, item, _container, mauiContext: _mauiContext); - ItemTemplateContext2 CreateHeaderContext(object group) => - new(_groupHeaderTemplate!, group, _container, null, null, null, isHeader: true, isFooter: false, mauiContext: _mauiContext); + ItemTemplateContext2? CreateHeaderContext(object group) => + _groupHeaderTemplate is not null + ? new(_groupHeaderTemplate, group, _container, null, null, null, isHeader: true, isFooter: false, mauiContext: _mauiContext) + : null; - ItemTemplateContext2 CreateFooterContext(object group) => - new(_groupFooterTemplate!, group, _container, null, null, null, isHeader: false, isFooter: true, mauiContext: _mauiContext); + ItemTemplateContext2? CreateFooterContext(object group) => + _groupFooterTemplate is not null + ? new(_groupFooterTemplate, group, _container, null, null, null, isHeader: false, isFooter: true, mauiContext: _mauiContext) + : null; - void SubscribeToGroups(IEnumerable? groups) - { - if (groups is null) - return; + void SubscribeToGroups(IEnumerable? groups) + { + if (groups is null) + return; - foreach (var group in groups) - { - SubscribeToGroup(group); - } + foreach (var group in groups) + { + SubscribeToGroup(group); } + } - void SubscribeToGroup(object group) + void SubscribeToGroup(object group) + { + if (group is INotifyCollectionChanged incc && !_groupSubscriptions.ContainsKey(group)) { - if (group is INotifyCollectionChanged incc && !_groupSubscriptions.ContainsKey(group)) - { - var handler = CreateGroupItemsChangedHandler(group); - incc.CollectionChanged += handler; - _groupSubscriptions[group] = handler; - } + var handler = CreateGroupItemsChangedHandler(group); + incc.CollectionChanged += handler; + _groupSubscriptions[group] = handler; } + } + + NotifyCollectionChangedEventHandler CreateGroupItemsChangedHandler(object group) => + (s, e) => _container.Dispatcher.DispatchIfRequired(() => OnGroupItemsChanged(group, e)); - NotifyCollectionChangedEventHandler CreateGroupItemsChangedHandler(object group) => - (s, e) => _container.Dispatcher.DispatchIfRequired(() => GroupItemsChanged(group, e)); + void UnsubscribeFromGroups(IEnumerable? groups) + { + if (groups is null) + return; - void UnsubscribeFromGroups(IEnumerable? groups) + foreach (var group in groups) { - if (groups is null) - return; + UnsubscribeFromGroup(group); + } + } - foreach (var group in groups) + void UnsubscribeFromGroup(object group) + { + if (_groupSubscriptions.TryGetValue(group, out var handler)) + { + if (group is INotifyCollectionChanged incc) { - UnsubscribeFromGroup(group); + incc.CollectionChanged -= handler; } + _groupSubscriptions.Remove(group); } + } - void UnsubscribeFromGroup(object group) + void UnsubscribeFromAllGroups() + { + foreach (var kvp in _groupSubscriptions) { - if (_groupSubscriptions.TryGetValue(group, out var handler)) + if (kvp.Key is INotifyCollectionChanged incc) { - if (group is INotifyCollectionChanged incc) - { - incc.CollectionChanged -= handler; - } - _groupSubscriptions.Remove(group); + incc.CollectionChanged -= kvp.Value; } } + _groupSubscriptions.Clear(); + } + + void RebuildFlatList() + { + Items.Clear(); - void UnsubscribeFromAllGroups() + foreach (var group in _itemsSource) { - foreach (var kvp in _groupSubscriptions) + if (group is not IList itemsList) + continue; + + var header = CreateHeaderContext(group); + if (header is not null) { - if (kvp.Key is INotifyCollectionChanged incc) - { - incc.CollectionChanged -= kvp.Value; - } + Items.Add(header); } - _groupSubscriptions.Clear(); - } - void RebuildFlatList() - { - Items.Clear(); + foreach (var item in itemsList) + { + Items.Add(CreateItemContext(item)); + } - foreach (var group in _itemsSource) + var footer = CreateFooterContext(group); + if (footer is not null) { - if (group is not IList itemsList) - continue; + Items.Add(footer); + } + } + } - if (_groupHeaderTemplate is not null) - { - Items.Add(CreateHeaderContext(group)); - } + void OnGroupsChanged(object? sender, NotifyCollectionChangedEventArgs args) => + _container.Dispatcher.DispatchIfRequired(() => OnGroupsCollectionChanged(args)); - foreach (var item in itemsList) + void OnGroupsCollectionChanged(NotifyCollectionChangedEventArgs args) + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + SubscribeToGroups(args.NewItems); + ResetWithoutResubscribe(); + break; + case NotifyCollectionChangedAction.Move: + ResetWithoutResubscribe(); + break; + case NotifyCollectionChangedAction.Remove: + if (args.OldItems is not null) { - Items.Add(CreateItemContext(item)); + UnsubscribeFromGroups(args.OldItems); + ResetWithoutResubscribe(); } + break; - if (_groupFooterTemplate is not null) - { - Items.Add(CreateFooterContext(group)); - } - } + case NotifyCollectionChangedAction.Replace: + case NotifyCollectionChangedAction.Reset: + Reset(); + break; } + } - void GroupsChanged(object? sender, NotifyCollectionChangedEventArgs args) => - _container.Dispatcher.DispatchIfRequired(() => OnGroupsCollectionChanged(args)); + /// + /// Gets the flat index for the first item in a group by iterating through the source collection. + /// Returns the index after the group header (if present), pointing to where group items start. + /// + int GetFlatIndexForGroupItems(object targetGroup) + { + int flatIndex = 0; - void OnGroupsCollectionChanged(NotifyCollectionChangedEventArgs args) + foreach (var group in _itemsSource) { - switch (args.Action) + if (ReferenceEquals(group, targetGroup)) { - case NotifyCollectionChangedAction.Add: - SubscribeToGroups(args.NewItems); - ResetWithoutResubscribe(); - break; - case NotifyCollectionChangedAction.Move: - ResetWithoutResubscribe(); - break; - case NotifyCollectionChangedAction.Remove: - if (args.OldItems is not null) - { - UnsubscribeFromGroups(args.OldItems); - ResetWithoutResubscribe(); - } - break; - - case NotifyCollectionChangedAction.Replace: - case NotifyCollectionChangedAction.Reset: - Reset(); - break; + // Found the group - return index after header (if present) + return _groupHeaderTemplate is not null ? flatIndex + 1 : flatIndex; } - } - /// - /// Gets the flat index for the first item in a group by iterating through the source collection. - /// Returns the index after the group header (if present), pointing to where group items start. - /// - int GetFlatIndexForGroupItems(object targetGroup) - { - int flatIndex = 0; - - foreach (var group in _itemsSource) + if (group is IList itemsList) { - if (ReferenceEquals(group, targetGroup)) - { - // Found the group - return index after header (if present) - return _groupHeaderTemplate is not null ? flatIndex + 1 : flatIndex; - } - - if (group is IList itemsList) - { - // Count header + items + footer for this group - if (_groupHeaderTemplate is not null) - flatIndex++; + // Count header + items + footer for this group + if (_groupHeaderTemplate is not null) + flatIndex++; - flatIndex += itemsList.Count; + flatIndex += itemsList.Count; - if (_groupFooterTemplate is not null) - flatIndex++; - } + if (_groupFooterTemplate is not null) + flatIndex++; } - - return -1; } - void GroupItemsChanged(object group, NotifyCollectionChangedEventArgs e) - { - int flatIndex = GetFlatIndexForGroupItems(group); - if (flatIndex == -1) - return; + return -1; + } - if (group is not IList groupList) - return; + void OnGroupItemsChanged(object group, NotifyCollectionChangedEventArgs e) + { + int flatIndex = GetFlatIndexForGroupItems(group); + if (flatIndex == -1) + return; - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: - HandleGroupItemsAdd(e, flatIndex, groupList); - break; + if (group is not IList groupList) + return; - case NotifyCollectionChangedAction.Remove: - HandleGroupItemsRemove(e, flatIndex); - break; + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + HandleGroupItemsAdd(e, flatIndex, groupList); + break; - case NotifyCollectionChangedAction.Replace: - HandleGroupItemsReplace(e, flatIndex); - break; + case NotifyCollectionChangedAction.Remove: + HandleGroupItemsRemove(e, flatIndex); + break; - case NotifyCollectionChangedAction.Move: - HandleGroupItemsMove(e, flatIndex); - break; + case NotifyCollectionChangedAction.Replace: + HandleGroupItemsReplace(e, flatIndex); + break; - case NotifyCollectionChangedAction.Reset: - ResetWithoutResubscribe(); - break; - } - } + case NotifyCollectionChangedAction.Move: + HandleGroupItemsMove(e, flatIndex); + break; - void HandleGroupItemsAdd(NotifyCollectionChangedEventArgs e, int flatIndex, IList groupList) - { - if (e.NewItems is null) - return; + case NotifyCollectionChangedAction.Reset: + ResetWithoutResubscribe(); + break; + } + } - int insertIndex = flatIndex + (e.NewStartingIndex >= 0 ? e.NewStartingIndex : groupList.Count - e.NewItems.Count); - var newItems = new List(e.NewItems.Count); + void HandleGroupItemsAdd(NotifyCollectionChangedEventArgs e, int flatIndex, IList groupList) + { + if (e.NewItems is null) + return; - _suppressNotifications = true; - foreach (var item in e.NewItems) - { - var newItem = CreateItemContext(item); - newItems.Add(newItem); - Items.Insert(insertIndex++, newItem); - } - _suppressNotifications = false; + int insertIndex = flatIndex + (e.NewStartingIndex >= 0 ? e.NewStartingIndex : groupList.Count - e.NewItems.Count); + var newItems = new List(e.NewItems.Count); - OnCollectionChanged(new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Add, newItems, insertIndex - newItems.Count)); + _suppressNotifications = true; + foreach (var item in e.NewItems) + { + var newItem = CreateItemContext(item); + newItems.Add(newItem); + Items.Insert(insertIndex++, newItem); } + _suppressNotifications = false; - void HandleGroupItemsRemove(NotifyCollectionChangedEventArgs e, int flatIndex) - { - if (e.OldItems is null) - return; + OnCollectionChanged(new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Add, newItems, insertIndex - newItems.Count)); + } - int removeIndex = flatIndex + (e.OldStartingIndex >= 0 ? e.OldStartingIndex : 0); - var removedItems = new List(e.OldItems.Count); + void HandleGroupItemsRemove(NotifyCollectionChangedEventArgs e, int flatIndex) + { + if (e.OldItems is null) + return; - _suppressNotifications = true; - for (int i = 0; i < e.OldItems.Count; i++) - { - removedItems.Add(Items[removeIndex]); - Items.RemoveAt(removeIndex); - } - _suppressNotifications = false; + int removeIndex = flatIndex + (e.OldStartingIndex >= 0 ? e.OldStartingIndex : 0); + var removedItems = new List(e.OldItems.Count); - OnCollectionChanged(new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Remove, removedItems, removeIndex)); + _suppressNotifications = true; + for (int i = 0; i < e.OldItems.Count; i++) + { + removedItems.Add(Items[removeIndex]); + Items.RemoveAt(removeIndex); } + _suppressNotifications = false; - void HandleGroupItemsReplace(NotifyCollectionChangedEventArgs e, int flatIndex) - { - if (e.NewItems is null || e.OldItems is null) - return; + OnCollectionChanged(new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Remove, removedItems, removeIndex)); + } - int replaceIndex = flatIndex + (e.NewStartingIndex >= 0 ? e.NewStartingIndex : 0); - var oldItems = new List(e.NewItems.Count); - var newItems = new List(e.NewItems.Count); + void HandleGroupItemsReplace(NotifyCollectionChangedEventArgs e, int flatIndex) + { + if (e.NewItems is null || e.OldItems is null) + return; - _suppressNotifications = true; - for (int i = 0; i < e.NewItems.Count; i++) - { - oldItems.Add(Items[replaceIndex + i]); - var newItem = CreateItemContext(e.NewItems[i]!); - newItems.Add(newItem); - Items[replaceIndex + i] = newItem; - } - _suppressNotifications = false; + int replaceIndex = flatIndex + (e.NewStartingIndex >= 0 ? e.NewStartingIndex : 0); + var oldItems = new List(e.NewItems.Count); + var newItems = new List(e.NewItems.Count); - OnCollectionChanged(new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Replace, newItems, oldItems, replaceIndex)); + _suppressNotifications = true; + for (int i = 0; i < e.NewItems.Count; i++) + { + oldItems.Add(Items[replaceIndex + i]); + var newItem = CreateItemContext(e.NewItems[i]!); + newItems.Add(newItem); + Items[replaceIndex + i] = newItem; } + _suppressNotifications = false; - void HandleGroupItemsMove(NotifyCollectionChangedEventArgs e, int flatIndex) - { - if (e.OldItems is null) - return; + OnCollectionChanged(new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Replace, newItems, oldItems, replaceIndex)); + } - int oldIndex = flatIndex + (e.OldStartingIndex >= 0 ? e.OldStartingIndex : 0); - int newIndex = flatIndex + (e.NewStartingIndex >= 0 ? e.NewStartingIndex : 0); - var movedItems = new List(e.OldItems.Count); + void HandleGroupItemsMove(NotifyCollectionChangedEventArgs e, int flatIndex) + { + if (e.OldItems is null) + return; - _suppressNotifications = true; - for (int i = 0; i < e.OldItems.Count; i++) - { - var item = Items[oldIndex]; - Items.RemoveAt(oldIndex); - Items.Insert(newIndex + i, item); - movedItems.Add(item); - } - _suppressNotifications = false; + int oldIndex = flatIndex + (e.OldStartingIndex >= 0 ? e.OldStartingIndex : 0); + int newIndex = flatIndex + (e.NewStartingIndex >= 0 ? e.NewStartingIndex : 0); + var movedItems = new List(e.OldItems.Count); - OnCollectionChanged(new NotifyCollectionChangedEventArgs( - NotifyCollectionChangedAction.Move, movedItems, newIndex, oldIndex)); + _suppressNotifications = true; + for (int i = 0; i < e.OldItems.Count; i++) + { + var item = Items[oldIndex]; + Items.RemoveAt(oldIndex); + Items.Insert(newIndex + i, item); + movedItems.Add(item); } + _suppressNotifications = false; - void ResetWithoutResubscribe() - { - _suppressNotifications = true; - RebuildFlatList(); - _suppressNotifications = false; + OnCollectionChanged(new NotifyCollectionChangedEventArgs( + NotifyCollectionChangedAction.Move, movedItems, newIndex, oldIndex)); + } - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } + void ResetWithoutResubscribe() + { + _suppressNotifications = true; + RebuildFlatList(); + _suppressNotifications = false; - /// - /// Full reset that also resubscribes to all groups. - /// Used for Replace and Reset actions where group references may have changed. - /// - public void Reset() - { - UnsubscribeFromAllGroups(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } - _suppressNotifications = true; - RebuildFlatList(); - _suppressNotifications = false; + /// + /// Full reset that also resubscribes to all groups. + /// Used for Replace and Reset actions where group references may have changed. + /// + void Reset() + { + UnsubscribeFromAllGroups(); - SubscribeToGroups(_itemsSource); - OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); - } + _suppressNotifications = true; + RebuildFlatList(); + _suppressNotifications = false; - /// - /// Unsubscribes from all group and top-level collection changed events. - /// Must be called when the collection is being replaced or the handler disconnects. - /// - public void CleanUp() - { - UnsubscribeFromAllGroups(); + SubscribeToGroups(_itemsSource); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } - if (_itemsSource is INotifyCollectionChanged incc) - { - incc.CollectionChanged -= GroupsChanged; - } + /// + /// Unsubscribes from all group and top-level collection changed events. + /// Must be called when the collection is being replaced or the handler disconnects. + /// + internal void CleanUp() + { + UnsubscribeFromAllGroups(); + + if (_itemsSource is INotifyCollectionChanged incc) + { + incc.CollectionChanged -= OnGroupsChanged; } } } diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs index 4dd4c64097d5..9f2297fd0ecc 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemFactory.cs @@ -3,220 +3,218 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -namespace Microsoft.Maui.Controls.Handlers.Items2 +namespace Microsoft.Maui.Controls.Handlers.Items2; +/// +/// Element factory that creates, recycles, and manages elements +/// for the WinUI ItemsView/ItemsRepeater, using a template-keyed recycle pool. +/// +internal partial class ItemFactory(ItemsView view) : IElementFactory { + readonly ItemsView _view = view; + Dictionary> _recyclePool = new(); + + internal static readonly BindableProperty OriginTemplateProperty = + BindableProperty.CreateAttached( + "OriginTemplate", typeof(DataTemplate), typeof(ItemFactory), null); + /// - /// Element factory that creates, recycles, and manages elements - /// for the WinUI ItemsView/ItemsRepeater, using a template-keyed recycle pool. + /// Creates or retrieves a recycled for the given data context. /// - internal partial class ItemFactory(ItemsView view) : IElementFactory + public UIElement? GetElement(ElementFactoryGetArgs args) { - private readonly ItemsView _view = view; - private Dictionary> _recyclePool = new(); - - internal static readonly BindableProperty OriginTemplateProperty = - BindableProperty.CreateAttached( - "OriginTemplate", typeof(DataTemplate), typeof(ItemFactory), null); - - /// - /// Creates or retrieves a recycled for the given data context. - /// - public UIElement? GetElement(ElementFactoryGetArgs args) + // NOTE: 1.6: replace w/ RecyclePool + if (args.Data is ItemTemplateContext2 templateContext) { - // NOTE: 1.6: replace w/ RecyclePool - if (args.Data is ItemTemplateContext2 templateContext) + DataTemplate? template = templateContext.MauiDataTemplate; + if (template is DataTemplateSelector selector) { - DataTemplate? template = templateContext.MauiDataTemplate; - if (template is DataTemplateSelector selector) - { - template = selector.SelectTemplate(templateContext.Item, _view); - } + template = selector.SelectTemplate(templateContext.Item, _view); + } - if (template is null) - { - template = _view.EmptyViewTemplate; - } + if (template is null) + { + template = _view.EmptyViewTemplate; + } - ItemContainer? container = null; - ElementWrapper? wrapper = null; + ItemContainer? container = null; + ElementWrapper? wrapper = null; - if (_recyclePool.TryGetValue(template, out var itemContainers)) + if (_recyclePool.TryGetValue(template, out var itemContainers)) + { + if (itemContainers.Count > 0) { - if (itemContainers.Count > 0) + container = itemContainers[0]; + if (container is not null) { - container = itemContainers[0]; - if (container is not null) - { - wrapper = container.Child as ElementWrapper; - } - - itemContainers.RemoveAt(0); + wrapper = container.Child as ElementWrapper; } + + itemContainers.RemoveAt(0); } + } - if (wrapper is null) + if (wrapper is null) + { + var viewContent = template.CreateContent() as View; + if (_view.Handler?.MauiContext is not null && viewContent is not null) { - var viewContent = template.CreateContent() as View; - if (_view.Handler?.MauiContext is not null && viewContent is not null) + wrapper = new ElementWrapper(_view.Handler.MauiContext); + wrapper.HorizontalAlignment = HorizontalAlignment.Stretch; + wrapper.HorizontalContentAlignment = HorizontalAlignment.Stretch; + wrapper.VerticalAlignment = VerticalAlignment.Stretch; + wrapper.VerticalContentAlignment = VerticalAlignment.Stretch; + wrapper.SetContent(viewContent); + wrapper.IsHeaderOrFooter = templateContext.IsHeader || templateContext.IsFooter; + + if (wrapper.VirtualView is View virtualView) { - wrapper = new ElementWrapper(_view.Handler.MauiContext); - wrapper.HorizontalAlignment = HorizontalAlignment.Stretch; - wrapper.HorizontalContentAlignment = HorizontalAlignment.Stretch; - wrapper.VerticalAlignment = VerticalAlignment.Stretch; - wrapper.VerticalContentAlignment = VerticalAlignment.Stretch; - wrapper.SetContent(viewContent); - wrapper.IsHeaderOrFooter = templateContext.IsHeader || templateContext.IsFooter; - - if (wrapper.VirtualView is View virtualView) - { - virtualView.SetValue(OriginTemplateProperty, template); - } + virtualView.SetValue(OriginTemplateProperty, template); } } + } - if (wrapper?.VirtualView is View view) + if (wrapper?.VirtualView is View view) + { + view.BindingContext = templateContext.Item ?? _view.BindingContext; + _view.AddLogicalChild(view); + if (_view is SelectableItemsView selectableItemsView && selectableItemsView.SelectionMode != SelectionMode.None) { - view.BindingContext = templateContext.Item ?? _view.BindingContext; - _view.AddLogicalChild(view); - if (_view is SelectableItemsView selectableItemsView && selectableItemsView.SelectionMode != SelectionMode.None) + bool isSelected = false; + if (selectableItemsView.SelectionMode == SelectionMode.Single) + isSelected = selectableItemsView.SelectedItem == templateContext.Item; + else + isSelected = selectableItemsView.SelectedItems.Contains(templateContext.Item); + + if (isSelected && view is VisualElement visualElement) { - bool isSelected = false; - if (selectableItemsView.SelectionMode == SelectionMode.Single) - isSelected = selectableItemsView.SelectedItem == templateContext.Item; - else - isSelected = selectableItemsView.SelectedItems.Contains(templateContext.Item); - - if (isSelected && view is VisualElement visualElement) - { - VisualStateManager.GoToState(visualElement, VisualStateManager.CommonStates.Selected); - } + VisualStateManager.GoToState(visualElement, VisualStateManager.CommonStates.Selected); } - } - container ??= new ItemContainer() - { - Child = wrapper, - VerticalAlignment = VerticalAlignment.Stretch, - HorizontalAlignment = HorizontalAlignment.Stretch - }; - - return container; } - return null; + container ??= new ItemContainer() + { + Child = wrapper, + VerticalAlignment = VerticalAlignment.Stretch, + HorizontalAlignment = HorizontalAlignment.Stretch + }; + + return container; } - /// - /// Returns an element to the recycle pool, keyed by its original . - /// - public void RecycleElement(ElementFactoryRecycleArgs args) + return null; + } + + /// + /// Returns an element to the recycle pool, keyed by its original . + /// + public void RecycleElement(ElementFactoryRecycleArgs args) + { + var item = args.Element as ItemContainer; + var wrapper = item?.Child as ElementWrapper; + var wrapperView = wrapper?.VirtualView as View; + DataTemplate? template = wrapperView?.GetValue(OriginTemplateProperty) as DataTemplate; + if (template != null && item is not null) { - var item = args.Element as ItemContainer; - var wrapper = item?.Child as ElementWrapper; - var wrapperView = wrapper?.VirtualView as View; - DataTemplate? template = wrapperView?.GetValue(OriginTemplateProperty) as DataTemplate; - if (template != null && item is not null) + if (_recyclePool.TryGetValue(template, out var itemContainers)) { - if (_recyclePool.TryGetValue(template, out var itemContainers)) - { - itemContainers.Add(item); - } - else - { - _recyclePool[template] = new List { item }; - } + itemContainers.Add(item); + } + else + { + _recyclePool[template] = new List { item }; } - - _view.RemoveLogicalChild(wrapperView); } - /// - /// Clears the recycle pool and removes logical children held by pooled elements. - /// Must be called when the items source changes or when the handler disconnects - /// to prevent memory leaks from pooled ItemContainers holding strong references. - /// - internal void CleanUp() + _view.RemoveLogicalChild(wrapperView); + } + + /// + /// Clears the recycle pool and removes logical children held by pooled elements. + /// Must be called when the items source changes or when the handler disconnects + /// to prevent memory leaks from pooled ItemContainers holding strong references. + /// + internal void CleanUp() + { + foreach (var kvp in _recyclePool) { - foreach (var kvp in _recyclePool) + foreach (var container in kvp.Value) { - foreach (var container in kvp.Value) + var wrapper = container?.Child as ElementWrapper; + var wrapperView = wrapper?.VirtualView as View; + if (wrapperView is not null) { - var wrapper = container?.Child as ElementWrapper; - var wrapperView = wrapper?.VirtualView as View; - if (wrapperView is not null) - { - _view.RemoveLogicalChild(wrapperView); - } + _view.RemoveLogicalChild(wrapperView); } } - - _recyclePool.Clear(); } + + _recyclePool.Clear(); } +} - /// - /// A wrapper that hosts a MAUI inside a WinUI element tree. - /// Handles MeasureFirstItem optimization by caching the first measured size. - /// - internal partial class ElementWrapper(IMauiContext context) : ContentControl - { - /// The MAUI virtual view hosted by this wrapper. - public IView? VirtualView { get; private set; } +/// +/// A wrapper that hosts a MAUI inside a WinUI element tree. +/// Handles MeasureFirstItem optimization by caching the first measured size. +/// +internal partial class ElementWrapper(IMauiContext context) : ContentControl +{ + /// The MAUI virtual view hosted by this wrapper. + public IView? VirtualView { get; private set; } - private IMauiContext _context = context; + IMauiContext _context = context; - /// Whether this wrapper hosts a group header or footer (excluded from size caching). - public bool IsHeaderOrFooter { get; set; } + /// Whether this wrapper hosts a group header or footer (excluded from size caching). + public bool IsHeaderOrFooter { get; set; } - /// - /// Sets the MAUI view content, converting it to a platform element. - /// Only sets content if not already initialized. - /// - public void SetContent(IView view) + /// + /// Sets the MAUI view content, converting it to a platform element. + /// Only sets content if not already initialized. + /// + public void SetContent(IView view) + { + if (VirtualView is null || VirtualView.Handler is null) { - if (VirtualView is null || VirtualView.Handler is null) - { - Content = view.ToPlatform(_context); - VirtualView = view; - } + Content = view.ToPlatform(_context); + VirtualView = view; } + } - protected override global::Windows.Foundation.Size MeasureOverride(global::Windows.Foundation.Size availableSize) + protected override global::Windows.Foundation.Size MeasureOverride(global::Windows.Foundation.Size availableSize) + { + // Access handler through view parent chain (same pattern as iOS) + CollectionViewHandler2? handler = null; + if (VirtualView is View view && + view.Parent is ItemsView itemsView && + itemsView.Handler is CollectionViewHandler2 cvHandler) { - // Access handler through view parent chain (same pattern as iOS) - CollectionViewHandler2? handler = null; - if (VirtualView is View view && - view.Parent is ItemsView itemsView && - itemsView.Handler is CollectionViewHandler2 cvHandler) - { - handler = cvHandler; - } + handler = cvHandler; + } - // Check if we should use cached first item size - var cachedSize = handler?.GetCachedFirstItemSize() ?? global::Windows.Foundation.Size.Empty; + // Check if we should use cached first item size + var cachedSize = handler?.GetCachedFirstItemSize() ?? global::Windows.Foundation.Size.Empty; - if (!cachedSize.IsEmpty) - { - // Use cached size for MeasureFirstItem strategy - base.MeasureOverride(cachedSize); - return cachedSize; - } + if (!cachedSize.IsEmpty) + { + // Use cached size for MeasureFirstItem strategy + base.MeasureOverride(cachedSize); + return cachedSize; + } - // Measure normally - var measuredSize = base.MeasureOverride(availableSize); + // Measure normally + var measuredSize = base.MeasureOverride(availableSize); - // Cache the size if this is the first item being measured - if (handler != null && !IsHeaderOrFooter) + // Cache the size if this is the first item being measured + if (handler != null && !IsHeaderOrFooter) + { + var currentCached = handler.GetCachedFirstItemSize(); + if (currentCached.IsEmpty) { - var currentCached = handler.GetCachedFirstItemSize(); - if (currentCached.IsEmpty) - { - handler.SetCachedFirstItemSize(measuredSize); - } + handler.SetCachedFirstItemSize(measuredSize); } - - return measuredSize; } + + return measuredSize; } -} +} \ No newline at end of file diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs index dc62a6b0f819..29fa6376655f 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs @@ -37,7 +37,7 @@ internal class ItemTemplateContext2 /// Whether this context represents a group footer. public bool IsFooter { get; } - public ItemTemplateContext2(DataTemplate mauiDataTemplate, object item, BindableObject container, + ItemTemplateContext2(DataTemplate mauiDataTemplate, object item, BindableObject container, double? height = null, double? width = null, Thickness? itemSpacing = null, bool isHeader = false, bool isFooter = false, IMauiContext? mauiContext = null) { diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextEnumerable2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextEnumerable2.cs index 60f9ad435580..a3d30a498455 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextEnumerable2.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextEnumerable2.cs @@ -1,46 +1,39 @@ using System.Collections; -namespace Microsoft.Maui.Controls.Handlers.Items2 +namespace Microsoft.Maui.Controls.Handlers.Items2; +/// +/// Wraps a non-list items source, yielding an +/// for each item on enumeration. +/// Used when the source does not implement . +/// +internal class ItemTemplateContextEnumerable2 : IEnumerable { - /// - /// Wraps a non-list items source, yielding an - /// for each item on enumeration. - /// Used when the source does not implement . - /// - internal class ItemTemplateContextEnumerable2 : IEnumerable - { - readonly IEnumerable _itemsSource; - readonly DataTemplate _formsDataTemplate; - readonly BindableObject _container; - readonly IMauiContext? _mauiContext; - readonly double _itemHeight; - readonly double _itemWidth; - readonly Thickness _itemSpacing; - - public ItemTemplateContextEnumerable2(IEnumerable itemsSource, DataTemplate formsDataTemplate, BindableObject container, - double? itemHeight = null, double? itemWidth = null, Thickness? itemSpacing = null, IMauiContext? mauiContext = null) - { - _itemsSource = itemsSource; - _formsDataTemplate = formsDataTemplate; - _container = container; - _mauiContext = mauiContext; - if (itemHeight.HasValue) - _itemHeight = itemHeight.Value; - - if (itemWidth.HasValue) - _itemWidth = itemWidth.Value; + readonly IEnumerable _itemsSource; + readonly DataTemplate _itemTemplate; + readonly BindableObject _container; + readonly IMauiContext? _mauiContext; + readonly double _itemHeight; + readonly double _itemWidth; + readonly Thickness _itemSpacing; - if (itemSpacing.HasValue) - _itemSpacing = itemSpacing.Value; - } + ItemTemplateContextEnumerable2(IEnumerable itemsSource, DataTemplate itemTemplate, BindableObject container, + double? itemHeight = null, double? itemWidth = null, Thickness? itemSpacing = null, IMauiContext? mauiContext = null) + { + _itemsSource = itemsSource; + _itemTemplate = itemTemplate; + _container = container; + _mauiContext = mauiContext; + _itemHeight = itemHeight ?? 0; + _itemWidth = itemWidth ?? 0; + _itemSpacing = itemSpacing ?? default; + } - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() + { + foreach (var item in _itemsSource) { - foreach (var item in _itemsSource) - { - yield return new ItemTemplateContext2(_formsDataTemplate, item, _container, _itemHeight, _itemWidth, _itemSpacing, - false, false, _mauiContext); - } + yield return new ItemTemplateContext2(_itemTemplate, item, _container, _itemHeight, _itemWidth, _itemSpacing, + false, false, _mauiContext); } } } \ No newline at end of file diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextList2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextList2.cs index 262fda6f2452..beeaf251a3d2 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextList2.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextList2.cs @@ -2,111 +2,102 @@ using System.Collections; using System.Collections.Generic; -namespace Microsoft.Maui.Controls.Handlers.Items2 +namespace Microsoft.Maui.Controls.Handlers.Items2; +/// +/// A lazily-populated read-only list that wraps an items source, +/// creating instances on demand and caching them by index. +/// Used for non-observable list sources. +/// +internal class ItemTemplateContextList2 : IReadOnlyList { - /// - /// A lazily-populated read-only list that wraps an items source, - /// creating instances on demand and caching them by index. - /// Used for non-observable list sources. - /// - internal class ItemTemplateContextList2 : IReadOnlyList - { - readonly IList _itemsSource; - readonly DataTemplate _itemTemplate; - readonly BindableObject _container; - readonly IMauiContext? _mauiContext; - readonly double _itemHeight; - readonly double _itemWidth; - readonly Thickness _itemSpacing; + readonly IList _itemsSource; + readonly DataTemplate _itemTemplate; + readonly BindableObject _container; + readonly IMauiContext? _mauiContext; + readonly double _itemHeight; + readonly double _itemWidth; + readonly Thickness _itemSpacing; - readonly Dictionary _itemTemplateContexts; + readonly Dictionary _itemTemplateContexts; - public int Count => _itemsSource.Count; + public int Count => _itemsSource.Count; - public ItemTemplateContext2 this[int index] + public ItemTemplateContext2 this[int index] + { + get { - get + if (!_itemTemplateContexts.TryGetValue(index, out var context)) { - if (!_itemTemplateContexts.TryGetValue(index, out var context)) - { - var item = _itemsSource[index]; - if (item is null) - throw new InvalidOperationException($"Item at index {index} is null. ItemTemplateContext2 requires a non-null item."); - - _itemTemplateContexts[index] = context = new ItemTemplateContext2(_itemTemplate, item, - _container, _itemHeight, _itemWidth, _itemSpacing, false, false, _mauiContext); - } - - return context; + var item = _itemsSource[index]; + if (item is null) + throw new InvalidOperationException($"Item at index {index} is null. ItemTemplateContext2 requires a non-null item."); + + _itemTemplateContexts[index] = context = new ItemTemplateContext2(_itemTemplate, item, + _container, _itemHeight, _itemWidth, _itemSpacing, false, false, _mauiContext); } + + return context; } + } - public ItemTemplateContextList2(IList itemsSource, DataTemplate itemTemplate, BindableObject container, - double? itemHeight = null, double? itemWidth = null, Thickness? itemSpacing = null, IMauiContext? mauiContext = null) - { - _itemsSource = itemsSource; - _itemTemplate = itemTemplate; - _container = container; - _mauiContext = mauiContext; - if (itemHeight.HasValue) - _itemHeight = itemHeight.Value; + public ItemTemplateContextList2(IList itemsSource, DataTemplate itemTemplate, BindableObject container, + double? itemHeight = null, double? itemWidth = null, Thickness? itemSpacing = null, IMauiContext? mauiContext = null) + { + _itemsSource = itemsSource; + _itemTemplate = itemTemplate; + _container = container; + _mauiContext = mauiContext; + _itemHeight = itemHeight ?? 0; + _itemWidth = itemWidth ?? 0; + _itemSpacing = itemSpacing ?? default; + + // Cap initial dictionary capacity at 64 to avoid over-allocation for small collections + _itemTemplateContexts = new(capacity: Math.Min(64, _itemsSource.Count)); + } - if (itemWidth.HasValue) - _itemWidth = itemWidth.Value; + public IEnumerator GetEnumerator() + { + return new ItemTemplateContextListEnumerator2(this); + } - if (itemSpacing.HasValue) - _itemSpacing = itemSpacing.Value; + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } - // Cap initial dictionary capacity at 64 to avoid over-allocation for small collections - _itemTemplateContexts = new(capacity: Math.Min(64, _itemsSource.Count)); - } + internal class ItemTemplateContextListEnumerator2 : IEnumerator + { + public ItemTemplateContext2 Current { get; private set; } + object IEnumerator.Current => Current; + int _currentIndex = -1; + ItemTemplateContextList2 _itemTemplateContextList; - public IEnumerator GetEnumerator() + public ItemTemplateContextListEnumerator2(ItemTemplateContextList2 itemTemplateContextList) { - return new ItemTemplateContextListEnumerator2(this); + _itemTemplateContextList = itemTemplateContextList; } - IEnumerator IEnumerable.GetEnumerator() + public void Dispose() { - return GetEnumerator(); } -#nullable disable - internal class ItemTemplateContextListEnumerator2 : IEnumerator + public bool MoveNext() { - public ItemTemplateContext2 Current { get; private set; } - object IEnumerator.Current => Current; - int _currentIndex = -1; - private ItemTemplateContextList2 _itemTemplateContextList; - - public ItemTemplateContextListEnumerator2(ItemTemplateContextList2 observableItemTemplateCollection) - { - _itemTemplateContextList = observableItemTemplateCollection; - } - - public void Dispose() + if (_currentIndex >= _itemTemplateContextList.Count - 1) { + return false; } - public bool MoveNext() - { - if (_currentIndex >= _itemTemplateContextList.Count - 1) - { - return false; - } - - _currentIndex += 1; - Current = _itemTemplateContextList[_currentIndex]; + _currentIndex += 1; + Current = _itemTemplateContextList[_currentIndex]; - return true; - } + return true; + } - public void Reset() - { - Current = null; - _currentIndex = -1; - } + public void Reset() + { + Current = null; + _currentIndex = -1; } -#nullable enable } } \ No newline at end of file diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs b/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs index 4ed698e3af14..e44ce991bd64 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs @@ -4,279 +4,212 @@ using Microsoft.UI.Xaml.Controls; using WApp = Microsoft.UI.Xaml.Application; using WControlTemplate = Microsoft.UI.Xaml.Controls.ControlTemplate; -using WVisibility = Microsoft.UI.Xaml.Visibility; using WStackPanel = Microsoft.UI.Xaml.Controls.StackPanel; using WScrollView = Microsoft.UI.Xaml.Controls.ScrollView; +using WVisibility = Microsoft.UI.Xaml.Visibility; -namespace Microsoft.Maui.Controls.Handlers.Items2 +namespace Microsoft.Maui.Controls.Handlers.Items2; +/// +/// Custom subclass that adds support for +/// empty views, headers, footers, and layout orientation for the CollectionView Handler 2. +/// +internal partial class MauiItemsView : UI.Xaml.Controls.ItemsView, IEmptyView { - /// - /// Custom subclass that adds support for - /// empty views, headers, footers, and layout orientation for the CollectionView Handler 2. - /// - internal partial class MauiItemsView : UI.Xaml.Controls.ItemsView, IEmptyView - { - ContentControl? _emptyViewContentControl; - FrameworkElement? _emptyView; - View? _mauiEmptyView; - - ContentControl? _headerContentControl; - FrameworkElement? _header; - View? _mauiHeader; - - ContentControl? _footerContentControl; - FrameworkElement? _footer; - View? _mauiFooter; - - WStackPanel? _containerPanel; - FrameworkElement? _itemsRepeater; - bool _isHorizontalLayout; - WScrollView? _scrollView; - - public MauiItemsView() - { - Template = (WControlTemplate)WApp.Current.Resources["MauiItemsViewTemplate"]; - } + ContentControl? _emptyViewContentControl; + FrameworkElement? _emptyView; + View? _mauiEmptyView; + + ContentControl? _headerContentControl; + FrameworkElement? _header; - public static readonly DependencyProperty EmptyViewVisibilityProperty = - DependencyProperty.Register(nameof(EmptyViewVisibility), typeof(Visibility), - typeof(MauiItemsView), new PropertyMetadata(WVisibility.Collapsed, EmptyViewVisibilityChanged)); + ContentControl? _footerContentControl; + FrameworkElement? _footer; - static void EmptyViewVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + WStackPanel? _containerPanel; + FrameworkElement? _itemsRepeater; + bool _isHorizontalLayout; + WScrollView? _scrollView; + + MauiItemsView() + { + Template = (WControlTemplate)WApp.Current.Resources["MauiItemsViewTemplate"]; + } + + /// Gets or sets the visibility of the empty view overlay. + public WVisibility EmptyViewVisibility + { + get => _emptyViewContentControl?.Visibility ?? WVisibility.Collapsed; + set { - if (d is MauiItemsView itemsView) + if (_emptyViewContentControl is not null) { - // Update this manually; normally we'd just bind this, but TemplateBinding doesn't seem to work - // for WASDK right now. - itemsView.UpdateEmptyViewVisibility((WVisibility)e.NewValue); + _emptyViewContentControl.Visibility = value; } } + } - /// Gets or sets the visibility of the empty view overlay. - public WVisibility EmptyViewVisibility + /// Gets or sets the visibility of the header element. + internal WVisibility HeaderVisibility + { + get => _headerContentControl?.Visibility ?? WVisibility.Collapsed; + set { - get - { - return (WVisibility)GetValue(EmptyViewVisibilityProperty); - } - set + if (_headerContentControl is not null) { - SetValue(EmptyViewVisibilityProperty, value); + _headerContentControl.Visibility = value; } } - - public static readonly DependencyProperty HeaderVisibilityProperty = - DependencyProperty.Register(nameof(HeaderVisibility), typeof(Visibility), - typeof(MauiItemsView), new PropertyMetadata(WVisibility.Collapsed, HeaderVisibilityChanged)); + } - static void HeaderVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + /// Gets or sets the visibility of the footer element. + internal WVisibility FooterVisibility + { + get => _footerContentControl?.Visibility ?? WVisibility.Collapsed; + set { - if (d is MauiItemsView itemsView) + if (_footerContentControl is not null) { - itemsView.UpdateHeaderVisibility((WVisibility)e.NewValue); + _footerContentControl.Visibility = value; } } + } + + /// Sets the empty view content and its MAUI view counterpart. + public void SetEmptyView(FrameworkElement emptyView, View mauiEmptyView) + { + _emptyView = emptyView; + _mauiEmptyView = mauiEmptyView; - /// Gets or sets the visibility of the header element. - public WVisibility HeaderVisibility + if (_emptyViewContentControl is not null) { - get => (WVisibility)GetValue(HeaderVisibilityProperty); - set => SetValue(HeaderVisibilityProperty, value); + _emptyViewContentControl.Content = emptyView; } - - public static readonly DependencyProperty FooterVisibilityProperty = - DependencyProperty.Register(nameof(FooterVisibility), typeof(Visibility), - typeof(MauiItemsView), new PropertyMetadata(WVisibility.Collapsed, FooterVisibilityChanged)); + } + + /// Sets the header content and its MAUI view counterpart. + internal void SetHeader(FrameworkElement header, View? mauiHeader) + { + _header = header; - static void FooterVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + if (_headerContentControl is not null) { - if (d is MauiItemsView itemsView) - { - itemsView.UpdateFooterVisibility((WVisibility)e.NewValue); - } + _headerContentControl.Content = header; } + } + + /// Sets the footer content and its MAUI view counterpart. + internal void SetFooter(FrameworkElement footer, View? mauiFooter) + { + _footer = footer; - /// Gets or sets the visibility of the footer element. - public WVisibility FooterVisibility + if (_footerContentControl is not null) { - get => (WVisibility)GetValue(FooterVisibilityProperty); - set => SetValue(FooterVisibilityProperty, value); + _footerContentControl.Content = footer; } + } - /// Sets the empty view content and its MAUI view counterpart. - public void SetEmptyView(FrameworkElement emptyView, View mauiEmptyView) - { - _emptyView = emptyView; - _mauiEmptyView = mauiEmptyView; + protected override void OnApplyTemplate() + { + base.OnApplyTemplate(); - if (_emptyViewContentControl is not null) - { - _emptyViewContentControl.Content = emptyView; - UpdateEmptyViewVisibility(EmptyViewVisibility); - } - } - - /// Sets the header content and its MAUI view counterpart. - public void SetHeader(FrameworkElement header, View? mauiHeader) - { - _header = header; - _mauiHeader = mauiHeader; + _emptyViewContentControl = GetTemplateChild("EmptyViewContentControl") as ContentControl; + _headerContentControl = GetTemplateChild("HeaderContentControl") as ContentControl; + _footerContentControl = GetTemplateChild("FooterContentControl") as ContentControl; + _containerPanel = GetTemplateChild("PART_ContainerStack") as WStackPanel; + _itemsRepeater = GetTemplateChild("PART_ItemsRepeater") as FrameworkElement; + _scrollView = GetTemplateChild("PART_ScrollView") as WScrollView; - if (_headerContentControl is not null) - { - _headerContentControl.Content = header; - UpdateHeaderVisibility(HeaderVisibility); - } - } - - /// Sets the footer content and its MAUI view counterpart. - public void SetFooter(FrameworkElement footer, View? mauiFooter) + if (_emptyView is not null && _emptyViewContentControl is not null) { - _footer = footer; - _mauiFooter = mauiFooter; - - if (_footerContentControl is not null) - { - _footerContentControl.Content = footer; - UpdateFooterVisibility(FooterVisibility); - } + _emptyViewContentControl.Content = _emptyView; } - protected override void OnApplyTemplate() + if (_header is not null && _headerContentControl is not null) { - base.OnApplyTemplate(); - - _emptyViewContentControl = GetTemplateChild("EmptyViewContentControl") as ContentControl; - _headerContentControl = GetTemplateChild("HeaderContentControl") as ContentControl; - _footerContentControl = GetTemplateChild("FooterContentControl") as ContentControl; - _containerPanel = GetTemplateChild("PART_ContainerGrid") as WStackPanel; - _itemsRepeater = GetTemplateChild("PART_ItemsRepeater") as FrameworkElement; - _scrollView = GetTemplateChild("PART_ScrollView") as WScrollView; - - if (_emptyView is not null && _emptyViewContentControl is not null) - { - _emptyViewContentControl.Content = _emptyView; - UpdateEmptyViewVisibility(EmptyViewVisibility); - } - - if (_header is not null && _headerContentControl is not null) - { - _headerContentControl.Content = _header; - UpdateHeaderVisibility(HeaderVisibility); - } - - if (_footer is not null && _footerContentControl is not null) - { - _footerContentControl.Content = _footer; - UpdateFooterVisibility(FooterVisibility); - } - - // Apply orientation if it was set before template was applied - ApplyLayoutOrientation(); + _headerContentControl.Content = _header; } - - /// Sets the layout orientation and updates the visual tree accordingly. - public void SetLayoutOrientation(bool isHorizontal) + + if (_footer is not null && _footerContentControl is not null) { - _isHorizontalLayout = isHorizontal; - ApplyLayoutOrientation(); + _footerContentControl.Content = _footer; } - void ApplyLayoutOrientation() - { - if (_containerPanel is null || _headerContentControl is null || _footerContentControl is null || _itemsRepeater is null) - { - return; - } + // Apply orientation if it was set before template was applied + ApplyLayoutOrientation(); + } - if (_isHorizontalLayout) - { - _containerPanel.Orientation = Orientation.Horizontal; - // For horizontal layout, the container panel should stretch vertically - _containerPanel.VerticalAlignment = UI.Xaml.VerticalAlignment.Stretch; - _containerPanel.HorizontalAlignment = UI.Xaml.HorizontalAlignment.Left; - - // Items should stretch vertically (cross-axis) - _itemsRepeater.VerticalAlignment = UI.Xaml.VerticalAlignment.Stretch; - _itemsRepeater.HorizontalAlignment = UI.Xaml.HorizontalAlignment.Left; - _headerContentControl.VerticalAlignment = UI.Xaml.VerticalAlignment.Stretch; - _headerContentControl.VerticalContentAlignment = UI.Xaml.VerticalAlignment.Stretch; - _headerContentControl.HorizontalContentAlignment = UI.Xaml.HorizontalAlignment.Left; - _footerContentControl.VerticalAlignment = UI.Xaml.VerticalAlignment.Stretch; - _footerContentControl.VerticalContentAlignment = UI.Xaml.VerticalAlignment.Stretch; - _footerContentControl.HorizontalContentAlignment = UI.Xaml.HorizontalAlignment.Left; - - if (_scrollView is not null) - { - _scrollView.ContentOrientation = UI.Xaml.Controls.ScrollingContentOrientation.Horizontal; - } - } - else - { - _containerPanel.Orientation = Orientation.Vertical; - // For vertical layout, the container panel should stretch horizontally - _containerPanel.VerticalAlignment = UI.Xaml.VerticalAlignment.Top; - _containerPanel.HorizontalAlignment = UI.Xaml.HorizontalAlignment.Stretch; - - // Items should stretch horizontally (cross-axis) - _itemsRepeater.VerticalAlignment = UI.Xaml.VerticalAlignment.Top; - _itemsRepeater.HorizontalAlignment = UI.Xaml.HorizontalAlignment.Stretch; - _headerContentControl.VerticalAlignment = UI.Xaml.VerticalAlignment.Top; - _headerContentControl.VerticalContentAlignment = UI.Xaml.VerticalAlignment.Top; - _headerContentControl.HorizontalContentAlignment = UI.Xaml.HorizontalAlignment.Stretch; - _footerContentControl.VerticalAlignment = UI.Xaml.VerticalAlignment.Top; - _footerContentControl.VerticalContentAlignment = UI.Xaml.VerticalAlignment.Top; - _footerContentControl.HorizontalContentAlignment = UI.Xaml.HorizontalAlignment.Stretch; - - if (_scrollView is not null) - { - _scrollView.ContentOrientation = UI.Xaml.Controls.ScrollingContentOrientation.Vertical; - } - } - } + /// Sets the layout orientation and updates the visual tree accordingly. + internal void SetLayoutOrientation(bool isHorizontal) + { + _isHorizontalLayout = isHorizontal; + ApplyLayoutOrientation(); + } - protected override global::Windows.Foundation.Size MeasureOverride(global::Windows.Foundation.Size availableSize) + void ApplyLayoutOrientation() + { + if (_containerPanel is null || _headerContentControl is null || _footerContentControl is null || _itemsRepeater is null) { - _mauiEmptyView?.Measure(availableSize.Width, availableSize.Height); - - return base.MeasureOverride(availableSize); + return; } - protected override global::Windows.Foundation.Size ArrangeOverride(global::Windows.Foundation.Size finalSize) + if (_isHorizontalLayout) { - _mauiEmptyView?.Arrange(new Rect(0, 0, finalSize.Width, finalSize.Height)); + _containerPanel.Orientation = Orientation.Horizontal; + // For horizontal layout, the container panel should stretch vertically + _containerPanel.VerticalAlignment = UI.Xaml.VerticalAlignment.Stretch; + _containerPanel.HorizontalAlignment = UI.Xaml.HorizontalAlignment.Left; - return base.ArrangeOverride(finalSize); - } + // Items should stretch vertically (cross-axis) + _itemsRepeater.VerticalAlignment = UI.Xaml.VerticalAlignment.Stretch; + _itemsRepeater.HorizontalAlignment = UI.Xaml.HorizontalAlignment.Left; + _headerContentControl.VerticalAlignment = UI.Xaml.VerticalAlignment.Stretch; + _headerContentControl.VerticalContentAlignment = UI.Xaml.VerticalAlignment.Stretch; + _headerContentControl.HorizontalContentAlignment = UI.Xaml.HorizontalAlignment.Left; + _footerContentControl.VerticalAlignment = UI.Xaml.VerticalAlignment.Stretch; + _footerContentControl.VerticalContentAlignment = UI.Xaml.VerticalAlignment.Stretch; + _footerContentControl.HorizontalContentAlignment = UI.Xaml.HorizontalAlignment.Left; - void UpdateEmptyViewVisibility(WVisibility visibility) - { - if (_emptyViewContentControl is null) + if (_scrollView is not null) { - return; + _scrollView.ContentOrientation = UI.Xaml.Controls.ScrollingContentOrientation.Horizontal; } - - _emptyViewContentControl.Visibility = visibility; } - - void UpdateHeaderVisibility(WVisibility visibility) + else { - if (_headerContentControl is null) - { - return; - } + _containerPanel.Orientation = Orientation.Vertical; + // For vertical layout, the container panel should stretch horizontally + _containerPanel.VerticalAlignment = UI.Xaml.VerticalAlignment.Top; + _containerPanel.HorizontalAlignment = UI.Xaml.HorizontalAlignment.Stretch; - _headerContentControl.Visibility = visibility; - } - - void UpdateFooterVisibility(WVisibility visibility) - { - if (_footerContentControl is null) + // Items should stretch horizontally (cross-axis) + _itemsRepeater.VerticalAlignment = UI.Xaml.VerticalAlignment.Top; + _itemsRepeater.HorizontalAlignment = UI.Xaml.HorizontalAlignment.Stretch; + _headerContentControl.VerticalAlignment = UI.Xaml.VerticalAlignment.Top; + _headerContentControl.VerticalContentAlignment = UI.Xaml.VerticalAlignment.Top; + _headerContentControl.HorizontalContentAlignment = UI.Xaml.HorizontalAlignment.Stretch; + _footerContentControl.VerticalAlignment = UI.Xaml.VerticalAlignment.Top; + _footerContentControl.VerticalContentAlignment = UI.Xaml.VerticalAlignment.Top; + _footerContentControl.HorizontalContentAlignment = UI.Xaml.HorizontalAlignment.Stretch; + + if (_scrollView is not null) { - return; + _scrollView.ContentOrientation = UI.Xaml.Controls.ScrollingContentOrientation.Vertical; } - - _footerContentControl.Visibility = visibility; } } + + protected override global::Windows.Foundation.Size MeasureOverride(global::Windows.Foundation.Size availableSize) + { + _mauiEmptyView?.Measure(availableSize.Width, availableSize.Height); + + return base.MeasureOverride(availableSize); + } + + protected override global::Windows.Foundation.Size ArrangeOverride(global::Windows.Foundation.Size finalSize) + { + _mauiEmptyView?.Arrange(new Rect(0, 0, finalSize.Width, finalSize.Height)); + + return base.ArrangeOverride(finalSize); + } + } diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ObservableItemTemplateCollection2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ObservableItemTemplateCollection2.cs index 7abe9122804d..0213db2a6274 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/ObservableItemTemplateCollection2.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ObservableItemTemplateCollection2.cs @@ -3,293 +3,305 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; -namespace Microsoft.Maui.Controls.Handlers.Items2 +namespace Microsoft.Maui.Controls.Handlers.Items2; +/// +/// An observable collection of that mirrors an +/// items source, keeping the template collection +/// synchronized with the underlying data and supporting drag/drop reordering. +/// +internal class ObservableItemTemplateCollection2 : ObservableCollection { + readonly IList _itemsSource; + readonly DataTemplate _itemTemplate; + readonly BindableObject _container; + readonly IMauiContext? _mauiContext; + readonly double _itemHeight; + readonly double _itemWidth; + readonly Thickness _itemSpacing; + NotifyCollectionChangedEventHandler _innerCollectionChangedHandler; + readonly WeakNotifyCollectionChangedProxy _proxy = new(); + + bool _innerCollectionChange = false; + bool _observeChanges = true; + + ~ObservableItemTemplateCollection2() => _proxy.Unsubscribe(); + + ObservableItemTemplateCollection2(IList itemsSource, DataTemplate itemTemplate, BindableObject container, + double? itemHeight = null, double? itemWidth = null, Thickness? itemSpacing = null, IMauiContext? mauiContext = null) + { + _itemsSource = itemsSource; + _itemTemplate = itemTemplate; + _container = container; + _mauiContext = mauiContext; + _itemHeight = itemHeight ?? 0; + _itemWidth = itemWidth ?? 0; + _itemSpacing = itemSpacing ?? default; + + PopulateInitialItems(itemsSource, itemTemplate, container); + SubscribeToSourceChanges(itemsSource); + } + /// - /// An observable collection of that mirrors an - /// items source, keeping the template collection - /// synchronized with the underlying data and supporting drag/drop reordering. + /// Subscribes to collection change events on the items source and this collection. /// - internal class ObservableItemTemplateCollection2 : ObservableCollection + void SubscribeToSourceChanges(IList itemsSource) { - readonly IList _itemsSource; - readonly DataTemplate _itemTemplate; - readonly BindableObject _container; - readonly IMauiContext? _mauiContext; - readonly double _itemHeight; - readonly double _itemWidth; - readonly Thickness _itemSpacing; - readonly NotifyCollectionChangedEventHandler _collectionChanged; - readonly WeakNotifyCollectionChangedProxy _proxy = new(); - - bool _innerCollectionChange = false; - bool _observeChanges = true; - - ~ObservableItemTemplateCollection2() => _proxy.Unsubscribe(); - - public ObservableItemTemplateCollection2(IList itemsSource, DataTemplate itemTemplate, BindableObject container, - double? itemHeight = null, double? itemWidth = null, Thickness? itemSpacing = null, IMauiContext? mauiContext = null) + _innerCollectionChangedHandler = InnerCollectionChanged; + if (itemsSource is INotifyCollectionChanged notifyCollectionChanged) { - _itemsSource = itemsSource; - _itemTemplate = itemTemplate; - _container = container; - _mauiContext = mauiContext; - - if (itemHeight.HasValue) - { - _itemHeight = itemHeight.Value; - } - - if (itemWidth.HasValue) - { - _itemWidth = itemWidth.Value; - } + _proxy.Subscribe(notifyCollectionChanged, _innerCollectionChangedHandler); + } - if (itemSpacing.HasValue) - { - _itemSpacing = itemSpacing.Value; - } + CollectionChanged += TemplateCollectionChanged; + } - for (int index = 0; index < itemsSource.Count; index++) - { - // We're using this as a source for a ListViewBase, and we need INCC to work. So ListViewBase is going - // to iterate over the entire source list right off the bat, no matter what we do. Creating one - // ItemTemplateContext per item in the collection is unavoidable. Luckily, ITC is pretty cheap. - Add(new ItemTemplateContext2(itemTemplate, itemsSource[index]!, container, _itemHeight, _itemWidth, _itemSpacing, - false, false, _mauiContext)); - } + /// + /// Unsubscribes from collection change events. Must be called when replacing + /// the items source or when the handler disconnects. + /// + internal void CleanUp() + { + CollectionChanged -= TemplateCollectionChanged; + _proxy.Unsubscribe(); + } - _collectionChanged = InnerCollectionChanged; - if (itemsSource is INotifyCollectionChanged notifyCollectionChanged) - { - _proxy.Subscribe(notifyCollectionChanged, _collectionChanged); - } + /// + /// Populates the collection with initial entries + /// for each item in the source list. + /// + void PopulateInitialItems(IList itemsSource, DataTemplate itemTemplate, BindableObject container) + { + for (int index = 0; index < itemsSource.Count; index++) + { + var item = itemsSource[index]; + if (item is null) + continue; - CollectionChanged += TemplateCollectionChanged; + Add(new ItemTemplateContext2(itemTemplate, item, container, _itemHeight, _itemWidth, _itemSpacing, + false, false, _mauiContext)); } + } - /// - /// Unsubscribes from collection change events. Must be called when replacing - /// the items source or when the handler disconnects. - /// - public void CleanUp() + void TemplateCollectionChanged(object? sender, NotifyCollectionChangedEventArgs args) + { + if (!_innerCollectionChange) { - CollectionChanged -= TemplateCollectionChanged; - _proxy.Unsubscribe(); - } + // When the template collection changes not as result of an inner collection change. + // The only time this happens is during a drag/drop item reorder (CanReorderItems). + // The ListView/GridView has notified us now we need to move those changes into the source. + // One might think it would be a "Move" event but it is actually a "Remove" followed by "Add". + _observeChanges = false; - void TemplateCollectionChanged(object? sender, NotifyCollectionChangedEventArgs args) - { - if (!_innerCollectionChange) + switch (args.Action) { - // When the template collection changes not as result of an inner collection change. - // The only time this happens is during a drag/drop item reorder (CanReorderItems). - // The ListView/GridView has notified us now we need to move those changes into the source. - // One might think it would be a "Move" event but it is actually a "Remove" followed by "Add". - _observeChanges = false; - - switch (args.Action) - { - case NotifyCollectionChangedAction.Add: - AddToSource(args); - break; - case NotifyCollectionChangedAction.Remove: - RemoveFromSource(args); - break; - default: - break; - } - - _observeChanges = true; + case NotifyCollectionChangedAction.Add: + AddToSource(args); + break; + case NotifyCollectionChangedAction.Remove: + RemoveFromSource(args); + break; + default: + break; } + + _observeChanges = true; } + } - void AddToSource(NotifyCollectionChangedEventArgs args) + void AddToSource(NotifyCollectionChangedEventArgs args) + { + if (args.NewItems is null) { - if (args.NewItems is null) - { - return; - } + return; + } - var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : IndexOf((ItemTemplateContext2)args.NewItems[0]!); + var firstItem = args.NewItems[0] as ItemTemplateContext2; + var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : (firstItem is not null ? IndexOf(firstItem) : -1); - var count = args.NewItems.Count; + var count = args.NewItems.Count; - for (int index = 0; index < count; index++) - { - var newItem = (ItemTemplateContext2?)args.NewItems[index]; - if (newItem != null) - _itemsSource.Insert(startIndex, newItem.Item); - } + for (int index = 0; index < count; index++) + { + var newItem = args.NewItems[index] as ItemTemplateContext2; + if (newItem is not null) + _itemsSource.Insert(startIndex, newItem.Item); } + } - void RemoveFromSource(NotifyCollectionChangedEventArgs args) + void RemoveFromSource(NotifyCollectionChangedEventArgs args) + { + if (args.OldItems is null) { - if (args.OldItems is null) - { - return; - } + return; + } - var startIndex = args.OldStartingIndex; + var startIndex = args.OldStartingIndex; - if (startIndex < 0) - { - // INCC implementation isn't giving us enough information to know where the removed items were in the - return; - } + if (startIndex < 0) + { + // INCC implementation isn't giving us enough information to know where the removed items were in the + return; + } - var count = args.OldItems.Count; + var count = args.OldItems.Count; - for (int index = startIndex + count - 1; index >= startIndex; index--) - { - _itemsSource.RemoveAt(index); - } + for (int index = startIndex + count - 1; index >= startIndex; index--) + { + _itemsSource.RemoveAt(index); } + } - void InnerCollectionChanged(object? sender, NotifyCollectionChangedEventArgs args) + void InnerCollectionChanged(object? sender, NotifyCollectionChangedEventArgs args) + { + if (!_observeChanges) { - if (!_observeChanges) - { - return; - } - - _container.Dispatcher.DispatchIfRequired(() => InnerCollectionChanged(args)); + return; } - void InnerCollectionChanged(NotifyCollectionChangedEventArgs args) + _container.Dispatcher.DispatchIfRequired(() => InnerCollectionChanged(args)); + } + + void InnerCollectionChanged(NotifyCollectionChangedEventArgs args) + { + _innerCollectionChange = true; + switch (args.Action) { - _innerCollectionChange = true; - switch (args.Action) - { - case NotifyCollectionChangedAction.Add: - Add(args); - break; - case NotifyCollectionChangedAction.Move: - Move(args); - break; - case NotifyCollectionChangedAction.Remove: - Remove(args); - break; - case NotifyCollectionChangedAction.Replace: - Replace(args); - break; - case NotifyCollectionChangedAction.Reset: - Reset(); - break; - default: - throw new ArgumentOutOfRangeException(); - } - _innerCollectionChange = false; + case NotifyCollectionChangedAction.Add: + Add(args); + break; + case NotifyCollectionChangedAction.Move: + Move(args); + break; + case NotifyCollectionChangedAction.Remove: + Remove(args); + break; + case NotifyCollectionChangedAction.Replace: + Replace(args); + break; + case NotifyCollectionChangedAction.Reset: + Reset(); + break; + default: + throw new ArgumentOutOfRangeException(); } + _innerCollectionChange = false; + } - void Add(NotifyCollectionChangedEventArgs args) + void Add(NotifyCollectionChangedEventArgs args) + { + if (args.NewItems is null) { - if (args.NewItems is null) - { - return; - } + return; + } - var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _itemsSource.IndexOf(args.NewItems[0]); + var startIndex = args.NewStartingIndex > -1 ? args.NewStartingIndex : _itemsSource.IndexOf(args.NewItems[0]); - var count = args.NewItems.Count; + var count = args.NewItems.Count; - for (int index = 0; index < count; index++) - { - Insert(startIndex, new ItemTemplateContext2(_itemTemplate, args.NewItems[index]!, _container, _itemHeight, _itemWidth, _itemSpacing, - false, false, _mauiContext)); - } - } - - void Move(NotifyCollectionChangedEventArgs args) + for (int index = 0; index < count; index++) { - if (args.NewItems is null) - { - return; - } + var item = args.NewItems[index]; + if (item is null) + continue; - var count = args.NewItems.Count; - if (args.OldStartingIndex > args.NewStartingIndex) - { - for (int index = 0; index < count; index++) - { - Move(args.OldStartingIndex + index, args.NewStartingIndex + index); - } + Insert(startIndex, new ItemTemplateContext2(_itemTemplate, item, _container, _itemHeight, _itemWidth, _itemSpacing, + false, false, _mauiContext)); + } + } - return; - } + void Move(NotifyCollectionChangedEventArgs args) + { + if (args.NewItems is null) + { + return; + } - for (int index = count - 1; index >= 0; index--) + var count = args.NewItems.Count; + if (args.OldStartingIndex > args.NewStartingIndex) + { + for (int index = 0; index < count; index++) { Move(args.OldStartingIndex + index, args.NewStartingIndex + index); } + + return; } - void Remove(NotifyCollectionChangedEventArgs args) + for (int index = count - 1; index >= 0; index--) { - if (args.OldItems is null) - { - return; - } + Move(args.OldStartingIndex + index, args.NewStartingIndex + index); + } + } - var startIndex = args.OldStartingIndex; + void Remove(NotifyCollectionChangedEventArgs args) + { + if (args.OldItems is null) + { + return; + } - if (startIndex < 0) - { - // INCC implementation isn't giving us enough information to know where the removed items were in the - // collection. So the best we can do is a full Reset. - Reset(); - return; - } + var startIndex = args.OldStartingIndex; - var count = args.OldItems.Count; + if (startIndex < 0) + { + // INCC implementation isn't giving us enough information to know where the removed items were in the + // collection. So the best we can do is a full Reset. + Reset(); + return; + } - for (int index = startIndex + count - 1; index >= startIndex; index--) - { - RemoveAt(index); - } + var count = args.OldItems.Count; + + for (int index = startIndex + count - 1; index >= startIndex; index--) + { + RemoveAt(index); } + } - void Replace(NotifyCollectionChangedEventArgs args) + void Replace(NotifyCollectionChangedEventArgs args) + { + if (args.OldItems == null || args.NewItems == null) { - if (args.OldItems == null || args.NewItems == null) - { - return; - } + return; + } - var newItemCount = args.NewItems.Count; + var newItemCount = args.NewItems.Count; - if (newItemCount == args.OldItems.Count) - { - for (int index = 0; index < newItemCount; index++) - { - var itemIndex = args.OldStartingIndex + index; - var oldItem = this[itemIndex]; - var newItem = new ItemTemplateContext2(_itemTemplate, args.NewItems[index]!, _container, _itemHeight, _itemWidth, _itemSpacing, - false, false, _mauiContext); - Items[itemIndex] = newItem; - var update = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItem, oldItem, itemIndex); - OnCollectionChanged(update); - } - } - else + if (newItemCount == args.OldItems.Count) + { + for (int index = 0; index < newItemCount; index++) { - // If we're replacing one set with an equal size set, we can do a soft reset; if not, we have to completely - // rebuild the collection - Reset(); + var item = args.NewItems[index]; + if (item is null) + continue; + + var itemIndex = args.OldStartingIndex + index; + var oldItem = this[itemIndex]; + var newItem = new ItemTemplateContext2(_itemTemplate, item, _container, _itemHeight, _itemWidth, _itemSpacing, + false, false, _mauiContext); + Items[itemIndex] = newItem; + var update = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, newItem, oldItem, itemIndex); + OnCollectionChanged(update); } } - - void Reset() + else { - Items.Clear(); - foreach (var item in _itemsSource) - { - Items.Add(new ItemTemplateContext2(_itemTemplate, item, _container, _itemHeight, _itemWidth, _itemSpacing, - false, false, _mauiContext)); - } + // If we're replacing one set with an equal size set, we can do a soft reset; if not, we have to completely + // rebuild the collection + Reset(); + } + } - var reset = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); - OnCollectionChanged(reset); + void Reset() + { + Items.Clear(); + foreach (var item in _itemsSource) + { + Items.Add(new ItemTemplateContext2(_itemTemplate, item, _container, _itemHeight, _itemWidth, _itemSpacing, + false, false, _mauiContext)); } + + var reset = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); + OnCollectionChanged(reset); } } \ No newline at end of file diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/TemplatedItemSourceFactory2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/TemplatedItemSourceFactory2.cs index 45808ae9e4f7..8eed1c67f63a 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/TemplatedItemSourceFactory2.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/TemplatedItemSourceFactory2.cs @@ -1,40 +1,38 @@ using System.Collections; using System.Collections.Specialized; -namespace Microsoft.Maui.Controls.Handlers.Items2 +namespace Microsoft.Maui.Controls.Handlers.Items2; +/// +/// Factory that creates the appropriate templated item source collection +/// based on the capabilities of the items source (observable, list, or enumerable). +/// +internal static class TemplatedItemSourceFactory2 { /// - /// Factory that creates the appropriate templated item source collection - /// based on the capabilities of the items source (observable, list, or enumerable). + /// Creates a templated item source for the given items source, choosing the best + /// collection type: observable (for INCC sources), indexed list, or enumerable fallback. /// - internal static class TemplatedItemSourceFactory2 + internal static object Create(IEnumerable itemsSource, DataTemplate itemTemplate, BindableObject container, + double? itemHeight = null, double? itemWidth = null, Thickness? itemSpacing = null, IMauiContext? mauiContext = null) { - /// - /// Creates a templated item source for the given items source, choosing the best - /// collection type: observable (for INCC sources), indexed list, or enumerable fallback. - /// - internal static object Create(IEnumerable itemsSource, DataTemplate itemTemplate, BindableObject container, - double? itemHeight = null, double? itemWidth = null, Thickness? itemSpacing = null, IMauiContext? mauiContext = null) + switch (itemsSource) { - switch (itemsSource) - { - case IList observable when itemsSource is INotifyCollectionChanged: - return new ObservableItemTemplateCollection2(observable, itemTemplate, container, itemHeight, itemWidth, itemSpacing, mauiContext); - case IList list: - return new ItemTemplateContextList2(list, itemTemplate, container, itemHeight, itemWidth, itemSpacing, mauiContext); - } - - return new ItemTemplateContextEnumerable2(itemsSource, itemTemplate, container, itemHeight, itemWidth, itemSpacing, mauiContext); + case IList observable when itemsSource is INotifyCollectionChanged: + return new ObservableItemTemplateCollection2(observable, itemTemplate, container, itemHeight, itemWidth, itemSpacing, mauiContext); + case IList list: + return new ItemTemplateContextList2(list, itemTemplate, container, itemHeight, itemWidth, itemSpacing, mauiContext); } - /// - /// Creates a grouped templated item source that flattens grouped data into a single - /// list with header and footer contexts for each group. - /// - internal static object CreateGrouped(IEnumerable itemsSource, DataTemplate itemTemplate, - DataTemplate groupHeaderTemplate, DataTemplate groupFooterTemplate, BindableObject container, IMauiContext? mauiContext = null) - { - return new GroupedItemTemplateCollection2(itemsSource, itemTemplate, groupHeaderTemplate, groupFooterTemplate, container, mauiContext); - } + return new ItemTemplateContextEnumerable2(itemsSource, itemTemplate, container, itemHeight, itemWidth, itemSpacing, mauiContext); + } + + /// + /// Creates a grouped templated item source that flattens grouped data into a single + /// list with header and footer contexts for each group. + /// + internal static object CreateGrouped(IEnumerable itemsSource, DataTemplate itemTemplate, + DataTemplate groupHeaderTemplate, DataTemplate groupFooterTemplate, BindableObject container, IMauiContext? mauiContext = null) + { + return new GroupedItemTemplateCollection2(itemsSource, itemTemplate, groupHeaderTemplate, groupFooterTemplate, container, mauiContext); } } diff --git a/src/Controls/src/Core/Platform/Windows/CollectionView/ItemsViewStyles.xaml b/src/Controls/src/Core/Platform/Windows/CollectionView/ItemsViewStyles.xaml index 440b5c88a868..daab3547b7fa 100644 --- a/src/Controls/src/Core/Platform/Windows/CollectionView/ItemsViewStyles.xaml +++ b/src/Controls/src/Core/Platform/Windows/CollectionView/ItemsViewStyles.xaml @@ -1,52 +1,55 @@ + xmlns:items="using:Microsoft.Maui.Controls.Handlers.Items2"> + + + - - - - + - + - + - - - - - + + - - + + + Padding="{TemplateBinding Padding}"/> @@ -101,68 +108,86 @@ get to a point where we don't have to support these earlier versions, we can replace this style with just the template. (See also FormsGridView.cs) --> - - @@ -207,26 +251,37 @@ + TargetType="items2:MauiItemsView"> - - - - + + + - - - - + + + + + - + From e282e84cbfcd7e22c42d7b21f27c45f550847270 Mon Sep 17 00:00:00 2001 From: SuthiYuvaraj <92777079+SuthiYuvaraj@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:56:08 +0530 Subject: [PATCH 042/109] fix-review changes --- .../Windows/GroupedItemTemplateCollection2.cs | 4 ++++ .../Items2/Windows/ItemTemplateContext2.cs | 14 ++++---------- .../Windows/ItemTemplateContextEnumerable2.cs | 2 +- .../Items2/Windows/ItemTemplateContextList2.cs | 2 ++ .../Core/Handlers/Items2/Windows/MauiItemsView.cs | 2 +- .../Windows/ObservableItemTemplateCollection2.cs | 6 ++---- .../Windows/CollectionView/ItemsViewStyles.xaml | 4 ++-- 7 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs index d600d31d235f..cffdd9f554bf 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/GroupedItemTemplateCollection2.cs @@ -295,6 +295,10 @@ void HandleGroupItemsReplace(NotifyCollectionChangedEventArgs e, int flatIndex) for (int i = 0; i < e.NewItems.Count; i++) { oldItems.Add(Items[replaceIndex + i]); + var item = e.NewItems[i]; + if(item is null) + continue; + var newItem = CreateItemContext(e.NewItems[i]!); newItems.Add(newItem); Items[replaceIndex + i] = newItem; diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs index 29fa6376655f..0c86fa10c888 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContext2.cs @@ -37,7 +37,7 @@ internal class ItemTemplateContext2 /// Whether this context represents a group footer. public bool IsFooter { get; } - ItemTemplateContext2(DataTemplate mauiDataTemplate, object item, BindableObject container, + public ItemTemplateContext2(DataTemplate mauiDataTemplate, object item, BindableObject container, double? height = null, double? width = null, Thickness? itemSpacing = null, bool isHeader = false, bool isFooter = false, IMauiContext? mauiContext = null) { @@ -45,15 +45,9 @@ internal class ItemTemplateContext2 Item = item; _container = new(container); MauiContext = mauiContext; - - if (height.HasValue) - ItemHeight = height.Value; - - if (width.HasValue) - ItemWidth = width.Value; - - if (itemSpacing.HasValue) - ItemSpacing = itemSpacing.Value; + ItemHeight = height ?? 0; + ItemWidth = width ?? 0; + ItemSpacing = itemSpacing ?? default; IsHeader = isHeader; IsFooter = isFooter; diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextEnumerable2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextEnumerable2.cs index a3d30a498455..c01b6484a5c9 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextEnumerable2.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextEnumerable2.cs @@ -16,7 +16,7 @@ internal class ItemTemplateContextEnumerable2 : IEnumerable readonly double _itemWidth; readonly Thickness _itemSpacing; - ItemTemplateContextEnumerable2(IEnumerable itemsSource, DataTemplate itemTemplate, BindableObject container, + public ItemTemplateContextEnumerable2(IEnumerable itemsSource, DataTemplate itemTemplate, BindableObject container, double? itemHeight = null, double? itemWidth = null, Thickness? itemSpacing = null, IMauiContext? mauiContext = null) { _itemsSource = itemsSource; diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextList2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextList2.cs index beeaf251a3d2..92e8e26aa933 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextList2.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ItemTemplateContextList2.cs @@ -65,6 +65,7 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } +#nullable disable internal class ItemTemplateContextListEnumerator2 : IEnumerator { public ItemTemplateContext2 Current { get; private set; } @@ -100,4 +101,5 @@ public void Reset() _currentIndex = -1; } } +#nullable enable } \ No newline at end of file diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs b/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs index e44ce991bd64..037624ba0352 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/MauiItemsView.cs @@ -30,7 +30,7 @@ internal partial class MauiItemsView : UI.Xaml.Controls.ItemsView, IEmptyView bool _isHorizontalLayout; WScrollView? _scrollView; - MauiItemsView() + public MauiItemsView() { Template = (WControlTemplate)WApp.Current.Resources["MauiItemsViewTemplate"]; } diff --git a/src/Controls/src/Core/Handlers/Items2/Windows/ObservableItemTemplateCollection2.cs b/src/Controls/src/Core/Handlers/Items2/Windows/ObservableItemTemplateCollection2.cs index 0213db2a6274..0be75da801e5 100644 --- a/src/Controls/src/Core/Handlers/Items2/Windows/ObservableItemTemplateCollection2.cs +++ b/src/Controls/src/Core/Handlers/Items2/Windows/ObservableItemTemplateCollection2.cs @@ -18,7 +18,6 @@ internal class ObservableItemTemplateCollection2 : ObservableCollection _proxy.Unsubscribe(); - ObservableItemTemplateCollection2(IList itemsSource, DataTemplate itemTemplate, BindableObject container, + public ObservableItemTemplateCollection2(IList itemsSource, DataTemplate itemTemplate, BindableObject container, double? itemHeight = null, double? itemWidth = null, Thickness? itemSpacing = null, IMauiContext? mauiContext = null) { _itemsSource = itemsSource; @@ -46,10 +45,9 @@ internal class ObservableItemTemplateCollection2 : ObservableCollection void SubscribeToSourceChanges(IList itemsSource) { - _innerCollectionChangedHandler = InnerCollectionChanged; if (itemsSource is INotifyCollectionChanged notifyCollectionChanged) { - _proxy.Subscribe(notifyCollectionChanged, _innerCollectionChangedHandler); + _proxy.Subscribe(notifyCollectionChanged, InnerCollectionChanged); } CollectionChanged += TemplateCollectionChanged; diff --git a/src/Controls/src/Core/Platform/Windows/CollectionView/ItemsViewStyles.xaml b/src/Controls/src/Core/Platform/Windows/CollectionView/ItemsViewStyles.xaml index daab3547b7fa..53575323a08f 100644 --- a/src/Controls/src/Core/Platform/Windows/CollectionView/ItemsViewStyles.xaml +++ b/src/Controls/src/Core/Platform/Windows/CollectionView/ItemsViewStyles.xaml @@ -3,7 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:Microsoft.Maui.Controls.Platform" xmlns:controls="using:Microsoft.UI.Xaml.Controls" - xmlns:items="using:Microsoft.Maui.Controls.Handlers.Items2"> + xmlns:items2="using:Microsoft.Maui.Controls.Handlers.Items2"> @@ -289,7 +289,7 @@ -