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+ }
0 commit comments