I have a WPF application that needs to parse a bunch of large XML files (about 40 MB) containing products and store information about all the products that are actually books. For the progress report, I have a datagrid that displays the file name, status ("pending", "parsing", "completed"), the number of products found, the number of products analyzed and the number of books found, for example this:
<DataGrid Grid.ColumnSpan="2" Grid.Row="1" ItemsSource="{Binding OnixFiles}" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False" CanUserReorderColumns="False" CanUserResizeColumns="False" CanUserResizeRows="False" CanUserSortColumns="False"> <DataGrid.Columns> <DataGridTextColumn Header="Bestand" IsReadOnly="True" Binding="{Binding FileName}" SortMemberPath="FileName" /> <DataGridTextColumn Header="Status" IsReadOnly="True" Binding="{Binding Status}" /> <DataGridTextColumn Header="Aantal producten" IsReadOnly="True" Binding="{Binding NumTotalProducts}" /> <DataGridTextColumn Header="Verwerkte producten" IsReadOnly="True" Binding="{Binding NumParsedProducts}" /> <DataGridTextColumn Header="Aantal geschikte boeken" IsReadOnly="True" Binding="{Binding NumSuitableBooks}" /> </DataGrid.Columns> </DataGrid>
When I click the "Parse" button, I want to iterate over the list of file names and analyze each file, reporting the number of products, the products being analyzed, and books on this path. Obviously, I want my user interface to remain responsive, so I want to parse in another thread using Task.Run ().
When the user clicks the button labeled "Parse", the application should begin parsing the files. If I call TaskRun directly in the command_executed command, everything works fine:
private async void ParseFilesCommand_Executed(object sender, ExecutedRoutedEventArgs e) { foreach (var f in OnixFiles) { await Task.Run(() => f.Parse()); } }
Everything is done superfast, ui is updated quickly and remains responsive. However, if I transfer the call to Task.Run () to the ParseFile method, like this:
private async void ParseFilesCommand_Executed(object sender, ExecutedRoutedEventArgs e) { foreach (var f in OnixFiles) { await f.ParseAsync(); } }
The user interface is blocked, not updated until the file has completed parsing and everything will look much slower.
What am I missing? Why does it work as expected if you call Task.Run () in the command_executed handler, but not if you call it in the async method called by this method?
Edit: at the request of Shaamaan, here is a simpler example of what I'm doing (using just thread.sleep to simulate a workload), but unfortunately the sample works the way I originally expected it, failing to isolate the problem I'm experiencing . However, adding it for completeness:
MainWindow.xaml:
<Window x:Class="ThreadingSample.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"> <StackPanel> <DataGrid Grid.ColumnSpan="2" Grid.Row="1" ItemsSource="{Binding Things}" AutoGenerateColumns="False" Height="250" CanUserAddRows="False" CanUserDeleteRows="False" CanUserReorderColumns="False" CanUserResizeColumns="False" CanUserResizeRows="False" CanUserSortColumns="False"> <DataGrid.Columns> <DataGridTextColumn Header="Name" IsReadOnly="True" Binding="{Binding Name}" /> <DataGridTextColumn Header="Value" IsReadOnly="True" Binding="{Binding Value}" /> </DataGrid.Columns> </DataGrid> <Button Click="RightButton_Click">Right</Button> <Button Click="WrongButton_Click">Wrong</Button> </StackPanel> </Window>
MainWindow.xaml.cs:
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace ThreadingSample {
Thing.cs:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace ThreadingSample { public class Thing : INotifyPropertyChanged { private string _name; public string Name { get { return _name; } set { _name = value; RaisePropertyChanged("Name"); } } private int _value; public int Value { get { return _value; } set { _value = value; RaisePropertyChanged("Value"); } } public Thing(int number) { Name = "Thing nr. " + number; Value = 0; } public void Parse() { var progressReporter = new Progress<int>(ReportProgress); HeavyParseMethod(progressReporter); } public async Task ParseAsync() { var progressReporter = new Progress<int>(ReportProgress); await HeavyParseMethodAsync(progressReporter); } private void HeavyParseMethod(IProgress<int> progressReporter) { for (int i = 0; i < 1000; i++) { Thread.Sleep(10); progressReporter.Report(i); } } private async Task HeavyParseMethodAsync(IProgress<int> progressReporter) { await Task.Run(() => { for (int i = 0; i < 1000; i++) { Thread.Sleep(100); progressReporter.Report(i); } }); } private void ReportProgress(int progressValue) { this.Value = progressValue; } private void RaisePropertyChanged(string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } public event PropertyChangedEventHandler PropertyChanged; } }
The only difference between this sample and my real code that I can tell is that my real code parses a bunch of 40 MB files using LINQ to XML, while this example just calls Thread.Sleep ().
Edit 2: I found a terrifying workaround. If I use the second method and call Thread.Sleep (1) after each product is parsed and before calling IProgress.Report () everything works fine. I see that the "NumParsedProducts" counter is incrementing and thatβs it. This is a terrible hack. What does this mean?