Accessing the first item in a CollectionViewSource containing multithreaded ObservableCollections

拜拜、爱过 提交于 2019-12-12 04:07:35

问题


As part of my attempt to fix my other problem regarding odd disabling, 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);
        }
     }
  }
}

I changed all references to the original collection to use the new one. I also changed the event handler code to add to or update existing items in the collections. This works fine. The view display updates correctly. Since I'm now updating instead of replacing, I have to use a CollectionViewSource to apply 2 different sort properties for the lists.

           <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>

Of course this means that just the view is sorted and not the collection itself. I need the data of the ViewModel that ends up in the first position. The big buttons (shown on the other issue) have to reflect the data from the first position. The tab manager that has all of the business code only has access to the host form and the main ViewModel. It does not work directly with the tab control hosting the list. So, these are the things I've tried.

  1. Create a FirstInView extension based on this post. I tried using it within the parent ViewModel, as well as within the tab manager, by calling MyCollectionProperty.FirstInView().
  2. Tried to access it by using examples from this post.
  3. Attempted to use MoveCurrentToFirst as mentioned in another post.
  4. Looked at this code review, but decided that it wouldn't help since items can be added, removed, or updated to cause them to move anywhere in the view every few seconds.

All of the attempts to retrieve the first ViewModel in the CollectionViewSource ended in one thing: the application closes without error. I don't know if this is because I'm attempting to perform these tasks on the mulithreaded collection, or if it's something else.

Either way, I'm not able to figure out how to get the top item from the sorted view. Before I used the CollectionViewSource, I was programatically sorting it and building a new list, so I could just grab the first one. This isn't possible now since I'm updating instead of rebuilding.

Do you have any ideas other than me copying the collection, sorting it, store the first ViewModel, and then tossing the sorted copy?

EDIT 1

I attempted #1 and 2 again. For both, an error occurs on getting the view AFTER a second item is added to the collection, but it appears to work after just the first item is added. It's very similar to the error that occurred when I was trying to update a regular ObservableCollection item (and led me to the multithreaded version).

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. [...]

So, now I'm back to a multithreading error. The call is made in the same event thread that alters the collection: Event >> Alter collection >> Get 1st sorted item >> Update big button properties.

EDIT 2

As usual, I keep hammering at this thing to try to get it to work. My job is at stake here. I've managed to come up with this:

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

The commented-out line will retrieve the collection and go through the whole call without a problem. But, it's missing the sorting, so the 0 index is still just the first item in the original collection. I can breakpoint and see that it doesn't have the sort descriptions. The uncommented line that uses ItemsSource does pull the correct one.

  • view contains the right one with sort descriptions and the source collection.
  • col still has the SortDescriptions along with SourceList. The list shows 1 entry of the DepthLevelViewModel type that I'm casting to, but the enumeration is empty.
  • list has zero items due to col having an empty enumeration. Therefore, list[0] fails.

This also happens if I try to retrieve the CollectionViewSource from the resources. Any feedback on this?

EDIT 2.1: If get rid of the col and list above and instead use this:

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 still is empty. It shows zero items, so moved is false, and col.Current fails.

EDIT 3

It looks like I'm going to have to fall back on sorting the collection manually and leave the CollectionViewSource alone.

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;

Since this is not the ideal method, I'm leaving this post as unanswered. But, it will suffice until a permanent solution is found.

EDIT 4

I have migrated the solution to VS2015 and upgraded the framework for every project to 4.6. So if you know something newer that would work, I'd love to hear it.


回答1:


This may not be optimal, but it's the only thing I've managed to do that works better than a re-sort.

In the tab content control (view) that has the CollectionViewSource defined and the parent DataContext that is used across the functionality (various views, tab manager, etc), I added this:

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

This stores a reference to the sorted collections used on the tab. In the tab manager, I put:

  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

When I need to get the first item, I just call it like this:

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

If a better solution isn't provided, I'll mark this one as the accepted answer.



来源:https://stackoverflow.com/questions/36342247/accessing-the-first-item-in-a-collectionviewsource-containing-multithreaded-obse

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!