如何构建一个现代的、同步的 ObservableCollection?
Posted
技术标签:
【中文标题】如何构建一个现代的、同步的 ObservableCollection?【英文标题】:How to build a modern, synchronized ObservableCollection? 【发布时间】:2021-07-10 14:46:11 【问题描述】:阅读severaltutorials、snippets(似乎是@Xcalibur37 的blog post 的来源或几乎1:1 的副本)当然还有他们的“起源”questionsonSO,我'我不仅仍然对跨线程访问感到困惑,而且还在努力让我的 WPF 应用程序在 CollectionChanged
上正确执行绑定更新 - 这在启动和删除时有效,但不适用于插入副本。
SO 都是关于代码的,所以让我们直接开始吧 - 首先是两个集合,然后是 VM,“工作”和“失败”:
同步ObservableCollection<T>
类:
public class SynchronizedCollection<T> : ObservableCollection<T> where T : class
// AFAICT, event overriding is needed, yet my app behaves the same without it?!
public override event NotifyCollectionChangedEventHandler CollectionChanged;
public SynchronizedCollection()
// Implemented this in my base-ViewModel's ctor first, but
// a) read somewhere that it's supposed to be done here instead
// b) call in base-VM resulted in 1 invocation per collection, though for _all_ VM at once!
BindingOperations.CollectionRegistering += (sender, eventArgs) =>
if (eventArgs.Collection.Equals(this)) // R# suggested, Equals() is wiser than == here.
BindingOperations.EnableCollectionSynchronization(this, SynchronizationLock);
;
// Can't be static due to class type parameter, but readonly should do.
// Also, since EnableCollectionSynchronization() is called in ctor, 1 lock object per collection.
private object SynchronizationLock get; = new object();
protected override void InsertItem(int index, T item)
lock (SynchronizationLock)
base.InsertItem(index, item);
// Named InsertItems instead of AddRange for consistency.
public void InsertItems(IEnumerable<T> items)
var list = items as IList<T> ?? items.ToList();
int start = Count;
foreach (T item in list)
lock (SynchronizationLock)
Items.Add(item);
// Multi-insert, but notify only once after completion.
OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count)));
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, list, start));
// Code left out for brevity...
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs eventArgs)
lock (SynchronizationLock)
if (!(CollectionChanged is NotifyCollectionChangedEventHandler eventHandler))
return;
foreach (Delegate @delegate in eventHandler.GetInvocationList())
var handler = (NotifyCollectionChangedEventHandler)@delegate;
if (handler.Target is DispatcherObject current && !current.CheckAccess())
current.Dispatcher.Invoke(DispatcherPriority.DataBind, handler, this, eventArgs);
else
handler(this, eventArgs);
INotifyPropertyChanged
支持上面的SynchronizedCollection
项目:
public class NotifySynchronizedCollection<T> : SynchronizedCollection<T>, INotifySynchronizedCollection
where T : class
public event CollectionItemPropertyChangedEventHandler CollectionItemPropertyChanged;
// Code left out for brevity...
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs eventArgs)
// Seems to me like lock() isn't needed here...
//lock (SynchronizationLock)
//
switch (eventArgs.Action)
case NotifyCollectionChangedAction.Add:
RegisterItemPropertyChanged(eventArgs.NewItems);
break;
case NotifyCollectionChangedAction.Remove:
case NotifyCollectionChangedAction.Reset when !(eventArgs.OldItems is null):
UnregisterItemPropertyChanged(eventArgs.OldItems);
break;
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Replace:
UnregisterItemPropertyChanged(eventArgs.OldItems);
RegisterItemPropertyChanged(eventArgs.NewItems);
break;
//
private void OnItemPropertyChanged(object item, PropertyChangedEventArgs itemArgs) =>
CollectionItemPropertyChanged?.Invoke(this, item, itemArgs);
private void RegisterItemPropertyChanged(IEnumerable items)
foreach (INotifyPropertyChanged item in items)
if (item != null)
item.PropertyChanged += OnItemPropertyChanged;
private void UnregisterItemPropertyChanged(IEnumerable items)
foreach (INotifyPropertyChanged item in items)
if (item != null)
item.PropertyChanged -= OnItemPropertyChanged;
众多 ViewModel 之一(使用 AsyncAwaitBestPractices.MVVM 的 IAsyncCommand
):
public class OrdersViewModel : BaseViewModel
// BindingOperations.EnableCollectionSynchronization was once in BaseViewModel's ctor (with
// mentioned side-effects at this question's intro) & even right in this VM's ctor - none of
// the tutorials I've found mentioned a solution for tedious EnableCollectionSynchronization
// calls for each collection, in each VM, hence I tried CollectionRegistering in base-VM...
// Code left out for brevity...
public OrdersViewModel(INavigationService navService, IOrderDataService dataService)
: base(navService)
DataService = dataService;
RegisterMessages();
// Code left out for brevity...
// Note: This works, except for the view which doesn't show the newly added item!
// However, another TextBlock-binding for Orders.Count _does_ update?!
// Using ConfigureAwait(true) inside instead didn't help either...
public IAsyncCommand<OrderModel> CopyCommand =>
_copy ?? (_copy = new AsyncRelayCommand<OrderModel>(
async original =>
if (!await ShowConfirmation("Copy this order?").ConfigureAwait(false))
return;
if (original.ProductId < 1)
throw new ArgumentOutOfRangeException(
nameof(original.ProductId),
original.ProductId,
@"Valid product missing.");
await AddOrder(
await DataService.CreateOrderCopy(original.Id).ConfigureAwait(false)
?? throw new ArgumentNullException(nameof(original.Id), $@"Copying failed."))
.ConfigureAwait(false);
,
original => original.Id > 0,
async exception => await ShowError("Copying", exception).ConfigureAwait(false)));
// Note: This works!
public IAsyncCommand<OrderModel> Delete =>
_delete ?? (_delete = new AsyncCommand<OrderModel>(
async deletable =>
bool isChild = deletable.ParentId > 0;
if (!await ShowConfirmation($"Delete this order?").ConfigureAwait(false))
return;
await DataService.DeleteOrder(deletable.Id).ConfigureAwait(false);
if (isChild)
await RefreshParent(Orders.Single(order => order.Id == deletable.ParentId))
.ConfigureAwait(false);
Orders.Remove(deletable);
await ShowInfo($"Order deleted.").ConfigureAwait(false);
,
deletable => (deletable.ParentId > 0)
|| (Orders.SingleOrDefault(order => order.Id == deletable.Id)
?.ChildrenCount < 1),
async exception => await ShowError("Deletion", exception).ConfigureAwait(false)));
private async Task AddOrder(int orderId)
// Note: Using ConfigureAwait(true) doesn't help either.
// But while
Orders.Add(await GetOrder(orderId, false).ConfigureAwait(false));
// Code left out for brevity...
private void RegisterMessages()
Default.Register<OrdersInitializeMessage>(this, async message =>
Orders.Clear();
Task<CustomerModel> customerTask = DataService.GetCustomer(message.CustomerId);
Task<List<OrderModel>> ordersTask = DataService.GetOrders(message.OrderId);
await Task.WhenAll(customerTask, ordersTask).ConfigureAwait(false);
Customer = await customerTask.ConfigureAwait(false) ?? Customer;
(await ordersTask.ConfigureAwait(false)).ForEach(Orders.Add); // NOTE: This works!
SelectedOrder =
Orders.Count == 1
? Orders[0]
: Orders.SingleOrDefault(order => order.Id == message.OrderId);
);
// Code left out for brevity...
为什么Delete
命令和Orders.Add()
(在RegisterMessages()
内部)都可以工作,而Copy
命令的Orders.Add()
调用却不行?
Delete
命令使用Orders.Remove(deletable);
,它又在SynchronizedCollection<T>
中调用我覆盖的RemoveItem
,其实现方式与上面的InsertItem
类似)
【问题讨论】:
为什么不使用带有或不带有BindingOperations.EnableCollectionSynchronization
的内置ObservableCollection<T>
?你想解决什么问题?
为什么不在后台线程上做后台工作,await
ing 那个工作,然后在await
返回到 UI 线程后更新ObservableCollection<T>
?
@Yoda:是的,只要你不需要上下文,就使用ConfigureAwait(false)
。如果您需要在 await
之后更新 UI,那么这是您确实需要上下文的完美示例,因此您不会在那里使用 ConfigureAwait(false)
。
@Yoda:如果您只从 UI 线程更新 ObservableCollection<T>
(我总是这样做),那么不需要自定义集合或 lock
s。 ConfigureAwait(false)
应该只用于不需要上下文的方法;如果一个方法调用另一个需要上下文的方法,那么父方法也需要上下文;如果它只是await
s 任务,那么它不会。
@Yoda:如果您有一个检索要显示的数据的命令,那么这不是“一劳永逸”或IAsyncCommand
。使用NotifyTask<T>
or similar 可以更好地表示此类事情。此外,上下文需求从 child 流向 parent,而不是相反。 HttpClient
从不需要上下文,因为它的方法(和子)不会更新 UI。
【参考方案1】:
这是跨线程操作的正确和现代方法吗
必须在创建集合的线程上使用集合。当我有一个需要添加数据的线程操作时,我会调用 GUI 线程,例如
public static void SafeOperationToGuiThread(Action operation)
=> System.Windows.Application.Current?.Dispatcher?.Invoke(operation);
线程使用
SafeOperationToGuiThread(() => MyCollection.Add( itemfromthread); );
【讨论】:
以上是关于如何构建一个现代的、同步的 ObservableCollection?的主要内容,如果未能解决你的问题,请参考以下文章