Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
eb4a629
fix for 12008
SuthiYuvaraj Sep 11, 2025
fc1ed97
Update Issue12008.cs
SuthiYuvaraj Sep 11, 2025
3c110bc
sample changes
SuthiYuvaraj Sep 11, 2025
b5556bf
commit for testcase changes
SuthiYuvaraj Sep 11, 2025
5f61f4c
fix for ios
SuthiYuvaraj Sep 16, 2025
189d300
Update ReorderableItemsViewDelegator.cs
SuthiYuvaraj Sep 16, 2025
b77d9ca
Update ReorderableItemsViewDelegator.cs
SuthiYuvaraj Sep 22, 2025
3b83122
CollectionView2 changes
SuthiYuvaraj Sep 26, 2025
5823e71
Update Issue12008.cs
SuthiYuvaraj Oct 10, 2025
b245a0e
Update Issue12008.cs
SuthiYuvaraj Oct 10, 2025
0842820
Commit for review changes
SuthiYuvaraj Feb 20, 2026
6717d1c
Add GitHub Actions workflow to run evaluate-pr-tests via Copilot CLI …
PureWeen Mar 25, 2026
720a9d4
Allow fork PRs to auto-trigger evaluate-pr-tests workflow (#34655)
PureWeen Mar 25, 2026
53e6575
Add regression test for #34713: Binding with Converter and x:DataType…
StephaneDelcroix Mar 28, 2026
23e0a2a
Add merge flow: net11.0 → next release branch (#34626)
PureWeen Mar 30, 2026
cdead27
Fix Flyout memory leak (#34485)
pictos Apr 1, 2026
59b36c2
[spec] Add `maui device list` command spec to CLI design doc (#34277)
rmarinho Apr 2, 2026
148b9aa
Fix x:Key values not escaped in source-generated C# string literals (…
StephaneDelcroix Apr 2, 2026
74a6940
[AI] Sample: Detail page, semantic search, streaming, and UI polish (…
mattleibow Apr 2, 2026
794a9fa
Add standalone code-review skill with maintainer-sourced review rules…
PureWeen Apr 2, 2026
64871a0
Merge branch 'main' into fix-12008
SuthiYuvaraj Apr 6, 2026
1fcc87d
Merge branch 'inflight/current' into fix-12008
kubaflo Apr 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand All @@ -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;
}
Expand Down
274 changes: 165 additions & 109 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue12008.cs
Original file line number Diff line number Diff line change
@@ -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<Issue12008Group> 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<Issue12008Group>
this.BindingContext = new Issue12008ViewModel();
var grid = new Grid
{
new Issue12008Group("Group A", new ObservableCollection<Item>
{
new Item("Item A1"),
new Item("Item A2"),
new Item("Item A3")
}),
new Issue12008Group("Group B", new ObservableCollection<Item>
RowDefinitions = new RowDefinitionCollection
{
new Item("Item B1"),
new Item("Item B2")
}),
new Issue12008Group("Empty Group", new ObservableCollection<Item>()), // Empty group
new Issue12008Group("Group C", new ObservableCollection<Item>
{
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<Issue12008Group> _groupedItems;

void OnReorderCompleted(object sender, EventArgs e)
public ObservableCollection<Issue12008Group> 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<Item>
public Issue12008ViewModel()
{
public string Name { get; set; }
InitializeData();
}

public Issue12008Group(string name, ObservableCollection<Item> items)
{
Name = name;
private void InitializeData()
{
GroupedItems = new ObservableCollection<Issue12008Group>
{
new Issue12008Group("GroupA", new List<Issue12008Item>
{
new Issue12008Item("ItemA1"),
new Issue12008Item("ItemA2"),
new Issue12008Item("ItemA3")
}),
new Issue12008Group("GroupB", new List<Issue12008Item>
{
new Issue12008Item("ItemB1"),
new Issue12008Item("ItemB2")
}),
new Issue12008Group("GroupC", new List<Issue12008Item>
{
new Issue12008Item("ItemC1")
})
};
}

foreach (var item in items)
Add(item);
}
public void CreateEmptyGroup()
{
var emptyGroup = new Issue12008Group("EmptyGroup", new List<Issue12008Item>());
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<Issue12008Item>
{
public string GroupName { get; }

public Issue12008Group(string groupName, IEnumerable<Issue12008Item> items) : base(items)
{
GroupName = groupName;
}
}

public class Issue12008Item
{
public string Name { get; }

public Issue12008Item(string name)
{
Name = name;
}
}

Loading
Loading