1
+ // Licensed to the .NET Foundation under one or more agreements.
2
+ // The .NET Foundation licenses this file to you under the MIT license.
3
+ // See the LICENSE file in the project root for more information.
4
+
5
+ using System . Collections . ObjectModel ;
6
+ using System . Collections . Specialized ;
7
+ using System . ComponentModel ;
8
+ using System . Diagnostics . CodeAnalysis ;
9
+
10
+ namespace CommunityToolkit . Datasync . Client ;
11
+
12
+ /// <summary>
13
+ /// A thread-safe implementation of the <see cref="ObservableCollection{T}"/> that allows us
14
+ /// to add/replace/remove ranges without notifying more than once.
15
+ /// </summary>
16
+ /// <typeparam name="T"></typeparam>
17
+ public class ConcurrentObservableCollection < T > : ObservableCollection < T >
18
+ {
19
+ private readonly SynchronizationContext context = SynchronizationContext . Current ! ;
20
+ private bool suppressNotification = false ;
21
+
22
+ /// <summary>
23
+ /// Creates a new (empty) collection.
24
+ /// </summary>
25
+ public ConcurrentObservableCollection ( ) : base ( )
26
+ {
27
+ }
28
+
29
+ /// <summary>
30
+ /// Creates a new collection seeded with the provided information.
31
+ /// </summary>
32
+ /// <param name="list">The information to be used for seeding the collection.</param>
33
+ public ConcurrentObservableCollection ( IEnumerable < T > list ) : base ( list )
34
+ {
35
+ }
36
+
37
+ /// <summary>
38
+ /// Replaces the contents of the observable collection with new contents.
39
+ /// </summary>
40
+ /// <param name="collection">The new collection.</param>
41
+ public void ReplaceAll ( IEnumerable < T > collection )
42
+ {
43
+ ArgumentNullException . ThrowIfNull ( collection , nameof ( collection ) ) ;
44
+ try
45
+ {
46
+ this . suppressNotification = true ;
47
+ Clear ( ) ;
48
+ foreach ( T ? item in collection )
49
+ {
50
+ Add ( item ) ;
51
+ }
52
+ }
53
+ finally
54
+ {
55
+ this . suppressNotification = false ;
56
+ }
57
+
58
+ OnCollectionChanged ( new NotifyCollectionChangedEventArgs ( NotifyCollectionChangedAction . Reset ) ) ;
59
+ }
60
+
61
+ /// <summary>
62
+ /// Adds a collection to the existing collection.
63
+ /// </summary>
64
+ /// <param name="collection">The collection of records to add.</param>
65
+ /// <returns><c>true</c> if any records were added; <c>false</c> otherwise.</returns>
66
+ public bool AddRange ( IEnumerable < T > collection )
67
+ {
68
+ ArgumentNullException . ThrowIfNull ( collection , nameof ( collection ) ) ;
69
+ bool changed = false ;
70
+ try
71
+ {
72
+ this . suppressNotification = true ;
73
+ foreach ( T ? item in collection )
74
+ {
75
+ Add ( item ) ;
76
+ changed = true ;
77
+ }
78
+ }
79
+ finally
80
+ {
81
+ this . suppressNotification = false ;
82
+ }
83
+
84
+ if ( changed )
85
+ {
86
+ OnCollectionChanged ( new NotifyCollectionChangedEventArgs ( NotifyCollectionChangedAction . Reset ) ) ;
87
+ }
88
+
89
+ return changed ;
90
+ }
91
+
92
+ /// <summary>
93
+ /// Adds an item within a collection only if there are no items identified by the match function.
94
+ /// </summary>
95
+ /// <param name="match">The match function.</param>
96
+ /// <param name="item">The item to add.</param>
97
+ /// <returns><c>true</c> if the item was added, <c>false</c> otherwise.</returns>
98
+ public bool AddIfMissing ( Func < T , bool > match , T item )
99
+ {
100
+ ArgumentNullException . ThrowIfNull ( match , nameof ( match ) ) ;
101
+ ArgumentNullException . ThrowIfNull ( item , nameof ( item ) ) ;
102
+ if ( ! this . Any ( match ) )
103
+ {
104
+ Add ( item ) ;
105
+ return true ;
106
+ }
107
+
108
+ return false ;
109
+ }
110
+
111
+ /// <summary>
112
+ /// Removes items within the collection based on a match function.
113
+ /// </summary>
114
+ /// <param name="match">The match predicate.</param>
115
+ /// <returns><c>true</c> if an item was removed, <c>false</c> otherwise.</returns>
116
+ public bool RemoveIf ( Func < T , bool > match )
117
+ {
118
+ ArgumentNullException . ThrowIfNull ( match , nameof ( match ) ) ;
119
+ T [ ] itemsToRemove = this . Where ( match ) . ToArray ( ) ;
120
+ foreach ( T ? item in itemsToRemove )
121
+ {
122
+ int idx = IndexOf ( item ) ;
123
+ RemoveAt ( idx ) ;
124
+ }
125
+
126
+ return itemsToRemove . Length > 0 ;
127
+ }
128
+
129
+ /// <summary>
130
+ /// Replaced items within the collection with a (single) replacement based on a match function.
131
+ /// </summary>
132
+ /// <param name="match">The match predicate.</param>
133
+ /// <param name="replacement">The replacement item.</param>
134
+ /// <returns><c>true</c> if an item was replaced, <c>false</c> otherwise.</returns>
135
+ public bool ReplaceIf ( Func < T , bool > match , T replacement )
136
+ {
137
+ ArgumentNullException . ThrowIfNull ( match , nameof ( match ) ) ;
138
+ ArgumentNullException . ThrowIfNull ( replacement , nameof ( replacement ) ) ;
139
+ T [ ] itemsToReplace = this . Where ( match ) . ToArray ( ) ;
140
+ foreach ( T ? item in itemsToReplace )
141
+ {
142
+ int idx = IndexOf ( item ) ;
143
+ this [ idx ] = replacement ;
144
+ }
145
+
146
+ return itemsToReplace . Length > 0 ;
147
+ }
148
+
149
+ /// <summary>
150
+ /// Event trigger to indicate that the collection has changed in a thread-safe way.
151
+ /// </summary>
152
+ /// <param name="e">The event arguments</param>
153
+ protected override void OnCollectionChanged ( NotifyCollectionChangedEventArgs e )
154
+ {
155
+ if ( SynchronizationContext . Current == this . context )
156
+ {
157
+ RaiseCollectionChanged ( e ) ;
158
+ }
159
+ else
160
+ {
161
+ this . context . Send ( RaiseCollectionChanged , e ) ;
162
+ }
163
+ }
164
+
165
+ /// <summary>
166
+ /// Event trigger to indicate that a property has changed in a thread-safe way.
167
+ /// </summary>
168
+ /// <param name="e">The event arguments</param>
169
+ protected override void OnPropertyChanged ( PropertyChangedEventArgs e )
170
+ {
171
+ if ( SynchronizationContext . Current == this . context )
172
+ {
173
+ RaisePropertyChanged ( e ) ;
174
+ }
175
+ else
176
+ {
177
+ this . context . Send ( RaisePropertyChanged , e ) ;
178
+ }
179
+ }
180
+
181
+ [ ExcludeFromCodeCoverage ]
182
+ private void RaiseCollectionChanged ( object ? param )
183
+ {
184
+ if ( ! this . suppressNotification )
185
+ {
186
+ base . OnCollectionChanged ( ( NotifyCollectionChangedEventArgs ) param ! ) ;
187
+ }
188
+ }
189
+
190
+ [ ExcludeFromCodeCoverage ]
191
+ private void RaisePropertyChanged ( object ? param )
192
+ {
193
+ base . OnPropertyChanged ( ( PropertyChangedEventArgs ) param ! ) ;
194
+ }
195
+ }
0 commit comments