访问包含多线程 ObservableCollections 的 CollectionViewSource 中的第一项

Accessing the first item in a CollectionViewSource containing multithreaded ObservableCollections

作为我尝试修复 , I tried to just updating the bound ObservableCollection instead of replacing the whole thing every few seconds. Obviously it gives an error since the event can't modify it. So, I created a multithreaded one based on 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().
  2. 在父 ViewModel 以及选项卡管理器中使用它
  3. 尝试使用 this post 中的示例访问它。
  4. 已尝试使用 MoveCurrentToFirst,如另一个 post 中所述。
  5. 查看了 this code review,但认为它无济于事,因为可以添加、删除或更新项目以使其每隔几秒移动一次视图中的任何位置。

在 CollectionViewSource 中检索第一个 ViewModel 的所有尝试都以一件事结束:应用程序无错关闭。我不知道这是因为我试图在多线程集合上执行这些任务,还是其他原因。

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

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

编辑 1

我再次尝试了#1 和 2。对于这两者,在将 second 项目添加到集合后获取视图时会发生错误,但在添加第一个项目后它似乎可以正常工作。这与我尝试更新常规 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. [...]

所以,现在我又回到了多线程错误。该调用是在更改集合的同一事件线程中进行的:事件 >> 更改集合 >> 获取第一个排序项 >> 更新大按钮属性。

编辑 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 的未注释行确实拉出了正确的行。

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

EDIT 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 为假,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;

由于这不是理想的方法,我将此 post 保留为未答复。但是,在找到永久解决方案之前就足够了。

编辑 4

我已经将解决​​方案迁移到VS2015,并将每个项目的框架升级到4.6。因此,如果您知道一些新的可行的方法,我很乐意听到。

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

在定义了 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)

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