为什么CollectionViewSource.GetDefaultView(...)从Task线程中返回错误的CurrentItem?

epa*_*alm 2 c# wpf multithreading task

我认为这是一个相当标准的设置,有一个ListBox支持ObservableCollection.

我有一些工作要处理可能需要花费大量时间(超过几百毫秒)的Things,ObservableCollection所以我想将其卸载到Task(我本来也可以使用BackgroundWorker)以免冻结用户界面.

什么奇怪的是,当我CollectionViewSource.GetDefaultView(vm.Things).CurrentItem开始之前Task,一切正常,但是如果发生这种情况Task那么CurrentItem似乎总是指向的第一个元素ObservableCollection.

我已经制定了一个完整的工作示例.

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>
Run Code Online (Sandbox Code Playgroud)

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; }
}
Run Code Online (Sandbox Code Playgroud)

Jon*_*eet 8

我相信它CollectionViewSource.GetDefaultView是有效的线程静态的 - 换句话说,每个线程都会看到不同的视图.这是一个简短的测试,表明:

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
    }        
}
Run Code Online (Sandbox Code Playgroud)

如果您希望您的任务处理特定项目,我建议您在开始任务之前获取该项目.

  • 在内部,数据绑定引擎是线程静态的,并且`CollectionViewSource.GetDefaultView`从当前(线程静态)数据绑定引擎请求给定集合的默认视图. (2认同)