diff --git a/src/Controls/src/Core/Binding.cs b/src/Controls/src/Core/Binding.cs index be91c9f8c178..74ca4eeaa267 100644 --- a/src/Controls/src/Core/Binding.cs +++ b/src/Controls/src/Core/Binding.cs @@ -200,10 +200,7 @@ internal override void Unapply(bool fromBindingContextChanged = false) base.Unapply(fromBindingContextChanged: fromBindingContextChanged); - if (_expression != null) - { - _expression.Unapply(); - } + _expression?.Unapply(); } } } \ No newline at end of file diff --git a/src/Controls/src/Core/CollectionSynchronizationContext.cs b/src/Controls/src/Core/CollectionSynchronizationContext.cs index 5d3de023de03..95203b11fa4d 100644 --- a/src/Controls/src/Core/CollectionSynchronizationContext.cs +++ b/src/Controls/src/Core/CollectionSynchronizationContext.cs @@ -15,7 +15,7 @@ internal CollectionSynchronizationContext(object context, CollectionSynchronizat internal object Context { - get { return ContextReference != null ? ContextReference.Target : null; } + get { return ContextReference?.Target; } } internal WeakReference ContextReference { get; } diff --git a/src/Controls/src/Core/Compatibility/Handlers/Android/VisualElementRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/Android/VisualElementRenderer.cs index 88c05a9a3649..5d61dcb51b0d 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Android/VisualElementRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Android/VisualElementRenderer.cs @@ -48,10 +48,7 @@ protected override void OnLayout(bool changed, int l, int t, int r, int b) if (ChildCount > 0) { var platformView = GetChildAt(0); - if (platformView != null) - { - platformView.Layout(0, 0, r - l, b - t); - } + platformView?.Layout(0, 0, r - l, b - t); } } diff --git a/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/EntryCellView.cs b/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/EntryCellView.cs index 621db975cbe1..9a42e2b6c043 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/EntryCellView.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/EntryCellView.cs @@ -118,7 +118,7 @@ void ITextWatcher.OnTextChanged(ICharSequence s, int start, int before, int coun { Action changed = TextChanged; if (changed != null) - changed(s != null ? s.ToString() : null); + changed(s?.ToString()); } public void SetLabelTextColor(Color color, int defaultColorResourceId) diff --git a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellSectionRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellSectionRenderer.cs index 24ceab4a95cf..c04fb0afa922 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellSectionRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/Shell/Android/ShellSectionRenderer.cs @@ -166,10 +166,7 @@ void UpdateTabTitle(ShellContent shellContent) if (index >= 0) { var tab = _tablayout.GetTabAt(index); - if (tab != null) - { - tab.SetText(new string(shellContent.Title)); - } + tab?.SetText(new string(shellContent.Title)); } } diff --git a/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs b/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs index 0838765d1d26..8e0a4f63272d 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/MauiCarouselRecyclerView.cs @@ -169,10 +169,7 @@ protected override void UpdateItemSpacing() var adapter = GetAdapter(); - if (adapter != null) - { - adapter.NotifyItemChanged(_oldPosition); - } + adapter?.NotifyItemChanged(_oldPosition); base.UpdateItemSpacing(); } diff --git a/src/Controls/src/Core/Handlers/Items/CarouselViewHandler.Windows.cs b/src/Controls/src/Core/Handlers/Items/CarouselViewHandler.Windows.cs index 5efafef7e98b..40c7a972bc83 100644 --- a/src/Controls/src/Core/Handlers/Items/CarouselViewHandler.Windows.cs +++ b/src/Controls/src/Core/Handlers/Items/CarouselViewHandler.Windows.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Specialized; +using System.ComponentModel; using System.Linq; using Microsoft.Maui.Controls.Platform; using Microsoft.UI.Xaml; @@ -27,10 +28,16 @@ public partial class CarouselViewHandler : ItemsViewHandler bool _isCarouselViewReady; NotifyCollectionChangedEventHandler _collectionChanged; readonly WeakNotifyCollectionChangedProxy _proxy = new(); + WeakNotifyPropertyChangedProxy _layoutPropertyChangedProxy; + PropertyChangedEventHandler _layoutPropertyChanged; - ~CarouselViewHandler() => _proxy.Unsubscribe(); + ~CarouselViewHandler() + { + _proxy.Unsubscribe(); + _layoutPropertyChangedProxy?.Unsubscribe(); + } - protected override IItemsLayout Layout { get; } + protected override IItemsLayout Layout => ItemsView?.ItemsLayout; LinearItemsLayout CarouselItemsLayout => ItemsView?.ItemsLayout; WDataTemplate CarouselItemsViewTemplate => (WDataTemplate)WApp.Current.Resources["CarouselItemsViewDefaultTemplate"]; @@ -42,6 +49,8 @@ protected override void ConnectHandler(ListViewBase platformView) UpdateScrollBarVisibilityForLoop(); + UpdateLayoutPropertyChangeProxy(); + base.ConnectHandler(platformView); } @@ -56,6 +65,12 @@ protected override void DisconnectHandler(ListViewBase platformView) _proxy.Unsubscribe(); } + if (_layoutPropertyChangedProxy is not null) + { + _layoutPropertyChangedProxy.Unsubscribe(); + _layoutPropertyChangedProxy = null; + } + if (_scrollViewer != null) { _scrollViewer.ViewChanging -= OnScrollViewChanging; @@ -74,6 +89,10 @@ protected override void UpdateItemsSource() return; base.UpdateItemsSource(); + + // Update snap points after items source changes + UpdateSnapPointsType(); + UpdateSnapPointsAlignment(); } protected override void UpdateItemTemplate() @@ -102,6 +121,10 @@ protected override void OnScrollViewerFound(ScrollViewer scrollViewer) { UpdateScrollBarVisibility(); } + + // Update snap points when ScrollViewer is found + UpdateSnapPointsType(); + UpdateSnapPointsAlignment(); } protected override ICollectionView GetCollectionView(CollectionViewSource collectionViewSource) @@ -433,16 +456,66 @@ WSnapPointsAlignment GetWindowsSnapPointsAlignment(SnapPointsAlignment snapPoint return WSnapPointsAlignment.Center; } + void LayoutPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == ItemsLayout.SnapPointsTypeProperty.PropertyName) + UpdateSnapPointsType(); + else if (e.PropertyName == ItemsLayout.SnapPointsAlignmentProperty.PropertyName) + UpdateSnapPointsAlignment(); + } + + public static void MapItemsLayout(CarouselViewHandler handler, CarouselView carouselView) + { + handler.UpdateLayoutPropertyChangeProxy(); + } + + void UpdateLayoutPropertyChangeProxy() + { + // Clean up the old proxy + if (_layoutPropertyChangedProxy is not null) + { + _layoutPropertyChangedProxy.Unsubscribe(); + _layoutPropertyChangedProxy = null; + } + + // Set up the new proxy if Layout is not null + if (Layout is not null) + { + _layoutPropertyChanged ??= LayoutPropertyChanged; + _layoutPropertyChangedProxy = new WeakNotifyPropertyChangedProxy(Layout, _layoutPropertyChanged); + } + } + void UpdateSnapPointsType() { if (_scrollViewer == null || CarouselItemsLayout == null) return; + var windowsSnapType = GetWindowsSnapPointsType(CarouselItemsLayout.SnapPointsType); + if (CarouselItemsLayout.Orientation == ItemsLayoutOrientation.Horizontal) - _scrollViewer.HorizontalSnapPointsType = GetWindowsSnapPointsType(CarouselItemsLayout.SnapPointsType); + { + _scrollViewer.HorizontalSnapPointsType = windowsSnapType; + // Ensure zoom mode is disabled for proper snap point behavior + _scrollViewer.ZoomMode = Microsoft.UI.Xaml.Controls.ZoomMode.Disabled; + // Ensure scroll mode is enabled for snap points to work + if (windowsSnapType != WSnapPointsType.None && ItemsView.IsSwipeEnabled) + { + _scrollViewer.HorizontalScrollMode = WScrollMode.Auto; + } + } if (CarouselItemsLayout.Orientation == ItemsLayoutOrientation.Vertical) - _scrollViewer.VerticalSnapPointsType = GetWindowsSnapPointsType(CarouselItemsLayout.SnapPointsType); + { + _scrollViewer.VerticalSnapPointsType = windowsSnapType; + // Ensure zoom mode is disabled for proper snap point behavior + _scrollViewer.ZoomMode = Microsoft.UI.Xaml.Controls.ZoomMode.Disabled; + // Ensure scroll mode is enabled for snap points to work + if (windowsSnapType != WSnapPointsType.None && ItemsView.IsSwipeEnabled) + { + _scrollViewer.VerticalScrollMode = WScrollMode.Auto; + } + } } void UpdateSnapPointsAlignment() @@ -596,6 +669,10 @@ void InvalidateItemSize() item.ItemWidth = itemWidth; } ListViewBase.InvalidateMeasure(); + + // Refresh snap points after item size changes + UpdateSnapPointsType(); + UpdateSnapPointsAlignment(); } } } diff --git a/src/Controls/src/Core/Handlers/Items/CarouselViewHandler.cs b/src/Controls/src/Core/Handlers/Items/CarouselViewHandler.cs index 019796a0c899..67c852b5899d 100644 --- a/src/Controls/src/Core/Handlers/Items/CarouselViewHandler.cs +++ b/src/Controls/src/Core/Handlers/Items/CarouselViewHandler.cs @@ -14,7 +14,7 @@ public CarouselViewHandler(PropertyMapper mapper = null) : base(mapper ?? Mapper public static PropertyMapper Mapper = new(ItemsViewMapper) { -#if TIZEN || ANDROID +#if TIZEN || ANDROID || WINDOWS [Controls.CarouselView.ItemsLayoutProperty.PropertyName] = MapItemsLayout, #endif [Controls.CarouselView.IsSwipeEnabledProperty.PropertyName] = MapIsSwipeEnabled, diff --git a/src/Controls/src/Core/Handlers/Shell/Tizen/ShellView.cs b/src/Controls/src/Core/Handlers/Shell/Tizen/ShellView.cs index 726e2c5ee81f..5220a30c0e1d 100644 --- a/src/Controls/src/Core/Handlers/Shell/Tizen/ShellView.cs +++ b/src/Controls/src/Core/Handlers/Shell/Tizen/ShellView.cs @@ -145,8 +145,7 @@ public void UpdateBackgroundColor(GColor color) public void UpdateCurrentItem(ShellItem newItem) { - if (_currentItemHandler != null) - _currentItemHandler.Dispose(); + _currentItemHandler?.Dispose(); if (newItem != null) { diff --git a/src/Controls/src/Core/ImageSource.cs b/src/Controls/src/Core/ImageSource.cs index d85f99ba78cb..836ed4871689 100644 --- a/src/Controls/src/Core/ImageSource.cs +++ b/src/Controls/src/Core/ImageSource.cs @@ -38,8 +38,7 @@ private set { if (_cancellationTokenSource == value) return; - if (_cancellationTokenSource != null) - _cancellationTokenSource.Cancel(); + _cancellationTokenSource?.Cancel(); _cancellationTokenSource = value; } } @@ -127,8 +126,7 @@ private protected async Task OnLoadingCompleted(bool cancelled) return; TaskCompletionSource tcs = Interlocked.Exchange(ref _completionSource, null); - if (tcs != null) - tcs.SetResult(cancelled); + tcs?.SetResult(cancelled); await _cancellationTokenSourceLock.WaitAsync(); try diff --git a/src/Controls/src/Core/ListProxy.cs b/src/Controls/src/Core/ListProxy.cs index eb217a0d96dc..82a9eaf1e08b 100644 --- a/src/Controls/src/Core/ListProxy.cs +++ b/src/Controls/src/Core/ListProxy.cs @@ -147,16 +147,13 @@ public void Clear() if (_enumerator != null) { var dispose = _enumerator as IDisposable; - if (dispose != null) - dispose.Dispose(); + dispose?.Dispose(); _enumerator = null; } - if (_items != null) - _items.Clear(); - if (_indexesCounted != null) - _indexesCounted.Clear(); + _items?.Clear(); + _indexesCounted?.Clear(); OnCountChanged(); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); @@ -303,8 +300,7 @@ bool TryGetValue(int index, out object value) _windowIndex = 0; var dispose = _enumerator as IDisposable; - if (dispose != null) - dispose.Dispose(); + dispose?.Dispose(); _enumerator = null; _enumeratorIndex = 0; @@ -335,8 +331,7 @@ bool TryGetValue(int index, out object value) if (!moved) { var dispose = _enumerator as IDisposable; - if (dispose != null) - dispose.Dispose(); + dispose?.Dispose(); _enumerator = null; _enumeratorIndex = 0; diff --git a/src/Controls/src/Core/LockingSemaphore.cs b/src/Controls/src/Core/LockingSemaphore.cs index cb6edde1d24e..31c239b17196 100644 --- a/src/Controls/src/Core/LockingSemaphore.cs +++ b/src/Controls/src/Core/LockingSemaphore.cs @@ -29,8 +29,7 @@ public void Release() else ++_currentCount; } - if (toRelease != null) - toRelease.TrySetResult(true); + toRelease?.TrySetResult(true); } public Task WaitAsync(CancellationToken token) diff --git a/src/Controls/src/Core/Platform/Android/Extensions/ToolbarExtensions.cs b/src/Controls/src/Core/Platform/Android/Extensions/ToolbarExtensions.cs index f341b1a5f049..54b2dbc37663 100644 --- a/src/Controls/src/Core/Platform/Android/Extensions/ToolbarExtensions.cs +++ b/src/Controls/src/Core/Platform/Android/Extensions/ToolbarExtensions.cs @@ -248,10 +248,7 @@ public static void UpdateMenuItems(this AToolbar toolbar, int toolBarItemCount = i; while (toolBarItemCount < previousMenuItems.Count) { - if (menu != null) - { - menu.RemoveItem(previousMenuItems[toolBarItemCount].ItemId); - } + menu?.RemoveItem(previousMenuItems[toolBarItemCount].ItemId); previousMenuItems[toolBarItemCount].Dispose(); previousMenuItems.RemoveAt(toolBarItemCount); } diff --git a/src/Controls/src/Core/Shapes/Path.cs b/src/Controls/src/Core/Shapes/Path.cs index 3ca2c5b34c1d..87ae0f84669a 100644 --- a/src/Controls/src/Core/Shapes/Path.cs +++ b/src/Controls/src/Core/Shapes/Path.cs @@ -109,8 +109,7 @@ public override PathF GetPath() { var path = new PathF(); - if (Data != null) - Data.AppendPath(path); + Data?.AppendPath(path); return path; } diff --git a/src/Controls/src/Core/TemplateBinding.cs b/src/Controls/src/Core/TemplateBinding.cs index 288bae4a56a6..da0820f30ebc 100644 --- a/src/Controls/src/Core/TemplateBinding.cs +++ b/src/Controls/src/Core/TemplateBinding.cs @@ -127,8 +127,7 @@ internal override void Unapply(bool fromBindingContextChanged = false) { base.Unapply(fromBindingContextChanged: fromBindingContextChanged); - if (_expression != null) - _expression.Unapply(); + _expression?.Unapply(); } void ApplyInner(Element templatedParent, BindableObject bindableObject, BindableProperty targetProperty) diff --git a/src/Controls/tests/DeviceTests/Elements/CarouselView/CarouselViewSnapPointsTests.Windows.cs b/src/Controls/tests/DeviceTests/Elements/CarouselView/CarouselViewSnapPointsTests.Windows.cs new file mode 100644 index 000000000000..ac6634d2006c --- /dev/null +++ b/src/Controls/tests/DeviceTests/Elements/CarouselView/CarouselViewSnapPointsTests.Windows.cs @@ -0,0 +1,45 @@ +using System.Threading.Tasks; +using Microsoft.Maui.Controls; +using Xunit; + +namespace Microsoft.Maui.DeviceTests +{ + public partial class CarouselViewSnapPointsTests + { + // Windows-specific test to verify that the ScrollViewer properties are actually set + [Fact] + public async Task CarouselViewSnapPointsAreAppliedToScrollViewerOnWindows() + { + SetupBuilder(); + + var carouselView = new CarouselView + { + ItemsSource = new[] { "Item 1", "Item 2", "Item 3" }, + ItemTemplate = new DataTemplate(() => new Label()) + }; + + var handler = await CreateHandlerAsync(carouselView); + + // Allow time for the handler to initialize + await Task.Delay(200); + + // Change the snap points type + carouselView.ItemsLayout.SnapPointsType = SnapPointsType.Mandatory; + + // Wait for the property change to propagate + await Task.Delay(100); + + // Verify the change was applied to the view model + Assert.Equal(SnapPointsType.Mandatory, carouselView.ItemsLayout.SnapPointsType); + + // Change the snap points alignment + carouselView.ItemsLayout.SnapPointsAlignment = SnapPointsAlignment.Start; + + // Wait for the property change to propagate + await Task.Delay(100); + + // Verify the change was applied to the view model + Assert.Equal(SnapPointsAlignment.Start, carouselView.ItemsLayout.SnapPointsAlignment); + } + } +} \ No newline at end of file diff --git a/src/Controls/tests/DeviceTests/Elements/CarouselView/CarouselViewSnapPointsTests.cs b/src/Controls/tests/DeviceTests/Elements/CarouselView/CarouselViewSnapPointsTests.cs new file mode 100644 index 000000000000..51a0af88fba0 --- /dev/null +++ b/src/Controls/tests/DeviceTests/Elements/CarouselView/CarouselViewSnapPointsTests.cs @@ -0,0 +1,126 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.Handlers.Items; +using Microsoft.Maui.Handlers; +using Microsoft.Maui.Hosting; +using Xunit; + +namespace Microsoft.Maui.DeviceTests +{ + public partial class CarouselViewSnapPointsTests : ControlsHandlerTestBase + { + void SetupBuilder() + { + EnsureHandlerCreated(builder => + { + builder.ConfigureMauiHandlers(handlers => + { + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + handlers.AddHandler(); + }); + }); + } + + [Fact] + public async Task CarouselViewSnapPointsTypeCanBeUpdatedAtRuntime() + { + SetupBuilder(); + + var carouselView = new CarouselView + { + ItemsSource = new[] { "Item 1", "Item 2", "Item 3" }, + ItemTemplate = new DataTemplate(() => new Label()) + }; + + var handler = await CreateHandlerAsync(carouselView); + + // Check initial state + Assert.Equal(SnapPointsType.MandatorySingle, carouselView.ItemsLayout.SnapPointsType); + + // Change the snap points type + carouselView.ItemsLayout.SnapPointsType = SnapPointsType.Mandatory; + + // Wait for the property change to propagate + await Task.Delay(100); + + // Verify the change was applied + Assert.Equal(SnapPointsType.Mandatory, carouselView.ItemsLayout.SnapPointsType); + + // Change to None + carouselView.ItemsLayout.SnapPointsType = SnapPointsType.None; + + // Wait for the property change to propagate + await Task.Delay(100); + + // Verify the change was applied + Assert.Equal(SnapPointsType.None, carouselView.ItemsLayout.SnapPointsType); + } + + [Fact] + public async Task CarouselViewSnapPointsAlignmentCanBeUpdatedAtRuntime() + { + SetupBuilder(); + + var carouselView = new CarouselView + { + ItemsSource = new[] { "Item 1", "Item 2", "Item 3" }, + ItemTemplate = new DataTemplate(() => new Label()) + }; + + var handler = await CreateHandlerAsync(carouselView); + + // Check initial state + Assert.Equal(SnapPointsAlignment.Center, carouselView.ItemsLayout.SnapPointsAlignment); + + // Change the snap points alignment + carouselView.ItemsLayout.SnapPointsAlignment = SnapPointsAlignment.Start; + + // Wait for the property change to propagate + await Task.Delay(100); + + // Verify the change was applied + Assert.Equal(SnapPointsAlignment.Start, carouselView.ItemsLayout.SnapPointsAlignment); + + // Change to End + carouselView.ItemsLayout.SnapPointsAlignment = SnapPointsAlignment.End; + + // Wait for the property change to propagate + await Task.Delay(100); + + // Verify the change was applied + Assert.Equal(SnapPointsAlignment.End, carouselView.ItemsLayout.SnapPointsAlignment); + } + + [Fact] + public async Task CarouselViewSnapPointsHandlerShouldNotCrashWhenUpdatingProperties() + { + SetupBuilder(); + + var carouselView = new CarouselView + { + ItemsSource = new[] { "Item 1", "Item 2", "Item 3" }, + ItemTemplate = new DataTemplate(() => new Label()) + }; + + var handler = await CreateHandlerAsync(carouselView); + + // This should not crash + carouselView.ItemsLayout.SnapPointsType = SnapPointsType.Mandatory; + carouselView.ItemsLayout.SnapPointsAlignment = SnapPointsAlignment.Start; + + // Wait for any background processing + await Task.Delay(100); + + // Rapid changes should also not crash + for (int i = 0; i < 10; i++) + { + carouselView.ItemsLayout.SnapPointsType = (SnapPointsType)(i % 3); + carouselView.ItemsLayout.SnapPointsAlignment = (SnapPointsAlignment)(i % 3); + await Task.Delay(10); + } + } + } +} \ No newline at end of file diff --git a/src/Controls/tests/TestCases.HostApp/Issues/CarouselViewSnapPointsTest.xaml b/src/Controls/tests/TestCases.HostApp/Issues/CarouselViewSnapPointsTest.xaml new file mode 100644 index 000000000000..23706cd0a3f3 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/CarouselViewSnapPointsTest.xaml @@ -0,0 +1,119 @@ + + + + + +