访问包含多线程 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。它不能直接与托管列表的选项卡控件一起使用。所以,这些是我尝试过的东西。
- 基于 this post 创建一个
FirstInView
扩展。我尝试通过调用 MyCollectionProperty.FirstInView(). 在父 ViewModel 以及选项卡管理器中使用它
- 尝试使用 this post 中的示例访问它。
- 已尝试使用 MoveCurrentToFirst,如另一个 post 中所述。
- 查看了 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
的未注释行确实拉出了正确的行。
view
包含带有排序说明和源集合的右侧。
col
仍然有 SortDescriptions
和 SourceList
。该列表显示了我要转换为的 DepthLevelViewModel
类型的 1 个条目,但枚举是 empty.
由于 col
的枚举为空,list
的项目为零。因此,list[0]
失败。
如果我尝试从资源中检索 CollectionViewSource,也会发生这种情况。对此有任何反馈吗?
EDIT 2.1:如果去掉上面的 col
和 list
而是使用这个:
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)
如果没有提供更好的解决方案,我会将其标记为已接受的答案。
作为我尝试修复
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。它不能直接与托管列表的选项卡控件一起使用。所以,这些是我尝试过的东西。
- 基于 this post 创建一个
FirstInView
扩展。我尝试通过调用 MyCollectionProperty.FirstInView(). 在父 ViewModel 以及选项卡管理器中使用它
- 尝试使用 this post 中的示例访问它。
- 已尝试使用 MoveCurrentToFirst,如另一个 post 中所述。
- 查看了 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
的未注释行确实拉出了正确的行。
view
包含带有排序说明和源集合的右侧。col
仍然有SortDescriptions
和SourceList
。该列表显示了我要转换为的DepthLevelViewModel
类型的 1 个条目,但枚举是 empty.
由于 list
的项目为零。因此,list[0]
失败。
col
的枚举为空,如果我尝试从资源中检索 CollectionViewSource,也会发生这种情况。对此有任何反馈吗?
EDIT 2.1:如果去掉上面的 col
和 list
而是使用这个:
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)
如果没有提供更好的解决方案,我会将其标记为已接受的答案。