Почему CollectionViewSource.GetDefaultView(...) возвращает неверный CurrentItem из потока задач?
У меня есть то, что я думаю, это довольно стандартная установка, ListBox
при поддержке ObservableCollection
,
У меня есть работа с Thing
в 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>
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 ответ
Я верю 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
}
}
Если вы хотите, чтобы ваша задача работала с определенным элементом, я предлагаю вам извлечь этот элемент перед началом задачи.