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

发布于 2024-10-28 07:07:10 字数 4105 浏览 1 评论 0原文

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

我有一些工作需要处理 ObservableCollection 中的 Thing,这可能需要大量时间(超过几百毫秒),所以我想卸载将其放到 Task 上(我也可以在此处使用 BackgroundWorker),以免冻结 UI。

奇怪的是,当我在启动 Task 之前执行 CollectionViewSource.GetDefaultView(vm.Things).CurrentItem 时,一切都会按预期工作,但是如果这种情况发生在期间< /strong> TaskCurrentItem 似乎总是指向 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; }
}

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

鹤仙姿 2024-11-04 07:07:10

我相信 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
    }        
}

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

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文