问题描述
阅读 several tutorials 后,snippets(似乎是@Xcalibur37 的 blog post 的来源或几乎 1:1 的副本),当然还有它们的“起源”{{3 }} questions on,我不仅仍然对跨线程访问感到困惑,而且还在努力让我的 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 之一(使用 SO 的 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);
,它反过来在 RemoveItem
中调用我覆盖的 SynchronizedCollection<T>
,它的实现就像上面的 InsertItem
)
解决方法
这是跨线程操作的正确和现代方法
必须在创建集合的线程上使用集合。当我有一个需要添加数据的线程操作时,我会调用GUI线程,例如
public static void SafeOperationToGuiThread(Action operation)
=> System.Windows.Application.Current?.Dispatcher?.Invoke(operation);
线程使用
SafeOperationToGuiThread(() => { MyCollection.Add( itemfromthread); } );