Skip to content

Commit e615b68

Browse files
Dhivya-SF4094rmarinho
authored andcommitted
Added test case for CollectionView with grouped data
1 parent 786d497 commit e615b68

File tree

3 files changed

+318
-0
lines changed

3 files changed

+318
-0
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<ContentPage
3+
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
4+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
5+
x:Class="Maui.Controls.Sample.Issues.Issue27797">
6+
<Grid>
7+
<CollectionView IsGrouped="True"
8+
ItemsSource="{Binding GroupedWorkItems}">
9+
<CollectionView.GroupHeaderTemplate>
10+
<DataTemplate>
11+
<Grid BackgroundColor="LightGray">
12+
<Label
13+
Margin="10"
14+
FontAttributes="Bold"
15+
Text="{Binding GroupDescription}"
16+
VerticalOptions="Center"/>
17+
</Grid>
18+
</DataTemplate>
19+
</CollectionView.GroupHeaderTemplate>
20+
<CollectionView.ItemTemplate>
21+
<DataTemplate>
22+
<Grid ColumnDefinitions="*,Auto,Auto">
23+
<Label Margin="20,5"
24+
Text="{Binding Description}">
25+
<Label.GestureRecognizers>
26+
<TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped"/>
27+
</Label.GestureRecognizers>
28+
</Label>
29+
</Grid>
30+
</DataTemplate>
31+
</CollectionView.ItemTemplate>
32+
</CollectionView>
33+
</Grid>
34+
</ContentPage>
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
using System.Collections.ObjectModel;
2+
3+
namespace Maui.Controls.Sample.Issues;
4+
[Issue(IssueTracker.Github, 27797, "CollectionView with grouped data crashes on iOS when the groups change", PlatformAffected.iOS)]
5+
public class Issue27797NavigationPage : NavigationPage
6+
{
7+
public Issue27797NavigationPage() : base(new Issue27797()) { }
8+
}
9+
10+
public partial class Issue27797 : ContentPage
11+
{
12+
public Issue27797()
13+
{
14+
InitializeComponent();
15+
BindingContext = new MainViewModel();
16+
}
17+
18+
private void TapGestureRecognizer_Tapped(object sender, TappedEventArgs e)
19+
{
20+
DetailPage detailPage = new();
21+
var workItemViewModel = (sender as VisualElement).BindingContext;
22+
detailPage.BindingContext = workItemViewModel;
23+
Navigation.PushAsync(detailPage);
24+
}
25+
26+
public class DetailPage : ContentPage
27+
{
28+
public DetailPage()
29+
{
30+
Title = "DetailPage";
31+
BindingContext = new WorkItemViewModel();
32+
33+
var statusLabel = new Label
34+
{
35+
FontAttributes = FontAttributes.Bold,
36+
FontSize = 20,
37+
HorizontalOptions = LayoutOptions.Center
38+
};
39+
statusLabel.SetBinding(Label.TextProperty, "Status");
40+
41+
var descriptionLabel = new Label
42+
{
43+
FontSize = 16,
44+
HorizontalOptions = LayoutOptions.Center
45+
};
46+
descriptionLabel.SetBinding(Label.TextProperty, "Description");
47+
48+
var todoButton = new Button
49+
{
50+
Text = "TODO",
51+
AutomationId = "TODO"
52+
};
53+
todoButton.SetBinding(Button.CommandProperty, "ChangeStatusCommand");
54+
todoButton.SetBinding(Button.CommandParameterProperty, new Binding { Source = "TODO" });
55+
56+
var activeButton = new Button
57+
{
58+
Text = "ACTIVE",
59+
AutomationId = "ACTIVE"
60+
};
61+
activeButton.SetBinding(Button.CommandProperty, "ChangeStatusCommand");
62+
activeButton.SetBinding(Button.CommandParameterProperty, new Binding { Source = "ACTIVE" });
63+
64+
var doneButton = new Button
65+
{
66+
Text = "DONE",
67+
AutomationId = "DONE",
68+
};
69+
doneButton.SetBinding(Button.CommandProperty, "ChangeStatusCommand");
70+
doneButton.SetBinding(Button.CommandParameterProperty, new Binding { Source = "DONE" });
71+
72+
var buttonLayout = new HorizontalStackLayout
73+
{
74+
HorizontalOptions = LayoutOptions.Center,
75+
Spacing = 5,
76+
Children = { todoButton, activeButton, doneButton }
77+
};
78+
79+
var mainLayout = new VerticalStackLayout
80+
{
81+
Spacing = 10,
82+
Children = { statusLabel, descriptionLabel, buttonLayout }
83+
};
84+
85+
Content = mainLayout;
86+
}
87+
}
88+
89+
public class MainViewModel : BindableObject
90+
{
91+
private List<WorkItemViewModel> _workItems = new();
92+
93+
private ObservableCollection<WorkItemGroupViewModel> _groupedWorkItems = new();
94+
public ObservableCollection<WorkItemGroupViewModel> GroupedWorkItems
95+
{
96+
get => _groupedWorkItems;
97+
set { _groupedWorkItems = value; OnPropertyChanged(); }
98+
}
99+
100+
static readonly string StatusTODO = "TODO";
101+
static readonly string StatusACTIVE = "ACTIVE";
102+
static readonly string StatusDONE = "DONE";
103+
104+
WorkItemGroupViewModel _todoGroup = new(StatusTODO);
105+
WorkItemGroupViewModel _activeGroup = new(StatusACTIVE);
106+
WorkItemGroupViewModel _doneGroup = new(StatusDONE);
107+
108+
public MainViewModel()
109+
{
110+
CreateWorkItemData();
111+
UpdateGroupedWorkItems();
112+
}
113+
114+
private void UpdateGroupedWorkItems()
115+
{
116+
foreach (var item in _workItems)
117+
{
118+
if (item.Status == StatusTODO && !_todoGroup.Contains(item))
119+
{
120+
_todoGroup.Add(item);
121+
}
122+
else if (item.Status == StatusACTIVE && !_activeGroup.Contains(item))
123+
{
124+
// move any existing active item back to TODO
125+
if (_activeGroup.Any())
126+
{
127+
var currentActiveItem = _activeGroup[0];
128+
_activeGroup.Remove(currentActiveItem);
129+
currentActiveItem.Status = StatusTODO;
130+
_todoGroup.Add(currentActiveItem);
131+
}
132+
_activeGroup.Add(item);
133+
}
134+
else if (item.Status == StatusDONE && !_doneGroup.Contains(item))
135+
{
136+
_doneGroup.Add(item);
137+
}
138+
}
139+
140+
if (_todoGroup.Any())
141+
{
142+
if (!GroupedWorkItems.Contains(_todoGroup))
143+
GroupedWorkItems.Add(_todoGroup);
144+
}
145+
else if (GroupedWorkItems.Contains(_todoGroup))
146+
{
147+
GroupedWorkItems.Remove(_todoGroup);
148+
}
149+
150+
if (_activeGroup.Any())
151+
{
152+
// ACTIVE always at top
153+
if (!GroupedWorkItems.Contains(_activeGroup))
154+
GroupedWorkItems.Insert(0, _activeGroup);
155+
}
156+
else if (GroupedWorkItems.Contains(_activeGroup))
157+
{
158+
GroupedWorkItems.Remove(_activeGroup);
159+
}
160+
161+
if (_doneGroup.Any())
162+
{
163+
if (!GroupedWorkItems.Contains(_doneGroup))
164+
GroupedWorkItems.Add(_doneGroup);
165+
}
166+
else if (GroupedWorkItems.Contains(_doneGroup))
167+
{
168+
GroupedWorkItems.Remove(_doneGroup);
169+
}
170+
}
171+
172+
private bool _statusUpdatesInProgress = false;
173+
private void WorkItem_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
174+
{
175+
if (_statusUpdatesInProgress)
176+
return;
177+
178+
if (sender is not WorkItemViewModel workItem)
179+
return;
180+
181+
if (e.PropertyName == nameof(WorkItemViewModel.Status))
182+
{
183+
_statusUpdatesInProgress = true;
184+
RemoveItemFromOldGroup(workItem);
185+
186+
UpdateGroupedWorkItems();
187+
_statusUpdatesInProgress = false;
188+
}
189+
}
190+
191+
private void RemoveItemFromOldGroup(WorkItemViewModel workItem)
192+
{
193+
if (_todoGroup.Contains(workItem))
194+
_todoGroup.Remove(workItem);
195+
196+
if (_activeGroup.Contains(workItem))
197+
_activeGroup.Remove(workItem);
198+
199+
if (_doneGroup.Contains(workItem))
200+
_doneGroup.Remove(workItem);
201+
}
202+
203+
private void CreateWorkItemData()
204+
{
205+
var cleanHouse = new WorkItemViewModel
206+
{
207+
Description = "CleanHouse",
208+
Status = StatusDONE
209+
};
210+
cleanHouse.PropertyChanged += WorkItem_PropertyChanged;
211+
_workItems.Add(cleanHouse);
212+
213+
var doLaundry = new WorkItemViewModel
214+
{
215+
Description = "DoLaundry",
216+
Status = StatusDONE
217+
};
218+
doLaundry.PropertyChanged += WorkItem_PropertyChanged;
219+
_workItems.Add(doLaundry);
220+
221+
var mowLawn = new WorkItemViewModel
222+
{
223+
Description = "MowLawn",
224+
Status = StatusTODO
225+
};
226+
mowLawn.PropertyChanged += WorkItem_PropertyChanged;
227+
_workItems.Add(mowLawn);
228+
229+
}
230+
}
231+
232+
public class WorkItemGroupViewModel : ObservableCollection<WorkItemViewModel>
233+
{
234+
public WorkItemGroupViewModel(string groupDescription)
235+
{
236+
GroupDescription = groupDescription;
237+
}
238+
239+
public string GroupDescription { get; }
240+
}
241+
242+
public class WorkItemViewModel : BindableObject
243+
{
244+
private string _description;
245+
public string Description
246+
{
247+
get => _description;
248+
set { _description = value; OnPropertyChanged(); }
249+
}
250+
251+
private string _status;
252+
public string Status
253+
{
254+
get => _status;
255+
set { _status = value; OnPropertyChanged(); }
256+
}
257+
public Command<string> ChangeStatusCommand => new((newStatus) => Status = newStatus);
258+
}
259+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using NUnit.Framework;
2+
using UITest.Appium;
3+
using UITest.Core;
4+
5+
namespace Microsoft.Maui.TestCases.Tests.Issues;
6+
public class Issue27797 : _IssuesUITest
7+
{
8+
public Issue27797(TestDevice testDevice) : base(testDevice)
9+
{
10+
}
11+
12+
public override string Issue => "CollectionView with grouped data crashes on iOS when the groups change";
13+
14+
[Test]
15+
[Category(UITestCategories.CollectionView)]
16+
public void AppShouldNotCrashWhenModifyingCollectionView()
17+
{
18+
App.WaitForElement("CleanHouse");
19+
App.Click("MowLawn");
20+
App.WaitForElement("ACTIVE");
21+
App.Click("ACTIVE");
22+
App.Click("TODO");
23+
App.Back();
24+
}
25+
}

0 commit comments

Comments
 (0)