diff --git a/src/Controls/src/Core/Handlers/Items/Android/Adapters/ReorderableItemsViewAdapter.cs b/src/Controls/src/Core/Handlers/Items/Android/Adapters/ReorderableItemsViewAdapter.cs index 3f6b68217827..c2b00fee0d6d 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/Adapters/ReorderableItemsViewAdapter.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/Adapters/ReorderableItemsViewAdapter.cs @@ -47,6 +47,16 @@ public bool OnItemMove(int fromPosition, int toPosition) return false; } + if (fromItemIndex < 0 || fromItemIndex >= fromList.Count) + { + return false; + } + + if (toItemIndex < 0 || toItemIndex > toList.Count) + { + return false; + } + if (fromList != null && toList != null) { var fromItem = fromList[fromItemIndex]; diff --git a/src/Controls/src/Core/Handlers/Items/Android/SimpleItemTouchHelperCallback.cs b/src/Controls/src/Core/Handlers/Items/Android/SimpleItemTouchHelperCallback.cs index 865027324f70..5f8e774659cd 100644 --- a/src/Controls/src/Core/Handlers/Items/Android/SimpleItemTouchHelperCallback.cs +++ b/src/Controls/src/Core/Handlers/Items/Android/SimpleItemTouchHelperCallback.cs @@ -18,7 +18,7 @@ public override int GetMovementFlags(RecyclerView recyclerView, RecyclerView.Vie var itemViewType = viewHolder.ItemViewType; if (itemViewType == ItemViewType.Header || itemViewType == ItemViewType.Footer || itemViewType == ItemViewType.GroupHeader || itemViewType == ItemViewType.GroupFooter) - { + { return MakeMovementFlags(0, 0); } @@ -28,12 +28,10 @@ public override int GetMovementFlags(RecyclerView recyclerView, RecyclerView.Vie public override bool OnMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { - // Block reordering onto structural elements (Header, Footer, GroupHeader, GroupFooter). - // Dragging FROM structural elements is already prevented by GetMovementFlags returning 0. - // All other items (including those with different DataTemplateSelector view types) can be freely reordered. - var targetViewType = target.ItemViewType; - if (targetViewType == ItemViewType.Header || targetViewType == ItemViewType.Footer - || targetViewType == ItemViewType.GroupHeader || targetViewType == ItemViewType.GroupFooter) + var sourceItemViewType = viewHolder.ItemViewType; + + if (sourceItemViewType == ItemViewType.Header || sourceItemViewType == ItemViewType.Footer + || sourceItemViewType == ItemViewType.GroupHeader || sourceItemViewType == ItemViewType.GroupFooter) { return false; } diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue12008.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue12008.cs index 0b7b23c63232..5d0713f60f8c 100644 --- a/src/Controls/tests/TestCases.HostApp/Issues/Issue12008.cs +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue12008.cs @@ -1,169 +1,225 @@ using System.Collections.ObjectModel; -using Microsoft.Maui.Controls.Shapes; +using System.ComponentModel; namespace Maui.Controls.Sample.Issues; -[Issue(IssueTracker.Github, 12008, "CollectionView Drag and Drop Reordering Can't Drop in Empty Group", PlatformAffected.iOS)] -public class Issue12008 : ContentPage +[Issue(IssueTracker.Github, 12008, "CollectionView Drag and Drop Reordering Can't Drop in Empty Group", PlatformAffected.Android | PlatformAffected.iOS | PlatformAffected.macOS)] +public partial class Issue12008 : ContentPage { - public ObservableCollection Groups { get; } - + private Label statusLabel; + private CollectionView collectionView; + private Button btnCreateEmptyGroup; public Issue12008() { - Title = "Issue 12008 - Drag into Empty Group"; - - // Create grouped data with one empty group - Groups = new ObservableCollection + this.BindingContext = new Issue12008ViewModel(); + var grid = new Grid { - new Issue12008Group("Group A", new ObservableCollection - { - new Item("Item A1"), - new Item("Item A2"), - new Item("Item A3") - }), - new Issue12008Group("Group B", new ObservableCollection + RowDefinitions = new RowDefinitionCollection { - new Item("Item B1"), - new Item("Item B2") - }), - new Issue12008Group("Empty Group", new ObservableCollection()), // Empty group - new Issue12008Group("Group C", new ObservableCollection - { - new Item("Item C1"), - }) + new RowDefinition { Height = GridLength.Auto }, + new RowDefinition { Height = GridLength.Star } + } + }; + + // Top StackLayout + var stackLayout = new StackLayout + { + Padding = 10 + }; + + var titleLabel = new Label + { + Text = "Drag and Drop Test - Empty Groups", + FontSize = 18, + FontAttributes = FontAttributes.Bold, + Margin = new Thickness(0, 0, 0, 10) + }; + + var instructionsLabel = new Label + { + Text = "Instructions: Try dragging items between groups, including into empty groups.", + FontSize = 14, + Margin = new Thickness(0, 0, 0, 10) + }; + + btnCreateEmptyGroup = new Button + { + Text = "Create Empty Group", + AutomationId = "CreateEmptyGroupButton12008" + }; + btnCreateEmptyGroup.Clicked += OnCreateEmptyGroupClicked; + + statusLabel = new Label + { + Text = "Status: Ready", + FontSize = 12, + AutomationId = "StatusLabel12008" }; - var collectionView = new CollectionView + stackLayout.Children.Add(titleLabel); + stackLayout.Children.Add(instructionsLabel); + stackLayout.Children.Add(btnCreateEmptyGroup); + stackLayout.Children.Add(statusLabel); + + Grid.SetRow(stackLayout, 0); + grid.Children.Add(stackLayout); + + // CollectionView + collectionView = new CollectionView { - AutomationId = "ReorderCollectionView", - ItemsSource = Groups, IsGrouped = true, CanReorderItems = true, CanMixGroups = true, - SelectionMode = SelectionMode.None + AutomationId = "CollectionView12008" }; + collectionView.SetBinding(ItemsView.ItemsSourceProperty, "GroupedItems"); - // Set GroupHeaderTemplate to display group name and item count + // Group Header Template collectionView.GroupHeaderTemplate = new DataTemplate(() => { - var groupNameLabel = new Label + var headerGrid = new Grid { - FontAttributes = FontAttributes.Bold, - FontSize = 18, - TextColor = Colors.Black + BackgroundColor = Colors.LightBlue, + Padding = 10, + ColumnDefinitions = new ColumnDefinitionCollection + { + new ColumnDefinition { Width = GridLength.Star }, + new ColumnDefinition { Width = GridLength.Auto } + } }; - groupNameLabel.SetBinding(Label.TextProperty, new Binding("Name")); - var countLabel = new Label + var headerLabel = new Label { - FontSize = 16, - TextColor = Colors.Gray, - HorizontalTextAlignment = TextAlignment.End + FontAttributes = FontAttributes.Bold, + FontSize = 16 }; - countLabel.SetBinding(Label.TextProperty, new Binding("Count", stringFormat: "({0} items)")); + headerLabel.SetBinding(Label.TextProperty, "GroupName"); + headerLabel.SetBinding(AutomationIdProperty, new Binding("GroupName", stringFormat: "GroupHeader12008{0}")); - var headerGrid = new Grid + var countLabel = new Label { - Padding = 12, - BackgroundColor = Color.FromArgb("#F0F0F0"), - ColumnDefinitions = { new ColumnDefinition { Width = GridLength.Star }, new ColumnDefinition { Width = GridLength.Auto } }, - Children = - { - groupNameLabel, - countLabel - } + FontSize = 12 }; - headerGrid.SetColumn(countLabel, 1); - - // Set AutomationId based on group name - headerGrid.SetBinding(AutomationProperties.NameProperty, new Binding("Name")); + countLabel.SetBinding(Label.TextProperty, new Binding("Count", stringFormat: "Count: {0}")); + countLabel.SetBinding(AutomationIdProperty, new Binding("GroupName", stringFormat: "GroupCount12008{0}")); + headerGrid.Children.Add(headerLabel); + headerGrid.Children.Add(countLabel); + Grid.SetColumn(headerLabel, 0); + Grid.SetColumn(countLabel, 1); return headerGrid; }); - // Set ItemTemplate + // Item Template collectionView.ItemTemplate = new DataTemplate(() => { - var itemLabel = new Label + var itemGrid = new Grid { - FontSize = 16, - Padding = 16 + Padding = 10, + BackgroundColor = Colors.LightGray, + Margin = new Thickness(2) }; - itemLabel.SetBinding(Label.TextProperty, "Name"); - var itemContainer = new Border + var itemLabel = new Label { - Padding = 0, - Margin = new Thickness(12, 4, 12, 4), - Content = itemLabel, - BackgroundColor = Colors.White, - StrokeShape = new RoundRectangle { CornerRadius = 5 }, - Shadow = new Shadow { Opacity = 0.3f, Radius = 2 } + FontSize = 14 }; + itemLabel.SetBinding(Label.TextProperty, "Name"); + itemLabel.SetBinding(AutomationIdProperty, new Binding("Name", stringFormat: "Item12008{0}")); - // Set AutomationId based on item name for testability - itemContainer.SetBinding(AutomationProperties.NameProperty, new Binding("Name")); - - return itemContainer; + itemGrid.Children.Add(itemLabel); + return itemGrid; }); - // Add ReorderCompleted event handler to update status - collectionView.ReorderCompleted += OnReorderCompleted; + Grid.SetRow(collectionView, 1); + grid.Children.Add(collectionView); - var statusLabel = new Label - { - AutomationId = "StatusLabel", - Text = "Ready to reorder items", - FontSize = 14, - Padding = 12, - BackgroundColor = Color.FromArgb("#E8F5E9") - }; + Content = grid; + } - Content = new VerticalStackLayout - { - Spacing = 8, - Padding = 8, - Children = - { - statusLabel, - collectionView - } - }; - BindingContext = this; + private void OnCreateEmptyGroupClicked(object sender, EventArgs e) + { + if (this.BindingContext is Issue12008ViewModel viewModel) + { + viewModel.CreateEmptyGroup(); + statusLabel.Text = "Status: Empty group created"; + } } +} + +public class Issue12008ViewModel : INotifyPropertyChanged +{ + private ObservableCollection _groupedItems; - void OnReorderCompleted(object sender, EventArgs e) + public ObservableCollection GroupedItems { - // Update status label with per-group counts so tests can verify actual data model changes - if (Content is VerticalStackLayout layout && layout.Children[0] is Label statusLabel) + get => _groupedItems; + set { - var groupCounts = string.Join(", ", Groups.Select(g => $"{g.Name}:{g.Count}")); - statusLabel.Text = $"Reorder completed! {groupCounts}"; - statusLabel.BackgroundColor = Color.FromArgb("#C8E6C9"); + _groupedItems = value; + OnPropertyChanged(); } } - public class Issue12008Group : ObservableCollection + public Issue12008ViewModel() { - public string Name { get; set; } + InitializeData(); + } - public Issue12008Group(string name, ObservableCollection items) - { - Name = name; + private void InitializeData() + { + GroupedItems = new ObservableCollection + { + new Issue12008Group("GroupA", new List + { + new Issue12008Item("ItemA1"), + new Issue12008Item("ItemA2"), + new Issue12008Item("ItemA3") + }), + new Issue12008Group("GroupB", new List + { + new Issue12008Item("ItemB1"), + new Issue12008Item("ItemB2") + }), + new Issue12008Group("GroupC", new List + { + new Issue12008Item("ItemC1") + }) + }; + } - foreach (var item in items) - Add(item); - } + public void CreateEmptyGroup() + { + var emptyGroup = new Issue12008Group("EmptyGroup", new List()); + GroupedItems.Add(emptyGroup); } - public class Item + public event PropertyChangedEventHandler PropertyChanged; + + protected virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = null) { - public string Name { get; set; } + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } +} - public Item(string name) - { - Name = name; - } +public class Issue12008Group : ObservableCollection +{ + public string GroupName { get; } + + public Issue12008Group(string groupName, IEnumerable items) : base(items) + { + GroupName = groupName; } } + +public class Issue12008Item +{ + public string Name { get; } + + public Issue12008Item(string name) + { + Name = name; + } +} + diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12008.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12008.cs index ec80a9395313..86e8f809432c 100644 --- a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12008.cs +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue12008.cs @@ -1,4 +1,4 @@ -#if TEST_FAILS_ON_WINDOWS && TEST_FAILS_ON_ANDROID +#if TEST_FAILS_ON_WINDOWS // The test fails on Windows because Drag and Drop with grouping is not supported on Windows using NUnit.Framework; using UITest.Appium; using UITest.Core; @@ -7,31 +7,38 @@ namespace Microsoft.Maui.TestCases.Tests.Issues; public class Issue12008 : _IssuesUITest { - public override string Issue => "CollectionView Drag and Drop Reordering Can't Drop in Empty Group"; + public Issue12008(TestDevice testDevice) : base(testDevice) + { + } - public Issue12008(TestDevice device) : base(device) { } + public override string Issue => "CollectionView Drag and Drop Reordering Can't Drop in Empty Group"; - [Test] - [Category(UITestCategories.CollectionView)] - public void CanDragItemIntoEmptyGroup() - { - // Wait for the CollectionView to be ready - App.WaitForElement("ReorderCollectionView"); + [Test] + [Category(UITestCategories.CollectionView)] + public void EmptyGroupCreationShouldWork() + { + App.WaitForElement("CreateEmptyGroupButton12008"); + App.Tap("CreateEmptyGroupButton12008"); - // Verify initial setup - items and empty group are present - App.WaitForElement("Item A1"); - App.WaitForElement("Empty Group"); + App.WaitForElement("GroupHeader12008EmptyGroup"); + App.WaitForElement("StatusLabel12008"); + var statusText = App.WaitForElement("StatusLabel12008").GetText(); + Assert.That(statusText, Does.Contain("Empty group created")); + } - // Drag an item from Group A into the Empty Group - App.DragAndDrop("Item A1", "Empty Group"); - - // Verify the data model actually changed: - // - Group A should have lost one item (3 -> 2) - // - Empty Group should have gained one item (0 -> 1) - var statusLabel = App.WaitForElement("StatusLabel"); - var statusText = statusLabel.GetText(); - Assert.That(statusText, Does.Contain("Empty Group:1"), "Empty Group should contain 1 item after drag-and-drop"); - Assert.That(statusText, Does.Contain("Group A:2"), "Group A should contain 2 items after losing one to Empty Group"); - } + [Test] + [Category(UITestCategories.CollectionView)] + public void DragItemIntoEmptyGroupShouldSucceed() + { + App.WaitForElement("CreateEmptyGroupButton12008"); + App.Tap("CreateEmptyGroupButton12008"); + App.WaitForElement("GroupHeader12008EmptyGroup"); + App.WaitForElement("Item12008ItemA1"); + App.DragAndDrop("Item12008ItemA1", "GroupHeader12008EmptyGroup"); + App.WaitForElement("GroupCount12008EmptyGroup"); + var groupCountLabel = App.WaitForElement("GroupCount12008EmptyGroup"); + var countText = groupCountLabel.GetText(); + Assert.That(countText, Is.EqualTo("Count: 1"), "Item was not moved into the empty group"); + } } #endif