Skip to content

Commit 47d38e3

Browse files
authored
feat: ConcurrentObservableCollection and extension methods for IDatasyncQueryable (#56)
1 parent d88ca21 commit 47d38e3

File tree

4 files changed

+739
-0
lines changed

4 files changed

+739
-0
lines changed
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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

Comments
 (0)