diff --git a/eng/cake/dotnet.cake b/eng/cake/dotnet.cake
index 084a098460c9..0a647032ea06 100644
--- a/eng/cake/dotnet.cake
+++ b/eng/cake/dotnet.cake
@@ -224,6 +224,13 @@ Task("uitests-apphost")
Information("Building with Material3 enabled");
properties.Add("UseMaterial3", "true");
}
+
+ var useWindowsCV1 = Argument("usewindowscv1", false);
+ if (useWindowsCV1)
+ {
+ Information("Building with UseWindowsCollectionView2Handler=false (CV1)");
+ properties.Add("UseWindowsCollectionView2Handler", "false");
+ }
if (useNuget)
{
diff --git a/eng/devices/windows.cake b/eng/devices/windows.cake
index 20c66c4e434f..2f5665bd1cf3 100644
--- a/eng/devices/windows.cake
+++ b/eng/devices/windows.cake
@@ -678,6 +678,14 @@ Task("uitest")
SetEnvironmentVariable("WINDOWS_APP_PATH", TEST_APP);
SetEnvironmentVariable("APPIUM_LOG_FILE", $"{BINLOG_DIR}/appium_windows_{name}.log");
+ // Set TEST_CONFIGURATION_ARGS for handler selection and snapshot folder determination
+ var TEST_CONFIGURATION_ARGS = Argument("TEST_CONFIGURATION_ARGS", EnvironmentVariable("TEST_CONFIGURATION_ARGS") ?? "");
+ if (!string.IsNullOrEmpty(TEST_CONFIGURATION_ARGS))
+ {
+ Information("TEST_CONFIGURATION_ARGS: {0}", TEST_CONFIGURATION_ARGS);
+ SetEnvironmentVariable("TEST_CONFIGURATION_ARGS", TEST_CONFIGURATION_ARGS);
+ }
+
Information("Run UITests project {0}",PROJECT.FullPath);
RunTestWithLocalDotNet(PROJECT.FullPath, CONFIGURATION, localToolPath, noBuild: true, resultsFileNameWithoutExtension: $"{name}-{CONFIGURATION}-windows");
});
diff --git a/eng/pipelines/common/ui-tests-build-sample.yml b/eng/pipelines/common/ui-tests-build-sample.yml
index 06e46751cf36..7784da6a4a9c 100644
--- a/eng/pipelines/common/ui-tests-build-sample.yml
+++ b/eng/pipelines/common/ui-tests-build-sample.yml
@@ -12,6 +12,7 @@ parameters:
headless: true
runtimeVariant: 'CoreCLR' #Mono, CoreCLR, NativeAOT
useMaterial3: false # NEW PARAMETER - Enable Material3 build
+ useWindowsCV1: false # NEW PARAMETER - Enable WindowsCollectionView1Handler testing (default false)
steps:
- ${{ if eq(parameters.platform, 'ios')}}:
@@ -75,6 +76,12 @@ steps:
Write-Host "Material3 is only supported for Android platform, ignoring useMaterial3 parameter"
}
+ # Add UseWindowsCV1 build argument if enabled
+ if (("${{ parameters.useWindowsCV1 }}" -eq "true") -and ("${{ parameters.platform }}" -eq "windows")) {
+ $buildCommand += " --usewindowscv1=true"
+ Write-Host "Building with UseWindowsCollectionView2Handler=false (CV1)"
+ }
+
Invoke-Expression $buildCommand
displayName: 'Build the samples'
@@ -107,9 +114,13 @@ steps:
artifact: ui-tests-samples-material3-coreclr
- publish: $(System.DefaultWorkingDirectory)/artifacts/bin
- condition: and(eq('${{ parameters.platform }}' , 'windows'), succeeded())
+ condition: and(eq('${{ parameters.platform }}' , 'windows'), eq('${{ parameters.useWindowsCV1 }}', 'false'), succeeded())
artifact: ui-tests-samples-windows
+- publish: $(System.DefaultWorkingDirectory)/artifacts/bin
+ condition: and(eq('${{ parameters.platform }}' , 'windows'), eq('${{ parameters.useWindowsCV1 }}', 'true'), succeeded())
+ artifact: ui-tests-samples-windows-cv1
+
- publish: $(System.DefaultWorkingDirectory)/artifacts/bin
condition: and(ne('${{ parameters.platform }}' , 'windows'), ne('${{ parameters.runtimeVariant }}' , 'NativeAOT'), ne('${{ parameters.runtimeVariant }}' , 'CoreCLR'), eq('${{ parameters.useMaterial3 }}', 'false'), failed())
artifact: ui-tests-samples_failed_$(System.JobAttempt)
@@ -128,8 +139,12 @@ steps:
artifact: ui-tests-samples-material3_failed_$(System.JobAttempt)
- publish: $(System.DefaultWorkingDirectory)/artifacts/bin
- condition: and(ne('${{ parameters.platform }}' , 'windows'), eq('${{ parameters.runtimeVariant }}' , 'CoreCLR'), eq('${{ parameters.useMaterial3 }}', 'true'), failed())
- artifact: ui-tests-samples-material3-coreclr_failed_$(System.JobAttempt)
+ condition: and(eq('${{ parameters.platform }}' , 'windows'), eq('${{ parameters.useWindowsCV1 }}', 'false'), failed())
+ artifact: ui-tests-samples-windows_failed_$(System.JobAttempt)
+
+- publish: $(System.DefaultWorkingDirectory)/artifacts/bin
+ condition: and(eq('${{ parameters.platform }}' , 'windows'), eq('${{ parameters.useWindowsCV1 }}', 'true'), failed())
+ artifact: ui-tests-samples-windows-cv1_failed_$(System.JobAttempt)
- publish: $(System.DefaultWorkingDirectory)/artifacts/bin
condition: and(eq('${{ parameters.platform }}' , 'windows'), failed())
diff --git a/eng/pipelines/common/ui-tests-steps.yml b/eng/pipelines/common/ui-tests-steps.yml
index 9176b6eb3f07..88728446cf55 100644
--- a/eng/pipelines/common/ui-tests-steps.yml
+++ b/eng/pipelines/common/ui-tests-steps.yml
@@ -10,6 +10,7 @@ parameters:
configuration: "Release"
runtimeVariant: "CoreCLR"
useMaterial3: false # NEW PARAMETER - Enable Material3 build
+ useWindowsCV1: false # NEW PARAMETER - Enable WindowsCollectionView1Handler test configuration
testFilter: ''
headless: true
testConfigurationArgs: ''
@@ -43,10 +44,15 @@ steps:
artifact: ui-tests-samples-material3-coreclr
- task: DownloadPipelineArtifact@2
- condition: eq('${{ parameters.platform }}' , 'windows')
+ condition: and(eq('${{ parameters.platform }}' , 'windows'), eq('${{ parameters.useWindowsCV1 }}', 'false'))
inputs:
artifact: ui-tests-samples-windows
+- task: DownloadPipelineArtifact@2
+ condition: and(eq('${{ parameters.platform }}' , 'windows'), eq('${{ parameters.useWindowsCV1 }}', 'true'))
+ inputs:
+ artifact: ui-tests-samples-windows-cv1
+
- ${{ if eq(parameters.platform, 'ios')}}:
- bash: |
chmod +x $(System.DefaultWorkingDirectory)/eng/scripts/clean-bot.sh
diff --git a/eng/pipelines/common/ui-tests.yml b/eng/pipelines/common/ui-tests.yml
index 1f786114fe14..fae087e15305 100644
--- a/eng/pipelines/common/ui-tests.yml
+++ b/eng/pipelines/common/ui-tests.yml
@@ -121,6 +121,25 @@ stages:
runtimeVariant: "CoreCLR"
skipProvisioning: ${{ parameters.skipProvisioning }}
+ # Windows CV1 Build Stage - Separate build with UseWindowsCollectionView2Handler=false
+ - stage: build_ui_tests_windows_cv1
+ displayName: Build UITests Windows CV1 Sample App
+ dependsOn: []
+ jobs:
+ - job: build_ui_tests_windows_cv1
+ displayName: Build Sample App (Windows CV1)
+ workspace:
+ clean: all
+ pool: ${{ parameters.windowsBuildPool }}
+ variables:
+ APPIUM_HOME: $(System.DefaultWorkingDirectory)/.appium/
+ steps:
+ - template: ui-tests-build-sample.yml
+ parameters:
+ platform: windows
+ runtimeVariant: "CoreCLR"
+ useWindowsCV1: true
+ skipProvisioning: ${{ parameters.skipProvisioning }}
- stage: android_ui_tests_coreclr
displayName: Android UITests CoreClr
dependsOn: build_ui_tests_coreclr
@@ -425,6 +444,41 @@ stages:
platform: 'Windows'
artifactName: 'uitest-snapshot-results-windows-$(System.StageName)-$(System.JobName)-$(System.JobAttempt)'
+ # Windows CV1 UI Tests - Testing CollectionView with UseWindowsCollectionView2Handler=false
+ - stage: winui_ui_tests_cv1
+ displayName: WinUI UITests CV1
+ dependsOn: build_ui_tests_windows_cv1
+ jobs:
+ - ${{ each project in parameters.projects }}:
+ - ${{ if ne(project.winui, '') }}:
+ - job: CV1_winui_ui_tests_${{ project.name }}
+ timeoutInMinutes: ${{ parameters.timeoutInMinutes }}
+ workspace:
+ clean: all
+ displayName: ${{ coalesce(project.desc, project.name) }} (CV1)
+ pool: ${{ parameters.windowsPool }}
+ variables:
+ APPIUM_HOME: $(System.DefaultWorkingDirectory)\.appium\
+ steps:
+ - template: ui-tests-steps.yml
+ parameters:
+ platform: windows
+ version: "10.0.19041.0"
+ device: windows10
+ path: ${{ project.winui }}
+ app: ${{ project.app }}
+ provisionatorChannel: ${{ parameters.provisionatorChannel }}
+ runtimeVariant: "CoreCLR"
+ useWindowsCV1: true
+ testFilter: "CollectionView"
+ testConfigurationArgs: "UseWindowsCV1:true"
+ skipProvisioning: ${{ parameters.skipProvisioning }}
+
+ # Collect and publish Windows CV1 snapshot diffs
+ - template: ui-tests-collect-snapshot-diffs.yml
+ parameters:
+ platform: 'Windows CV1'
+ artifactName: 'uitest-snapshot-results-windows-cv1-$(System.StageName)-$(System.JobName)-$(System.JobAttempt)'
- stage: maccatalyst_ui_tests_coreclr
displayName: MacCatalyst UITests CoreCLR
dependsOn: build_ui_tests_coreclr
diff --git a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets
index 1bae631fbeb1..3165d6cd1a8b 100644
--- a/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets
+++ b/src/Controls/src/Build.Tasks/nuget/buildTransitive/netstandard2.0/Microsoft.Maui.Controls.targets
@@ -380,6 +380,10 @@
Condition="'$(UseMaterial3)' != ''"
Value="$(UseMaterial3)"
Trim="true" />
+
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..8d7d07fe90a5
--- /dev/null
+++ b/src/Controls/src/Core/Handlers/Items/ItemsViewExtensions.Windows.cs
@@ -0,0 +1,107 @@
+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;
+using WASDKScrollingScrollBarVisibility = Microsoft.UI.Xaml.Controls.ScrollingScrollBarVisibility;
+
+namespace Microsoft.Maui.Controls.Handlers.Items
+{
+ internal static class ItemsViewExtensions
+ {
+ internal static FrameworkElement RealizeEmptyViewTemplate(object? bindingContext, DataTemplate? emptyViewTemplate, IMauiContext mauiContext, ref View? mauiEmptyView, BindableObject? container = null)
+ {
+ if (emptyViewTemplate is null)
+ {
+ return CreateDefaultEmptyViewTextBlock(bindingContext);
+ }
+
+ // Pass the owning ItemsView as the container so DataTemplateSelector
+ // implementations can inspect it (parity with iOS/Android/Tizen).
+ var template = emptyViewTemplate.SelectDataTemplate(bindingContext, container);
+
+ var view = template.CreateContent() as View;
+ if (view is not null)
+ {
+ view.BindingContext = 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));
+
+ 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, BindableObject? container = null)
+ {
+ if (template is null)
+ {
+ return new TextBlock
+ {
+ Text = bindingContext?.ToString() ?? string.Empty,
+ Margin = new Microsoft.UI.Xaml.Thickness(0, 5, 0, 5)
+ };
+ }
+
+ // Pass the owning ItemsView as the container so DataTemplateSelector
+ // implementations can inspect it (parity with iOS/Android/Tizen).
+ var dataTemplate = template.SelectDataTemplate(bindingContext, container);
+ 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");
+ }
+
+ ///
+ /// Applies MAUI margin and layout options to a WinUI element that hosts a MAUI View
+ /// (e.g., CollectionView header/footer placed in a WinUI ContentControl).
+ /// The cross-platform layout system never applies these because the element
+ /// is not inside a MAUI layout.
+ ///
+ internal static void ApplyMauiLayoutProperties(View? mauiView, FrameworkElement? platformElement)
+ {
+ if (mauiView is null || platformElement is null)
+ {
+ return;
+ }
+
+ // Apply MAUI margin as WinUI margin
+ var margin = mauiView.Margin;
+ platformElement.Margin = new Microsoft.UI.Xaml.Thickness(margin.Left, margin.Top, margin.Right, margin.Bottom);
+
+ // Map HorizontalOptions/VerticalOptions to WinUI alignment
+ platformElement.UpdateHorizontalOptions(mauiView);
+ platformElement.UpdateVerticalOptions(mauiView);
+ }
+ }
+}
diff --git a/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.Windows.cs b/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.Windows.cs
index e8fc4ae79906..281436097c50 100644
--- a/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.Windows.cs
+++ b/src/Controls/src/Core/Handlers/Items/ItemsViewHandler.Windows.cs
@@ -382,11 +382,11 @@ protected virtual void UpdateEmptyView()
if (emptyViewTemplate is DataTemplate template)
{
- _emptyView = RealizeEmptyViewTemplate(emptyView, template);
+ _emptyView = ItemsViewExtensions.RealizeEmptyViewTemplate(emptyView, template, MauiContext, ref _formsEmptyView, Element);
}
else if (emptyView is View view)
{
- _emptyView = RealizeEmptyView(view);
+ _emptyView = ItemsViewExtensions.RealizeEmptyView(view, MauiContext, ref _formsEmptyView);
}
else
{
@@ -535,24 +535,6 @@ void ScrollViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
HandleScroll(_scrollViewer);
}
- FrameworkElement RealizeEmptyViewTemplate(object bindingContext, DataTemplate emptyViewTemplate)
- {
- var template = emptyViewTemplate.SelectDataTemplate(bindingContext, ItemsView);
- var templatedElement = template.CreateContent() as View;
- templatedElement.BindingContext = bindingContext;
- return RealizeEmptyView(templatedElement);
- }
-
- 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/CollectionViewHandler2.Windows.cs b/src/Controls/src/Core/Handlers/Items2/CollectionViewHandler2.Windows.cs
new file mode 100644
index 000000000000..8e4b49e2dfdd
--- /dev/null
+++ b/src/Controls/src/Core/Handlers/Items2/CollectionViewHandler2.Windows.cs
@@ -0,0 +1,525 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Maui.Controls.Platform;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Data;
+using WItemsView = Microsoft.UI.Xaml.Controls.ItemsView;
+using WSolidColorBrush = Microsoft.UI.Xaml.Media.SolidColorBrush;
+using WThickness = Microsoft.UI.Xaml.Thickness;
+
+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(ReorderableItemsViewMapper)
+ {
+ [GroupableItemsView.IsGroupedProperty.PropertyName] = MapIsGrouped,
+ [GroupableItemsView.GroupHeaderTemplateProperty.PropertyName] = MapGroupHeaderTemplate,
+ [GroupableItemsView.GroupFooterTemplateProperty.PropertyName] = MapGroupFooterTemplate,
+ [SelectableItemsView.SelectedItemProperty.PropertyName] = MapSelectedItem,
+ [SelectableItemsView.SelectedItemsProperty.PropertyName] = MapSelectedItems,
+ [SelectableItemsView.SelectionModeProperty.PropertyName] = MapSelectionMode,
+ [StructuredItemsView.ItemSizingStrategyProperty.PropertyName] = MapItemSizingStrategy,
+
+ };
+}
+public partial class CollectionViewHandler2 : ReorderableItemsViewHandler2
+{
+ bool _ignorePlatformSelectionChange;
+ bool _selectionDirty;
+
+ // Cache for MeasureFirstItem optimization
+ global::Windows.Foundation.Size _firstItemMeasuredSize = global::Windows.Foundation.Size.Empty;
+
+ public static void MapIsGrouped(CollectionViewHandler2 handler, GroupableItemsView itemsView)
+ {
+ // When IsGrouped changes with GridItemsLayout, we need to recreate the layout
+ // because grouped grids use GroupableUniformGridLayout while ungrouped use UniformGridLayout
+ if (handler.Layout is GridItemsLayout)
+ {
+ handler.UpdateItemsLayout();
+ }
+
+ 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 MapItemSizingStrategy(CollectionViewHandler2 handler, ItemsView itemsView)
+ {
+ handler.InvalidateFirstItemSize();
+ handler.UpdateItemsSource();
+ }
+
+ public static void MapItemsSource(CollectionViewHandler2 handler, SelectableItemsView itemsView)
+ {
+ ItemsViewHandler2.MapItemsSource(handler, itemsView);
+ }
+
+ public static void MapSelectedItem(CollectionViewHandler2 handler, SelectableItemsView itemsView)
+ {
+ handler.UpdatePlatformSelection();
+ }
+
+ public static void MapSelectedItems(CollectionViewHandler2 handler, SelectableItemsView itemsView)
+ {
+ handler.UpdatePlatformSelection();
+ }
+
+ ///
+ /// Left margin applied to item content when in multi-select mode to avoid overlapping
+ /// the WinUI ItemContainer checkbox. Matches WinUI's ListViewItemMultiselectCheckBoxMargin.
+ ///
+ internal static readonly WThickness MultiSelectContentMargin = new WThickness(32, 0, 0, 0);
+ static readonly WThickness ZeroMargin = new WThickness(0);
+
+ public static void MapSelectionMode(CollectionViewHandler2 handler, SelectableItemsView itemsView)
+ {
+ handler.UpdatePlatformSelection();
+ handler.UpdateMultiSelectContentMargin();
+ // The margin change affects item sizes, so invalidate the MeasureFirstItem cache
+ // to avoid stale sizes being applied to items realized after the mode switch.
+ handler.InvalidateFirstItemSize();
+ }
+
+ ///
+ /// When in multi-select mode the WinUI ItemContainer renders a checkbox overlay
+ /// on the left. Add left margin to the content (ElementWrapper) so that user
+ /// content is not hidden behind the checkbox.
+ ///
+ void UpdateMultiSelectContentMargin()
+ {
+ if (PlatformView is null || ItemsView is null)
+ return;
+
+ var margin = ItemsView.SelectionMode == SelectionMode.Multiple
+ ? MultiSelectContentMargin
+ : ZeroMargin;
+
+ foreach (var container in PlatformView.GetChildren())
+ {
+ if (container?.Child is ElementWrapper wrapper && !wrapper.IsHeaderOrFooter)
+ {
+ wrapper.Margin = margin;
+ }
+ }
+ }
+
+ ///
+ /// 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);
+
+ 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 SelectionModeConverter(),
+ Mode = UI.Xaml.Data.BindingMode.TwoWay
+ });
+
+ PlatformView.SelectionChanged += PlatformSelectionChanged;
+ PlatformView.Loaded += OnPlatformViewLoaded;
+ }
+ }
+
+ void OnPlatformViewLoaded(object? sender, UI.Xaml.RoutedEventArgs e)
+ {
+ // Re-sync visual states to MAUI selection whenever the view re-enters the visual tree.
+ // This is the fix for the stale selection highlight on navigation back
+ if (_selectionDirty)
+ {
+ _selectionDirty = false;
+ UpdatePlatformSelection();
+ }
+ }
+
+ protected override void DisconnectHandler(WItemsView platformView)
+ {
+ if (platformView is null)
+ {
+ return;
+ }
+
+ platformView.SelectionChanged -= PlatformSelectionChanged;
+ platformView.Loaded -= OnPlatformViewLoaded;
+ platformView.ClearValue(WItemsView.SelectionModeProperty);
+
+ if (ItemsView is not null)
+ {
+ ItemsView.SelectionChanged -= VirtualSelectionChanged;
+ }
+
+ base.DisconnectHandler(platformView);
+ }
+
+ protected override void UpdateItemsSource()
+ {
+ _ignorePlatformSelectionChange = true;
+ try
+ {
+ base.UpdateItemsSource();
+ UpdatePlatformSelection();
+ }
+ finally
+ {
+ _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)
+ return;
+
+ UpdateVirtualSelection();
+ }
+
+ ///
+ /// Reads the current platform selection state and updates the virtual (MAUI) selection accordingly.
+ ///
+ void UpdateVirtualSelection()
+ {
+ if (_ignorePlatformSelectionChange || ItemsView is null || PlatformView is null)
+ {
+ return;
+ }
+
+ switch (PlatformView.SelectionMode)
+ {
+ case ItemsViewSelectionMode.Single:
+ UpdateVirtualSingleSelection();
+ break;
+ case ItemsViewSelectionMode.Multiple:
+ UpdateVirtualMultipleSelection();
+ break;
+ case ItemsViewSelectionMode.None:
+ default:
+ break;
+ }
+
+ UpdateVisualStates();
+ }
+
+ void UpdateVisualStates()
+ {
+ if (PlatformView is null || ItemsView is null)
+ return;
+
+ var transparent = new WSolidColorBrush(Microsoft.UI.Colors.Transparent);
+
+ foreach (var itemContainer in PlatformView.GetChildren())
+ {
+ if (itemContainer?.Child is ElementWrapper wrapper && wrapper.VirtualView is VisualElement visualElement)
+ {
+ var actualItem = visualElement.BindingContext;
+ bool isSelected = object.Equals(ItemsView.SelectedItem, actualItem) || ItemsView.SelectedItems.Contains(actualItem);
+ VisualStateManager.GoToState(visualElement, isSelected ? VisualStateManager.CommonStates.Selected : VisualStateManager.CommonStates.Normal);
+
+ // When the item template defines a "Selected" visual state, MAUI
+ // handles the selection appearance. Suppress the native WinUI
+ // selection border (PART_SelectionVisual) and selected background
+ // fill on that container so the two don't overlap. Items without
+ // a Selected visual state keep the default WinUI selection visuals.
+ if (visualElement.HasVisualState(VisualStateManager.CommonStates.Selected))
+ {
+ itemContainer.Resources["ItemContainerSelectedBorderBrush"] = transparent;
+ itemContainer.Resources["ItemContainerSelectedPointerOverBorderBrush"] = transparent;
+ itemContainer.Resources["ItemContainerSelectedPressedBorderBrush"] = transparent;
+ itemContainer.Resources["ItemContainerSelectionVisualBackground"] = transparent;
+ itemContainer.Resources["ItemContainerSelectionVisualPointerOverBackground"] = transparent;
+ itemContainer.Resources["ItemContainerSelectionVisualPressedBackground"] = transparent;
+ itemContainer.Resources["ItemContainerSelectedInnerBorderBrush"] = transparent;
+ // Also suppress the selected background fill so MAUI's VSM
+ // is the only thing painting the selection appearance.
+ itemContainer.Resources["ItemContainerSelectedBackground"] = transparent;
+ itemContainer.Resources["ItemContainerSelectedPointerOverBackground"] = transparent;
+ itemContainer.Resources["ItemContainerSelectedPressedBackground"] = transparent;
+ }
+ }
+ }
+ }
+
+ void UpdateVirtualSingleSelection()
+ {
+ if (PlatformView is null || ItemsView is null)
+ return;
+
+ var selectedItem = PlatformView.SelectedItem is ItemTemplateContext2 itemPair
+ ? itemPair.Item
+ : PlatformView.SelectedItem;
+
+ ItemsView.SelectionChanged -= VirtualSelectionChanged;
+ ItemsView.SelectedItem = selectedItem;
+
+ ItemsView.SelectionChanged += VirtualSelectionChanged;
+ }
+
+ void UpdateVirtualMultipleSelection()
+ {
+ if (PlatformView is null || ItemsView is null)
+ return;
+
+ ItemsView.SelectionChanged -= VirtualSelectionChanged;
+
+ var newSelection = ComputeNewMultipleSelection();
+ if (!SelectionListsAreEqual(newSelection, ItemsView.SelectedItems))
+ {
+ ItemsView.UpdateSelectedItems(newSelection);
+ }
+
+ ItemsView.SelectionChanged += VirtualSelectionChanged;
+ }
+
+ static bool SelectionListsAreEqual(IList