I have what I think is a fairly standard setup, a ListBox
backed by an ObservableCollection
.
I have some work to do with the Thing
s 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; }
}