diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ListView.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ListView.cs new file mode 100644 index 000000000000..8bcb81455962 --- /dev/null +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_ListView.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Private.Infrastructure; +using Uno.UI.RuntimeTests.Helpers; +using Windows.Foundation; +using Windows.UI.Input.Preview.Injection; +using Microsoft.UI.Xaml.Controls; +using System.Linq; + +namespace Uno.UI.RuntimeTests.Tests.Microsoft_UI_Xaml_Controls +{ +#if HAS_UNO + [TestClass] + public class Given_ListView + { + [TestMethod] + [RunsOnUIThread] +#if !HAS_INPUT_INJECTOR + [Ignore("InputInjector is not supported on this platform.")] +#endif + public async Task When_ItemClicked_SelectsCorrectIndex() + { + var items = new object[] { "Item 1", "Item 2", "Item 3" }; + var loggingSelectionInfo = new LoggingSelectionInfo(items); + var listViewBase = new ListView + { + ItemsSource = loggingSelectionInfo, + }; + + TestServices.WindowHelper.WindowContent = listViewBase; + await TestServices.WindowHelper.WaitForIdle(); + var tapTarget = listViewBase.TransformToVisual(null).TransformPoint(new Point(listViewBase.ActualWidth / 2, listViewBase.ActualHeight / 6)); + var injector = InputInjector.TryCreate() ?? throw new InvalidOperationException("Failed to init the InputInjector"); + using var finger = injector.GetFinger(); + + finger.Press(tapTarget); + finger.Release(); + + // Confirm that selection has been pushed to the source using the ISelectionInfo + Assert.AreEqual(loggingSelectionInfo.IsSelected(0), true, "Item 0 should be selected now."); + + // Confirm that the selection HAS NOT been pushed using the `ICollectionView` + Assert.AreEqual(loggingSelectionInfo.CurrentPosition, -1, "CurrentPosition should not have been updated."); + Assert.AreEqual(loggingSelectionInfo.CurrentItem, null, "CurrentItem should be not have been updated."); + } + } +#endif +} diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/LoggingSelectionInfo.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/LoggingSelectionInfo.cs new file mode 100644 index 000000000000..58fdcd841a99 --- /dev/null +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/LoggingSelectionInfo.cs @@ -0,0 +1,276 @@ +#if HAS_UNO +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using Microsoft.UI.Xaml.Data; +using Windows.Foundation.Collections; +using Windows.Foundation; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +internal class LoggingSelectionInfo : ICollectionView, ISelectionInfo, IList, INotifyCollectionChanged, INotifyPropertyChanged, IObservableVector +{ + private readonly List _collection = new List(); + private readonly HashSet _selectedIndices = new HashSet(); + private object _currentItem; + private int _currentPosition = -1; + + public LoggingSelectionInfo(IEnumerable items) + { + _collection.AddRange(items); + } + + #region ICollectionView Implementation + + public event CurrentChangingEventHandler CurrentChanging; + public event EventHandler CurrentChanged; + + public object CurrentItem => _currentItem; + + public int CurrentPosition => _currentPosition; + + public bool IsCurrentAfterLast => _currentPosition >= _collection.Count; + + public bool IsCurrentBeforeFirst => _currentPosition < 0; + + public bool MoveCurrentTo(object item) + { + int index = _collection.IndexOf(item); + return MoveCurrentToPosition(index); + } + + public bool MoveCurrentToPosition(int index) + { + if (index >= 0 && index < _collection.Count) + { + if (_currentPosition != index) + { + OnCurrentChanging(); + _currentItem = _collection[index]; + _currentPosition = index; + OnCurrentChanged(); + } + return true; + } + return false; + } + + public bool MoveCurrentToFirst() => MoveCurrentToPosition(0); + + public bool MoveCurrentToLast() => MoveCurrentToPosition(_collection.Count - 1); + + public bool MoveCurrentToNext() => MoveCurrentToPosition(_currentPosition + 1); + + public bool MoveCurrentToPrevious() => MoveCurrentToPosition(_currentPosition - 1); + + public Predicate Filter { get; set; } + + public bool CanFilter => Filter != null; + + public bool Contains(object item) => _collection.Contains(item); + + public IComparer SortDescriptions { get; set; } + + public bool CanSort => SortDescriptions != null; + + public IObservableVector CollectionGroups { get; } = null; + + public bool CanGroup => false; + + public object GetItemAt(int index) => _collection[index]; + + public int IndexOf(object item) => _collection.IndexOf(item); + + public bool HasMoreItems => false; + + public IAsyncOperation LoadMoreItemsAsync(uint count) + { + throw new NotImplementedException("LoadMoreItemsAsync is not implemented."); + } + + protected virtual void OnCurrentChanged() + { + CurrentChanged?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnCurrentChanging() + { + CurrentChanging?.Invoke(this, new CurrentChangingEventArgs(false)); + } + + #endregion + + #region ISelectionInfo Implementation + + public void SelectRange(ItemIndexRange itemIndexRange) + { + for (int i = itemIndexRange.FirstIndex; i < itemIndexRange.FirstIndex + itemIndexRange.Length; i++) + { + if (i >= 0 && i < _collection.Count) + { + _selectedIndices.Add(i); + } + } + OnSelectionChanged(); + } + + public void DeselectRange(ItemIndexRange itemIndexRange) + { + for (int i = itemIndexRange.FirstIndex; i < itemIndexRange.FirstIndex + itemIndexRange.Length; i++) + { + _selectedIndices.Remove(i); + } + OnSelectionChanged(); + } + + public bool IsSelected(int index) + { + return _selectedIndices.Contains(index); + } + + public IReadOnlyList GetSelectedRanges() + { + if (_selectedIndices.Count == 0) + { + return new List(); + } + + List ranges = new List(); + List sortedIndices = new List(_selectedIndices); + sortedIndices.Sort(); + + int rangeStart = sortedIndices[0]; + int rangeLength = 1; + + for (int i = 1; i < sortedIndices.Count; i++) + { + if (sortedIndices[i] == sortedIndices[i - 1] + 1) + { + rangeLength++; + } + else + { + ranges.Add(new ItemIndexRange(rangeStart, (uint)rangeLength)); + rangeStart = sortedIndices[i]; + rangeLength = 1; + } + } + + ranges.Add(new ItemIndexRange(rangeStart, (uint)rangeLength)); + return ranges; + } + + protected virtual void OnSelectionChanged() + { + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + #endregion + + #region IList and ICollection Implementation + + public int Count => _collection.Count; + + public bool IsReadOnly => false; + + public object this[int index] + { + get => _collection[index]; + set + { + if (_collection[index] != value) + { + _collection[index] = value; + OnPropertyChanged($"Item[{index}]"); + } + } + } + + public void Add(object item) + { + _collection.Add(item); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item)); + } + + public void Insert(int index, object item) + { + _collection.Insert(index, item); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)); + } + + public void RemoveAt(int index) + { + var removedItem = _collection[index]; + _collection.RemoveAt(index); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItem, index)); + } + + public void Clear() + { + _collection.Clear(); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + + public void CopyTo(object[] array, int arrayIndex) + { + _collection.CopyTo(array, arrayIndex); + } + + public bool Remove(object item) + { + var index = _collection.IndexOf(item); + if (index >= 0) + { + _collection.RemoveAt(index); + OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index)); + return true; + } + return false; + } + + #endregion + + #region IEnumerable Implementation + + public IEnumerator GetEnumerator() => _collection.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _collection.GetEnumerator(); + + #endregion + + #region INotifyCollectionChanged Implementation + + public event NotifyCollectionChangedEventHandler CollectionChanged; + + protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e) + { + CollectionChanged?.Invoke(this, e); + } + + #endregion + + #region INotifyPropertyChanged Implementation + + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + #endregion + + #region IObservableVector Implementation + + public event VectorChangedEventHandler VectorChanged; + + protected virtual void OnVectorChanged(CollectionChange change, int index) + { + VectorChanged?.Invoke(this, new VectorChangedEventArgs(change, (uint)index)); + } + + #endregion +} +#endif diff --git a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBase.cs b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBase.cs index 46d73dd9773d..f402091eabef 100644 --- a/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBase.cs +++ b/src/Uno.UI/UI/Xaml/Controls/ListViewBase/ListViewBase.cs @@ -609,8 +609,18 @@ internal override void OnItemClicked(int clickedIndex, VirtualKeyModifiers modif void SingleSelectionCase() { + //The `ISelectionInfo` handled directly by the `ItemsSource` has precedence over the `ICollectionView`. + //If an object implements both interfaces, as WinUI, we make sure to use only the `ISelectionInfo` to avoid conflicting interaction. - if (ItemsSource is ICollectionView collectionView) + // Ensure that we do not alter the selection if ISelectionInfo is in use + if (ItemsSource is ISelectionInfo) + { + // The selection will be managed by ISelectionInfo, so we do nothing here. + // This prevents ICollectionView from interfering with the selection logic. + return; + } + + if (ItemsSource is ICollectionView collectionView && !(ItemsSource is ISelectionInfo)) { //NOTE: Windows seems to call MoveCurrentTo(item); we set position instead to have expected behavior when you have duplicate items in the list. collectionView.MoveCurrentToPosition(clickedIndex);