访问包含多线程ObservableCollections

时间:2016-03-31 19:38:39

标签: c# wpf multithreading sorting collectionviewsource

作为我尝试修复my other problem regarding odd disabling的一部分,我尝试更新绑定的ObservableCollection,而不是每隔几秒更换一次。显然它会出错,因为事件无法修改它。所以,我基于this link创建了一个多线程的。

public class MTObservableCollection<T> : ObservableCollection<T>
{
  public override event NotifyCollectionChangedEventHandler CollectionChanged;
  protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
  {
     var eh = CollectionChanged;
     if (eh != null)
     {
        Dispatcher dispatcher = (from NotifyCollectionChangedEventHandler nh in eh.GetInvocationList()
                                 let dpo = nh.Target as DispatcherObject
                                 where dpo != null
                                 select dpo.Dispatcher).FirstOrDefault();

        if (dispatcher != null && dispatcher.CheckAccess() == false)
        {
           dispatcher.Invoke(DispatcherPriority.DataBind, (Action)(() => OnCollectionChanged(e)));
        }
        else
        {
           foreach (NotifyCollectionChangedEventHandler nh in eh.GetInvocationList())
              nh.Invoke(this, e);
        }
     }
  }
}

我更改了对原始集合的所有引用以使用新集合。我还更改了事件处理程序代码以添加或更新集合中的现有项。这很好用。视图显示正确更新。由于我现在正在更新而不是替换,因此我必须使用CollectionViewSource为列表应用2种不同的排序属性。

           <CollectionViewSource x:Key="AskDepthCollection" Source="{Binding Path=AskDepthAsync}">
              <CollectionViewSource.SortDescriptions>
                 <scm:SortDescription PropertyName="SortOrderKey" Direction="Ascending" />
                 <scm:SortDescription PropertyName="QuotePriceDouble" Direction="Ascending" />
              </CollectionViewSource.SortDescriptions>
           </CollectionViewSource>

           <CollectionViewSource x:Key="BidDepthCollection" Source="{Binding Path=BidDepthAsync}">
              <CollectionViewSource.SortDescriptions>
                 <scm:SortDescription PropertyName="SortOrderKey" Direction="Ascending" />
                 <scm:SortDescription PropertyName="QuotePriceDouble" Direction="Descending" />
              </CollectionViewSource.SortDescriptions>
           </CollectionViewSource>

当然这意味着只对视图进行排序而不是集合本身。我需要最终位于第一个位置的ViewModel数据。大按钮(在另一个问题上显示)必须反映第一个位置的数据。具有所有业务代码的选项卡管理器只能访问主机表单和主ViewModel。它不能直接与托管列表的选项卡控件一起使用。所以,这些都是我尝试过的。

  1. 根据this post创建FirstInView扩展程序。我尝试通过调用MyCollectionProperty.FirstInView()在父ViewModel中以及在选项卡管理器中使用它。
  2. 尝试使用this post中的示例来访问它。
  3. 尝试使用另一篇文章中提及的MoveCurrentToFirst
  4. 查看了this code review,但认为它不会有用,因为可以添加,删除或更新项目,使其每隔几秒钟就可以移动到视图中的任何位置。
  5. 所有在CollectionViewSource中检索第一个ViewModel的尝试都以一件事结束:应用程序关闭而没有错误。我不知道这是因为我试图在多线程集合上执行这些任务,或者它是否是其他内容。

    无论哪种方式,我都无法弄清楚如何从排序视图中获取顶部项目。在我使用CollectionViewSource之前,我以编程方式对其进行排序并构建新列表,因此我可以抓住第一个。现在这是不可能的,因为我更新而不是重建。

    除了我复制集合,排序,存储第一个ViewModel,然后抛出已排序的副本之外,您有什么想法吗?

    编辑1

    我又尝试了#1和2。对于两者,在将第二个项添加到集合后获取视图时出现错误,但在添加第一个项后它似乎有效。它与我尝试更新常规ObservableCollection项时出现的错误非常类似(并引导我使用多线程版本)。

    CollectionViewSource.GetDefaultView(AskDepthAsync)
    
     Error: [error.initialization.failed] NotSupportedException, This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.,
        at System.Windows.Data.CollectionView.OnCollectionChanged(Object sender, NotifyCollectionChangedEventArgs args) 
        at (mynamespace).MTObservableCollection`1.OnCollectionChanged(NotifyCollectionChangedEventArgs e) in C:\path\to\class\MTObservableCollection.cs:line 34
        at (mynamespace).MTObservableCollection`1.c__DisplayClass9.b__4() in C:\path\to\class\MTObservableCollection.cs:line 29
        TargetInvocationException, Exception has been thrown by the target of an invocation. [...]

    所以,现在我回到多线程错误。调用是在改变集合的同一事件线程中进行的:事件&gt;&gt;改变收集&gt;&gt; 获取第一个已排序的项目&gt;&gt;更新大按钮属性。

    编辑2

    像往常一样,我一直在努力争取让它发挥作用。我的工作在这里受到威胁。我设法想出了这个:

     this.Dispatcher.Invoke(DispatcherPriority.Normal, new DispatcherOperationCallback(delegate
     {
        //ICollectionView view = CollectionViewSource.GetDefaultView(((MyBaseViewModel)this.DataContext).AskDepthAsync);
        ICollectionView view = CollectionViewSource.GetDefaultView(this.askDepthList.ItemsSource);
        IEnumerable<DepthLevelViewModel> col = view.Cast<DepthLevelViewModel>();
        List<DepthLevelViewModel> list = col.ToList();
        ((MyBaseViewModel)this.DataContext).AskDepthCurrent = list[0];
        return null;
     }), null);
    

    注释掉的行将检索集合并完成整个调用而不会出现问题。但是,它缺少排序,因此0索引仍然只是原始集合中的第一个项目。我可以断点,看到它没有排序描述。使用ItemsSource的未注释行确实会提取正确的行。

    • view包含正确的排序说明和源集合。
    • col仍然有SortDescriptionsSourceList。该列表显示了我投射到的DepthLevelViewModel类型的1个条目,但枚举为 为空
    • 由于list枚举空,
    • col的项目为零。因此,list[0]失败。

    如果我尝试从资源中检索CollectionViewSource,也会发生这种情况。对此有何反馈?

    编辑2.1 :如果摆脱上面的collist,请改用:

    ICollectionView view = CollectionViewSource.GetDefaultView(this.askDepthList.ItemsSource);
    var col = view.GetEnumerator();
    bool moved = col.MoveNext();
    DepthLevelViewModel item = (DepthLevelViewModel)col.Current;
    ((MyBaseViewModel)this.DataContext).AskDepthCurrent = item;
    

    col 仍为为空。它显示零项,因此moved为false,col.Current失败。

    编辑3

    看起来我不得不依靠手动排序集合并单独留下CollectionViewSource

    IEnumerable<DepthLevelViewModel> depthQuery = ((MyBaseViewModel)this.DataContext).AskDepthAsync.OrderBy(d => d.QuotePriceDouble);
    List<DepthLevelViewModel> depthList = depthQuery.ToList<DepthLevelViewModel>();
    DepthLevelViewModel item = depthList[0];
    ((MyBaseViewModel)this.DataContext).AskDepthCurrent = item;
    

    由于这不是理想的方法,因此我将这篇文章作为未答复的。但是,只要找到永久解决方案就足够了。

    编辑4

    我已将解决方案迁移到VS2015,并将每个项目的框架升级到4.6。所以,如果你知道一些更新的东西,我会喜欢听到它。

1 个答案:

答案 0 :(得分:0)

这可能不是最佳选择,但它是我设法做到的唯一比重新排序更好的方法。

在定义了CollectionViewSource的标签内容控件(视图)和跨功能使用的父DataContext(各种视图,标签管理器等)中,我添加了这个:

  void TabItemControl_DataContextChanged(object sender,
     DependencyPropertyChangedEventArgs e)
  {
     ((TabViewModel)DataContext).AskCollection = (CollectionViewSource)Resources["AskDepthCollection"];
     ((TabViewModel)DataContext).BidCollection = (CollectionViewSource)Resources["BidDepthCollection"];
  }

它存储对选项卡上使用的已排序集合的引用。在标签管理器中,我把:

  Private Function GetFirstQuote(ByVal eType As BridgeTraderBuyIndicator) As DepthLevelViewModel

     Dim cView As ICollectionView
     Dim cSource As CollectionViewSource
     Dim retView As DepthLevelViewModel = Nothing

     If eType = BridgeTraderBuyIndicator.Buy Then
        cSource = multilegViewModel.AskCollection
     Else
        cSource = multilegViewModel.BidCollection
     End If

     Try
        cView = cSource.View()

        Dim enumerator As IEnumerator = cView.Cast(Of DepthLevelViewModel).GetEnumerator()
        Dim moved As Boolean = enumerator.MoveNext()
        If moved Then
           retView = DirectCast(enumerator.Current(), DepthLevelViewModel)
        Else
           ApplicationModel.Logger.WriteToLog((New StackFrame(0).GetMethod().Name) & ": ERROR - Unable to retrieve the first quote.", LoggerConstants.LogLevel.Heavy)
        End If
     Catch ex As Exception
        ApplicationModel.AppMsgBox.DebugMsg(ex, (New StackFrame(0).GetMethod().Name))
     End Try

     Return retView

  End Function

当我需要获得第一项时,我就这样称呼它:

        Dim ask As DepthLevelViewModel
        ask = GetFirstQuote(MyTypeEnum.Buy)

如果没有提供更好的解决方案,我会将此标记作为已接受的答案。