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 : public class AvaloniaDispatcher :
IDispatcher IDispatcher
{ {
public bool CheckAccess() =>
Dispatcher.UIThread.CheckAccess();
public async Task Invoke(Action action) => public async Task Invoke(Action action) =>
await Dispatcher.UIThread.InvokeAsync(action); await Dispatcher.UIThread.InvokeAsync(action);
} }
+2
View File
@@ -3,4 +3,6 @@
public interface IDispatcher public interface IDispatcher
{ {
Task Invoke(Action action); Task Invoke(Action action);
bool CheckAccess();
} }
+1 -1
View File
@@ -2,7 +2,7 @@
namespace Toolkit.Foundation; namespace Toolkit.Foundation;
public interface IScopeServiceFactory<TService> public interface IServiceScopeFactory<TService>
{ {
(IServiceScope, TService) Create(params object?[] parameters); (IServiceScope, TService) Create(params object?[] parameters);
} }
+78 -28
View File
@@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using System.Collections; using System.Collections;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Reactive.Concurrency;
using System.Reactive.Disposables; using System.Reactive.Disposables;
namespace Toolkit.Foundation; namespace Toolkit.Foundation;
@@ -32,6 +33,8 @@ public abstract partial class ObservableCollection<TViewModel> :
{ {
private readonly System.Collections.ObjectModel.ObservableCollection<TViewModel> collection = []; private readonly System.Collections.ObjectModel.ObservableCollection<TViewModel> collection = [];
private readonly Lock syncLock = new();
private readonly IDispatcher dispatcher; private readonly IDispatcher dispatcher;
private readonly Dictionary<string, object> trackedProperties = []; private readonly Dictionary<string, object> trackedProperties = [];
@@ -95,8 +98,27 @@ public abstract partial class ObservableCollection<TViewModel> :
public TViewModel this[int index] public TViewModel this[int index]
{ {
get => collection[index]; get
set => SetItem(index, value); {
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] object? IList.this[int index]
@@ -129,11 +151,19 @@ public abstract partial class ObservableCollection<TViewModel> :
public void Add(TViewModel item) public void Add(TViewModel item)
{ {
int index = collection.Count; if (dispatcher.CheckAccess())
InsertItem(index, item); {
lock (syncLock)
{
InsertItem(collection.Count, item);
UpdateSelection(item); UpdateSelection(item);
} }
}
else
{
dispatcher.Invoke(() => Add(item));
}
}
public void Add(object item) public void Add(object item)
{ {
@@ -263,37 +293,33 @@ public abstract partial class ObservableCollection<TViewModel> :
public bool Move(int oldIndex, int newIndex) public bool Move(int oldIndex, int newIndex)
{ {
if (oldIndex < 0) if (dispatcher.CheckAccess())
{
lock (syncLock)
{
if (oldIndex < 0 || newIndex < 0 || oldIndex >= Count || newIndex >= Count)
{ {
return false; return false;
} }
TViewModel item = this[oldIndex]; TViewModel item = collection[oldIndex];
collection.Move(oldIndex, newIndex);
bool moveSelection = false; if (item is ISelectable selectable && selectable.IsSelected)
if (item is ISelectable oldSelection)
{ {
if (oldSelection.IsSelected)
{
moveSelection = true;
SelectedItem = default;
}
}
RemoveItem(oldIndex);
InsertItem(newIndex, item);
if (moveSelection)
{
if (item is ISelectable newSelection)
{
newSelection.IsSelected = true;
dispatcher.Invoke(() => SelectedItem = item); dispatcher.Invoke(() => SelectedItem = item);
} }
}
return true; return true;
} }
}
else
{
bool result = false;
dispatcher.Invoke(() => result = Move(oldIndex, newIndex));
return result;
}
}
public bool Move(int index, TViewModel item) public bool Move(int index, TViewModel item)
{ {
@@ -367,6 +393,10 @@ public abstract partial class ObservableCollection<TViewModel> :
} }
public bool Remove(TViewModel item) public bool Remove(TViewModel item)
{
if (dispatcher.CheckAccess())
{
lock (syncLock)
{ {
int index = collection.IndexOf(item); int index = collection.IndexOf(item);
if (index < 0) if (index < 0)
@@ -384,11 +414,19 @@ public abstract partial class ObservableCollection<TViewModel> :
{ {
int newIndex = Math.Min(index, Count - 1); int newIndex = Math.Min(index, Count - 1);
TViewModel? selectedItem = newIndex >= 0 ? this[newIndex] : default; TViewModel? selectedItem = newIndex >= 0 ? this[newIndex] : default;
dispatcher.Invoke(() => SelectedItem = selectedItem); SelectedItem = selectedItem;
} }
return true; return true;
} }
}
else
{
bool result = false;
dispatcher.Invoke(() => result = Remove(item));
return result;
}
}
void IList.Remove(object? value) void IList.Remove(object? value)
{ {
@@ -421,8 +459,11 @@ public abstract partial class ObservableCollection<TViewModel> :
return true; return true;
} }
public bool Replace(int index, public bool Replace(int index, TViewModel item)
TViewModel item) {
if (dispatcher.CheckAccess())
{
lock (syncLock)
{ {
if (index <= Count - 1) if (index <= Count - 1)
{ {
@@ -436,6 +477,15 @@ public abstract partial class ObservableCollection<TViewModel> :
Insert(index, item); Insert(index, item);
return true; return true;
} }
}
else
{
bool result = false;
dispatcher.Invoke(() => result = Replace(index, item));
return result;
}
}
public void Revert() public void Revert()
{ {
@@ -2,9 +2,9 @@
namespace Toolkit.Foundation; namespace Toolkit.Foundation;
public class ScopeServiceFactory<TService>(IServiceScopeFactory serviceScopeFactory, public class ServiceScopeFactory<TService>(IServiceScopeFactory serviceScopeFactory,
ICache<TService, IServiceScope> cache) : ICache<TService, IServiceScope> cache) :
IScopeServiceFactory<TService> IServiceScopeFactory<TService>
where TService : notnull where TService : notnull
{ {
public (IServiceScope, TService) Create(params object?[] parameters) public (IServiceScope, TService) Create(params object?[] parameters)
+2
View File
@@ -6,6 +6,8 @@ namespace Toolkit.WinUI;
public class WinUIDispatcher(DispatcherQueue dispatcherQueue) : public class WinUIDispatcher(DispatcherQueue dispatcherQueue) :
IDispatcher IDispatcher
{ {
public bool CheckAccess() => dispatcherQueue.HasThreadAccess;
public Task Invoke(Action action) public Task Invoke(Action action)
{ {
dispatcherQueue.TryEnqueue(action.Invoke); dispatcherQueue.TryEnqueue(action.Invoke);