Why does CollectionViewSource.GetDefaultView(…) re

2019-07-10 11:18发布

问题:

I have what I think is a fairly standard setup, a ListBox backed by an ObservableCollection.

I have some work to do with the Things in the ObservableCollection which might take a significant amount of time (more than a few hundred milliseconds) so I'd like to offload that onto a Task (I could have also used BackgroundWorker here) so as to not freeze the UI.

What's strange is that when I do CollectionViewSource.GetDefaultView(vm.Things).CurrentItem before starting the Task, everything works as expected, however if this happens during the Task then CurrentItem seems to always point to the first element in the ObservableCollection.

I've drawn up a complete working example.

XAML:

<Window x:Class="WpfApplication2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <DockPanel>
        <ToolBar DockPanel.Dock="Top">
            <Button Content="Click Me Sync" Click="ButtonSync_Click" />
            <Button Content="Click Me Async Good" Click="ButtonAsyncGood_Click" />
            <Button Content="Click Me Async Bad" Click="ButtonAsyncBad_Click" />
        </ToolBar>
        <TextBlock DockPanel.Dock="Bottom" Text="{Binding Path=SelectedThing.Name}" />
        <ListBox Name="listBox1" IsSynchronizedWithCurrentItem="True" ItemsSource="{Binding Path=Things}" SelectedItem="{Binding Path=SelectedThing}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Path=Name}" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </DockPanel>
</Window>

C#:

public partial class MainWindow : Window
{
    private readonly ViewModel vm;

    public MainWindow()
    {
        InitializeComponent();
        vm = new ViewModel();
        DataContext = vm;
    }

    private ICollectionView GetCollectionView()
    {
        return CollectionViewSource.GetDefaultView(vm.Things);
    }

    private Thing GetSelected()
    {
        var view = GetCollectionView();
        return view == null ? null : (Thing)view.CurrentItem;
    }

    private void NewTask(Action start, Action finish)
    {
        Task.Factory
            .StartNew(start)
            .ContinueWith(t => finish());
            //.ContinueWith(t => finish(), TaskScheduler.Current);
            //.ContinueWith(t => finish(), TaskScheduler.Default);
            //.ContinueWith(t => finish(), TaskScheduler.FromCurrentSynchronizationContext());
    }

    private void ButtonSync_Click(object sender, RoutedEventArgs e)
    {
        var thing = GetSelected();
        DoWork(thing);
        MessageBox.Show("all done");
    }

    private void ButtonAsyncGood_Click(object sender, RoutedEventArgs e)
    {
        var thing = GetSelected(); // outside new task
        NewTask(() =>
        {
            DoWork(thing);
        }, () =>
        {
            MessageBox.Show("all done");
        });
    }

    private void ButtonAsyncBad_Click(object sender, RoutedEventArgs e)
    {
        NewTask(() =>
        {
            var thing = GetSelected(); // inside new task
            DoWork(thing); // thing will ALWAYS be the first element -- why?
        }, () =>
        {
            MessageBox.Show("all done");
        });
    }

    private void DoWork(Thing thing)
    {
        Thread.Sleep(1000);
        var msg = thing == null ? "nothing selected" : thing.Name;
        MessageBox.Show(msg);
    }
}

public class ViewModel
{
    public ObservableCollection<Thing> Things { get; set; }
    public Thing SelectedThing { get; set; }

    public ViewModel()
    {
        Things = new ObservableCollection<Thing>();
        Things.Add(new Thing() { Name = "one" });
        Things.Add(new Thing() { Name = "two" });
        Things.Add(new Thing() { Name = "three" });
        Things.Add(new Thing() { Name = "four" });
    }
}

public class Thing
{
    public string Name { get; set; }
}

回答1:

I believe CollectionViewSource.GetDefaultView is effectively thread-static - in other words, each thread will see a different view. Here's a short test to show that:

using System;
using System.Windows.Data;
using System.Threading.Tasks;

internal class Test
{
    static void Main() 
    {
        var source = "test";
        var view1 = CollectionViewSource.GetDefaultView(source);
        var view2 = CollectionViewSource.GetDefaultView(source);        
        var view3 = Task.Factory.StartNew
            (() => CollectionViewSource.GetDefaultView(source))
            .Result;

        Console.WriteLine(ReferenceEquals(view1, view2)); // True
        Console.WriteLine(ReferenceEquals(view1, view3)); // False
    }        
}

If you want your task to work on a particular item, I suggest you fetch that item before starting the task.