微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

如何构建现代的、同步的 ObservableCollection? 同步 ObservableCollection<T> 类:INotifyPropertyChanged 支持上述 SynchronizedCollection 项:众多 ViewModel 之一使用 SO 的 IAsyncCommand:

如何解决如何构建现代的、同步的 ObservableCollection? 同步 ObservableCollection<T> 类:INotifyPropertyChanged 支持上述 SynchronizedCollection 项:众多 ViewModel 之一使用 SO 的 IAsyncCommand:

阅读 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 之一(使用 SOIAsyncCommand):

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); } );

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。