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);
} }
+104 -54
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,10 +151,18 @@ 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)
UpdateSelection(item); {
InsertItem(collection.Count, item);
UpdateSelection(item);
}
}
else
{
dispatcher.Invoke(() => Add(item));
}
} }
public void Add(object item) public void Add(object item)
@@ -263,36 +293,32 @@ 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())
{ {
return false; lock (syncLock)
}
TViewModel item = this[oldIndex];
bool moveSelection = false;
if (item is ISelectable oldSelection)
{
if (oldSelection.IsSelected)
{ {
moveSelection = true; if (oldIndex < 0 || newIndex < 0 || oldIndex >= Count || newIndex >= Count)
SelectedItem = default; {
return false;
}
TViewModel item = collection[oldIndex];
collection.Move(oldIndex, newIndex);
if (item is ISelectable selectable && selectable.IsSelected)
{
dispatcher.Invoke(() => SelectedItem = item);
}
return true;
} }
} }
else
RemoveItem(oldIndex);
InsertItem(newIndex, item);
if (moveSelection)
{ {
if (item is ISelectable newSelection) bool result = false;
{ dispatcher.Invoke(() => result = Move(oldIndex, newIndex));
newSelection.IsSelected = true; return result;
dispatcher.Invoke(() => SelectedItem = item);
}
} }
return true;
} }
public bool Move(int index, TViewModel item) public bool Move(int index, TViewModel item)
@@ -368,26 +394,38 @@ public abstract partial class ObservableCollection<TViewModel> :
public bool Remove(TViewModel item) public bool Remove(TViewModel item)
{ {
int index = collection.IndexOf(item); if (dispatcher.CheckAccess())
if (index < 0)
{ {
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;
}
} }
else
Disposer.Dispose(item);
Disposer.Remove(this, item);
TViewModel? oldSelection = SelectedItem;
RemoveItem(index);
if (item.Equals(oldSelection))
{ {
int newIndex = Math.Min(index, Count - 1); bool result = false;
TViewModel? selectedItem = newIndex >= 0 ? this[newIndex] : default; dispatcher.Invoke(() => result = Remove(item));
dispatcher.Invoke(() => SelectedItem = selectedItem); return result;
} }
return true;
} }
void IList.Remove(object? value) void IList.Remove(object? value)
@@ -421,22 +459,34 @@ 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 (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 else
{ {
index = Count; bool result = false;
dispatcher.Invoke(() => result = Replace(index, item));
return result;
} }
Insert(index, item);
return true;
} }
public void Revert() public void Revert()
{ {
foreach (object trackedProperty in trackedProperties.Values) foreach (object trackedProperty in trackedProperties.Values)
@@ -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);