Thread safely ObservableCollection

This commit is contained in:
Dan Clark
2024-12-02 09:17:38 +00:00
parent fd1b7525d3
commit 1ac2db25e0
6 changed files with 114 additions and 57 deletions
+3
View File
@@ -6,6 +6,9 @@ namespace Toolkit.Avalonia;
public class AvaloniaDispatcher :
IDispatcher
{
public bool CheckAccess() =>
Dispatcher.UIThread.CheckAccess();
public async Task Invoke(Action action) =>
await Dispatcher.UIThread.InvokeAsync(action);
}
+2
View File
@@ -3,4 +3,6 @@
public interface IDispatcher
{
Task Invoke(Action action);
bool CheckAccess();
}
+1 -1
View File
@@ -2,7 +2,7 @@
namespace Toolkit.Foundation;
public interface IScopeServiceFactory<TService>
public interface IServiceScopeFactory<TService>
{
(IServiceScope, TService) Create(params object?[] parameters);
}
+104 -54
View File
@@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.DependencyInjection;
using System.Collections;
using System.Collections.Specialized;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
namespace Toolkit.Foundation;
@@ -32,6 +33,8 @@ public abstract partial class ObservableCollection<TViewModel> :
{
private readonly System.Collections.ObjectModel.ObservableCollection<TViewModel> collection = [];
private readonly Lock syncLock = new();
private readonly IDispatcher dispatcher;
private readonly Dictionary<string, object> trackedProperties = [];
@@ -95,8 +98,27 @@ public abstract partial class ObservableCollection<TViewModel> :
public TViewModel this[int index]
{
get => collection[index];
set => SetItem(index, value);
get
{
lock (syncLock)
{
return collection[index];
}
}
set
{
if (dispatcher.CheckAccess())
{
lock (syncLock)
{
SetItem(index, value);
}
}
else
{
dispatcher.Invoke(() => SetItem(index, value));
}
}
}
object? IList.this[int index]
@@ -129,10 +151,18 @@ public abstract partial class ObservableCollection<TViewModel> :
public void Add(TViewModel item)
{
int index = collection.Count;
InsertItem(index, item);
UpdateSelection(item);
if (dispatcher.CheckAccess())
{
lock (syncLock)
{
InsertItem(collection.Count, item);
UpdateSelection(item);
}
}
else
{
dispatcher.Invoke(() => Add(item));
}
}
public void Add(object item)
@@ -263,36 +293,32 @@ public abstract partial class ObservableCollection<TViewModel> :
public bool Move(int oldIndex, int newIndex)
{
if (oldIndex < 0)
if (dispatcher.CheckAccess())
{
return false;
}
TViewModel item = this[oldIndex];
bool moveSelection = false;
if (item is ISelectable oldSelection)
{
if (oldSelection.IsSelected)
lock (syncLock)
{
moveSelection = true;
SelectedItem = default;
if (oldIndex < 0 || newIndex < 0 || oldIndex >= Count || newIndex >= Count)
{
return false;
}
TViewModel item = collection[oldIndex];
collection.Move(oldIndex, newIndex);
if (item is ISelectable selectable && selectable.IsSelected)
{
dispatcher.Invoke(() => SelectedItem = item);
}
return true;
}
}
RemoveItem(oldIndex);
InsertItem(newIndex, item);
if (moveSelection)
else
{
if (item is ISelectable newSelection)
{
newSelection.IsSelected = true;
dispatcher.Invoke(() => SelectedItem = item);
}
bool result = false;
dispatcher.Invoke(() => result = Move(oldIndex, newIndex));
return result;
}
return true;
}
public bool Move(int index, TViewModel item)
@@ -368,26 +394,38 @@ public abstract partial class ObservableCollection<TViewModel> :
public bool Remove(TViewModel item)
{
int index = collection.IndexOf(item);
if (index < 0)
if (dispatcher.CheckAccess())
{
return false;
lock (syncLock)
{
int index = collection.IndexOf(item);
if (index < 0)
{
return false;
}
Disposer.Dispose(item);
Disposer.Remove(this, item);
TViewModel? oldSelection = SelectedItem;
RemoveItem(index);
if (item.Equals(oldSelection))
{
int newIndex = Math.Min(index, Count - 1);
TViewModel? selectedItem = newIndex >= 0 ? this[newIndex] : default;
SelectedItem = selectedItem;
}
return true;
}
}
Disposer.Dispose(item);
Disposer.Remove(this, item);
TViewModel? oldSelection = SelectedItem;
RemoveItem(index);
if (item.Equals(oldSelection))
else
{
int newIndex = Math.Min(index, Count - 1);
TViewModel? selectedItem = newIndex >= 0 ? this[newIndex] : default;
dispatcher.Invoke(() => SelectedItem = selectedItem);
bool result = false;
dispatcher.Invoke(() => result = Remove(item));
return result;
}
return true;
}
void IList.Remove(object? value)
@@ -421,22 +459,34 @@ public abstract partial class ObservableCollection<TViewModel> :
return true;
}
public bool Replace(int index,
TViewModel item)
public bool Replace(int index, TViewModel item)
{
if (index <= Count - 1)
if (dispatcher.CheckAccess())
{
RemoveItem(index);
lock (syncLock)
{
if (index <= Count - 1)
{
RemoveItem(index);
}
else
{
index = Count;
}
Insert(index, item);
return true;
}
}
else
{
index = Count;
bool result = false;
dispatcher.Invoke(() => result = Replace(index, item));
return result;
}
Insert(index, item);
return true;
}
public void Revert()
{
foreach (object trackedProperty in trackedProperties.Values)
@@ -2,9 +2,9 @@
namespace Toolkit.Foundation;
public class ScopeServiceFactory<TService>(IServiceScopeFactory serviceScopeFactory,
public class ServiceScopeFactory<TService>(IServiceScopeFactory serviceScopeFactory,
ICache<TService, IServiceScope> cache) :
IScopeServiceFactory<TService>
IServiceScopeFactory<TService>
where TService : notnull
{
public (IServiceScope, TService) Create(params object?[] parameters)
+2
View File
@@ -6,6 +6,8 @@ namespace Toolkit.WinUI;
public class WinUIDispatcher(DispatcherQueue dispatcherQueue) :
IDispatcher
{
public bool CheckAccess() => dispatcherQueue.HasThreadAccess;
public Task Invoke(Action action)
{
dispatcherQueue.TryEnqueue(action.Invoke);