问题
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.
- 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(). - Tried to access it by using examples from this post.
- Attempted to use MoveCurrentToFirst as mentioned in another post.
- 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 theSortDescriptions
along withSourceList
. The list shows 1 entry of theDepthLevelViewModel
type that I'm casting to, but the enumeration is empty.list
has zero items due tocol
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