Skip to content
This repository was archived by the owner on May 1, 2024. It is now read-only.

Commit 50612ef

Browse files
jsuarezruizsamhouts
authored andcommitted
[CollectionView] Fixed crash modifying an ObservableCollection from non UI thread (#8568)
* Fixed crash modifying an ObservableCollection from non UI thread * Fixed mistake on UWP * Replaced SynchronizationContext by Device.BeginInvokeOnMainThread * Removed unnecessary namespaces * Check IsInvokeRequired in CollectionChanged fixes #8392
1 parent c036900 commit 50612ef

File tree

8 files changed

+224
-8
lines changed

8 files changed

+224
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
using System.Collections.ObjectModel;
2+
using System.Threading.Tasks;
3+
using System.Windows.Input;
4+
using Xamarin.Forms.CustomAttributes;
5+
using Xamarin.Forms.Internals;
6+
7+
#if UITEST
8+
using Xamarin.Forms.Core.UITests;
9+
using Xamarin.UITest;
10+
using NUnit.Framework;
11+
#endif
12+
13+
namespace Xamarin.Forms.Controls.Issues
14+
{
15+
[Preserve(AllMembers = true)]
16+
[Issue(IssueTracker.Bugzilla, 8392, "[Bug] CollectionView does not marshal ObservableCollection.CollectionChanged to MainThread")]
17+
public class Issue8392 : TestContentPage
18+
{
19+
public Issue8392()
20+
{
21+
Title = "Issue 8392";
22+
23+
var collectionView = new CollectionView
24+
{
25+
ItemTemplate = new Issue8392DataTemplate()
26+
};
27+
28+
collectionView.SetBinding(ItemsView.ItemsSourceProperty, nameof(Issue8392ViewModel.DataList));
29+
30+
var refreshView = new RefreshView
31+
{
32+
Content = collectionView
33+
};
34+
35+
refreshView.SetBinding(RefreshView.IsRefreshingProperty, nameof(Issue8392ViewModel.IsRefreshing));
36+
refreshView.SetBinding(RefreshView.CommandProperty, nameof(Issue8392ViewModel.PullToRefreshCommand));
37+
38+
Content = refreshView;
39+
40+
BindingContext = new Issue8392ViewModel();
41+
}
42+
43+
protected override void Init()
44+
{
45+
46+
}
47+
48+
protected override void OnAppearing()
49+
{
50+
base.OnAppearing();
51+
52+
if (Content is RefreshView refreshView
53+
&& refreshView.Content is CollectionView)
54+
{
55+
refreshView.IsRefreshing = true;
56+
}
57+
}
58+
}
59+
60+
[Preserve(AllMembers = true)]
61+
public class Issue8392DataTemplate : DataTemplate
62+
{
63+
public Issue8392DataTemplate() : base(CreateTemplate)
64+
{
65+
}
66+
67+
static Layout CreateTemplate()
68+
{
69+
var numberLabel = new Label
70+
{
71+
FontSize = 14,
72+
HorizontalOptions = LayoutOptions.Start,
73+
VerticalOptions = LayoutOptions.Center,
74+
HorizontalTextAlignment = TextAlignment.Start,
75+
VerticalTextAlignment = TextAlignment.Center,
76+
};
77+
numberLabel.SetBinding(Label.TextProperty, nameof(Issue8392Model.Number));
78+
79+
var grid = new Grid
80+
{
81+
RowDefinitions =
82+
{
83+
new RowDefinition { Height = new GridLength(20,GridUnitType.Absolute) }
84+
},
85+
ColumnDefinitions =
86+
{
87+
new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }
88+
}
89+
};
90+
91+
grid.Children.Add(numberLabel, 0, 0);
92+
93+
return grid;
94+
}
95+
}
96+
97+
public class Issue8392Model
98+
{
99+
public Issue8392Model(int number) => Number = number;
100+
101+
public int Number { get; }
102+
}
103+
104+
[Preserve(AllMembers = true)]
105+
public class Issue8392ViewModel : BindableObject
106+
{
107+
bool _isRefreshing;
108+
109+
public ICommand PullToRefreshCommand => new Command(async () => await ExecutePullToRefreshCommand());
110+
111+
public bool IsRefreshing
112+
{
113+
get { return _isRefreshing; }
114+
set
115+
{
116+
_isRefreshing = value;
117+
OnPropertyChanged();
118+
}
119+
}
120+
121+
public ObservableCollection<Issue8392Model> DataList { get; } = new ObservableCollection<Issue8392Model>();
122+
123+
async Task ExecutePullToRefreshCommand()
124+
{
125+
DataList.Clear();
126+
127+
try
128+
{
129+
// If the items load without exception, the test has passed.
130+
for (int i = 0; i < 30; i++)
131+
{
132+
await Task.Delay(200).ConfigureAwait(false);
133+
DataList.Add(new Issue8392Model(i + 1));
134+
}
135+
}
136+
finally
137+
{
138+
IsRefreshing = false;
139+
}
140+
}
141+
}
142+
}

Xamarin.Forms.Controls.Issues/Xamarin.Forms.Controls.Issues.Shared/Xamarin.Forms.Controls.Issues.Shared.projitems

+1
Original file line numberDiff line numberDiff line change
@@ -1143,6 +1143,7 @@
11431143
</Compile>
11441144
<Compile Include="$(MSBuildThisFileDirectory)Issue7898.cs" />
11451145
<Compile Include="$(MSBuildThisFileDirectory)Issue7510.cs" />
1146+
<Compile Include="$(MSBuildThisFileDirectory)Issue8392.cs" />
11461147
<Compile Include="$(MSBuildThisFileDirectory)Issue8672.cs" />
11471148
<Compile Include="$(MSBuildThisFileDirectory)Issue8326.xaml.cs" />
11481149
<Compile Include="$(MSBuildThisFileDirectory)Issue8449.xaml.cs" />

Xamarin.Forms.Platform.Android/CollectionView/ObservableGroupedSource.cs

+17-6
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,10 @@ internal class ObservableGroupedSource : IGroupableItemsViewSource, ICollectionC
1010
readonly ICollectionChangedNotifier _notifier;
1111
readonly IList _groupSource;
1212
List<IItemsViewSource> _groups = new List<IItemsViewSource>();
13+
readonly bool _hasGroupHeaders;
14+
readonly bool _hasGroupFooters;
1315
bool _disposed;
1416

15-
bool _hasGroupHeaders;
16-
bool _hasGroupFooters;
17-
1817
public int Count
1918
{
2019
get
@@ -42,14 +41,14 @@ public ObservableGroupedSource(GroupableItemsView groupableItemsView, ICollectio
4241
_notifier = notifier;
4342
_groupSource = groupSource as IList ?? new ListSource(groupSource);
4443

44+
_hasGroupFooters = groupableItemsView.GroupFooterTemplate != null;
45+
_hasGroupHeaders = groupableItemsView.GroupHeaderTemplate != null;
46+
4547
if (_groupSource is INotifyCollectionChanged incc)
4648
{
4749
incc.CollectionChanged += CollectionChanged;
4850
}
4951

50-
_hasGroupFooters = groupableItemsView.GroupFooterTemplate != null;
51-
_hasGroupHeaders = groupableItemsView.GroupHeaderTemplate != null;
52-
5352
UpdateGroupTracking();
5453
}
5554

@@ -231,6 +230,18 @@ void ClearGroupTracking()
231230
}
232231

233232
void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
233+
{
234+
if (Device.IsInvokeRequired)
235+
{
236+
Device.BeginInvokeOnMainThread(() => CollectionChanged(args));
237+
}
238+
else
239+
{
240+
CollectionChanged(args);
241+
}
242+
}
243+
244+
void CollectionChanged(NotifyCollectionChangedEventArgs args)
234245
{
235246
switch (args.Action)
236247
{

Xamarin.Forms.Platform.Android/CollectionView/ObservableItemsSource.cs

+13
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public ObservableItemsSource(IList itemSource, ICollectionChangedNotifier notifi
1414
{
1515
_itemsSource = itemSource;
1616
_notifier = notifier;
17+
1718
((INotifyCollectionChanged)itemSource).CollectionChanged += CollectionChanged;
1819
}
1920

@@ -81,6 +82,18 @@ int AdjustPositionForHeader(int position)
8182
}
8283

8384
void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
85+
{
86+
if (Device.IsInvokeRequired)
87+
{
88+
Device.BeginInvokeOnMainThread(() => CollectionChanged(args));
89+
}
90+
else
91+
{
92+
CollectionChanged(args);
93+
}
94+
}
95+
96+
void CollectionChanged(NotifyCollectionChangedEventArgs args)
8497
{
8598
switch (args.Action)
8699
{

Xamarin.Forms.Platform.UAP/CollectionView/GroupedItemTemplateCollection.cs

+13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Collections;
22
using System.Collections.ObjectModel;
33
using System.Collections.Specialized;
4+
using System.Threading;
45

56
namespace Xamarin.Forms.Platform.UWP
67
{
@@ -50,6 +51,18 @@ GroupTemplateContext CreateGroupTemplateContext(object group)
5051
}
5152

5253
void GroupsChanged(object sender, NotifyCollectionChangedEventArgs args)
54+
{
55+
if (Device.IsInvokeRequired)
56+
{
57+
Device.BeginInvokeOnMainThread(() => GroupsChanged(args));
58+
}
59+
else
60+
{
61+
GroupsChanged(args);
62+
}
63+
}
64+
65+
void GroupsChanged(NotifyCollectionChangedEventArgs args)
5366
{
5467
switch (args.Action)
5568
{

Xamarin.Forms.Platform.UAP/CollectionView/ObservableItemTemplateCollection.cs

+13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections;
33
using System.Collections.ObjectModel;
44
using System.Collections.Specialized;
5+
using System.Threading;
56

67
namespace Xamarin.Forms.Platform.UWP
78
{
@@ -55,6 +56,18 @@ public void CleanUp()
5556
}
5657

5758
void InnerCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
59+
{
60+
if (Device.IsInvokeRequired)
61+
{
62+
Device.BeginInvokeOnMainThread(() => InnerCollectionChanged(args));
63+
}
64+
else
65+
{
66+
InnerCollectionChanged(args);
67+
}
68+
}
69+
70+
void InnerCollectionChanged(NotifyCollectionChangedEventArgs args)
5871
{
5972
switch (args.Action)
6073
{

Xamarin.Forms.Platform.iOS/CollectionView/ObservableGroupedSource.cs

+13-2
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,13 @@
44
using System.Collections.Specialized;
55
using Foundation;
66
using UIKit;
7-
using Xamarin.Forms.Internals;
87

98
namespace Xamarin.Forms.Platform.iOS
109
{
1110
internal class ObservableGroupedSource : IItemsViewSource
1211
{
1312
readonly UICollectionView _collectionView;
14-
UICollectionViewController _collectionViewController;
13+
readonly UICollectionViewController _collectionViewController;
1514
readonly IList _groupSource;
1615
bool _disposed;
1716
List<ObservableItemsSource> _groups = new List<ObservableItemsSource>();
@@ -129,6 +128,18 @@ void ResetGroupTracking()
129128
}
130129

131130
void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
131+
{
132+
if (Device.IsInvokeRequired)
133+
{
134+
Device.BeginInvokeOnMainThread(() => CollectionChanged(args));
135+
}
136+
else
137+
{
138+
CollectionChanged(args);
139+
}
140+
}
141+
142+
void CollectionChanged(NotifyCollectionChangedEventArgs args)
132143
{
133144
switch (args.Action)
134145
{

Xamarin.Forms.Platform.iOS/CollectionView/ObservableItemsSource.cs

+12
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,18 @@ public object this[NSIndexPath indexPath]
9393
}
9494

9595
void CollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
96+
{
97+
if (Device.IsInvokeRequired)
98+
{
99+
Device.BeginInvokeOnMainThread(() => CollectionChanged(args));
100+
}
101+
else
102+
{
103+
CollectionChanged(args);
104+
}
105+
}
106+
107+
void CollectionChanged(NotifyCollectionChangedEventArgs args)
96108
{
97109
switch (args.Action)
98110
{

0 commit comments

Comments
 (0)