c# - 为什么 CollectionViewSource.GetDefaultView(...) 从任务线程内部返回错误的 CurrentItem?

标签 c# wpf multithreading task

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

我有一些工作要处理 ObservableCollection 中的 Thing,这可能需要很长时间(超过几百毫秒),所以我会喜欢将其卸载到 Task 上(我也可以在这里使用 BackgroundWorker),以免卡住 UI。

奇怪的是,当我在开始 Task 之前执行 CollectionViewSource.GetDefaultView(vm.Things).CurrentItem 时,一切都按预期进行,但是如果发生这种情况 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>

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

最佳答案

我相信 CollectionViewSource.GetDefaultView 实际上是线程静态的 - 换句话说,每个线程都会看到不同的 View 。这是一个简短的测试来证明:

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

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

关于c# - 为什么 CollectionViewSource.GetDefaultView(...) 从任务线程内部返回错误的 CurrentItem?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/5489004/

相关文章:

c# - 为 UserControl 填充模型的正确方法是什么?

c# - 二叉树单向最大高度的解决方法

c# - 如何将菜单与 WPF MVVM 合并?

c# - 消费者的 Amazon Kinesis KCL 客户端无法在 .NET 中运行

c# - 更改 ModelView 中的样式(MVVM + WPF)

c# - 全局范围附加依赖属性

C++ 多线程应用程序永远挂起

c++ - 在 unique_lock 上导致 std::system_error 的生产者-消费者场景

multithreading - 在 Rust 中并行处理树

c# - 来自 web api : access_denied error 的谷歌身份验证