Reputation: 81
1- Copy and paste the following codes into MainWindow.xaml file.
<Window x:Class="WpfApplication1.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" Loaded="Window_Loaded">
<Grid>
<DataGrid x:Name="DataGrid1"/>
</Grid>
</Window>
2- Copy and paste the following codes into MainWindow.xaml.cs file.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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;
using System.ComponentModel;
using System.Threading;
namespace WpfApplication1
{
public partial class MainWindow : Window
{
BackgroundWorker BackgroundWorker1 = new BackgroundWorker();
BackgroundWorker BackgroundWorker2 = new BackgroundWorker();
System.Data.DataTable DataTable1 = new System.Data.DataTable();
public MainWindow()
{
InitializeComponent();
BackgroundWorker1.DoWork += BackgroundWorker1_DoWork;
BackgroundWorker2.DoWork += BackgroundWorker2_DoWork;
}
void Window_Loaded(object sender, RoutedEventArgs e)
{
BackgroundWorker1.RunWorkerAsync();
BackgroundWorker2.RunWorkerAsync();
}
private void BackgroundWorker1_DoWork(System.Object sender, System.ComponentModel.DoWorkEventArgs e)
{
Dispatcher.Invoke(() =>
{
Window1 myWindow1 = new Window1();
myWindow1.ShowDialog();
});
}
private void BackgroundWorker2_DoWork(System.Object sender, System.ComponentModel.DoWorkEventArgs e)
{
for (int i = 1; i <= 7; i++)
DataTable1.Columns.Add();
for (int i = 1; i <= 1048576; i++)
DataTable1.Rows.Add(i);
Dispatcher.Invoke(() =>
{
DataGrid1.ItemsSource = DataTable1.DefaultView;
});
}
}
}
3- Create a new Window and named it as Window1.
4- Copy and paste the following codes into Window1.xaml file.
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="1000" ContentRendered="Window_ContentRendered">
<Grid>
<ProgressBar x:Name="ProgressBar1" Height="25" Width="850"/>
</Grid>
</Window>
5- Copy and paste the following codes into Window1.xaml.cs file.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
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.Shapes;
namespace WpfApplication1
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
private void Window_ContentRendered(object sender, EventArgs e)
{
ProgressBar1.IsIndeterminate = true;
}
}
}
6- When you run this project you will see that ProgressBar1 is freezing for two or three seconds while the following line runs because of adding 1048576 rows to the DataGrid. (Huge rows)
DataGrid1.ItemsSource = DataTable1.DefaultView;
I dont want ProgressBar1 is freezing.
So why BackgroundWorker is not able to prevent ProgressBar freezing?
Upvotes: 3
Views: 188
Reputation: 29018
The problem with your approach is that DataTable
doesn't implement INotifyPropertyChanged
. Therefore adding a row won't update the view (the binding to be more precisely). To force a refresh you have to reset the ItemsSource
each time a row was added or reset it after all n rows were created.
This results in a UI thread which is busy to draw e.g., 1,048,576 rows * 7 columns at once - no resources left to draw the ProgressBar
and it will freeze.
This makes the DataTable
a bad choice when dealing huge amount of data while you can't tolerate freezing time.
The solution would be to choose a data source that allows to add data row by row without forcing to redraw the complete view.
The following solutions eliminate freezing completely as long as virtualization is enabled (which is true for the DataGrid
by default) and the number of columns doesn't exceed a critical count (virtualization only works for rows and not columns):
ObservableCollection
allows to draw only the new/changed rows. It will raise INotifyCollectionChanged.CollectionChanged
which triggers the DataGrid
to add/remove/move the changed items only:
MainWindow.xaml
<Window>
<StackPanel>
<DataGrid x:Name="DataGrid1"
AutoGenerateColumns="False"
ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=local:MainWindow}, Path=DataTableRowCollection}"/>
</StackPanel>
</Window>
MainWindow.xaml.cs
public ObservableCollection<RowItem> DataTableRowCollection { get; } = new ObservableCollection<RowItem>();
private async void BackgroundWorker2_DoWork(System.Object sender, System.ComponentModel.DoWorkEventArgs e)
{
for (int i = 1; i <= 1048576; i++)
{
// Use Dispatcher because
// INotifyCollectionChanged.CollectionChanged is not raised on the UI thread
// (opposed to INotifyPropertyChanged.PropertyChanged)
await Application.Current.Dispatcher.InvokeAsync(
() => this.DataTableRowCollection.Add(new RowItem(i)),
DispatcherPriority.Background);
}
}
RowItem.cs
public class RowItem
{
public RowItem(int value)
{
this.Value = value;
}
public int Value { get; set; }
}
Note
The disadvantage is that your column count is coupled to the data model. Adding columns to the DataGrid
at runtime isn't possible except you also create types dynamically at runtime (using reflection) or use nested data collections to represent columns.
But adding a column would always result in a redraw of the complete table (all new cells of the newly added columns at best), except when used virtualization.
When dynamic column count is a requirement you could directly handle the DataGrid
using C# encapsulated in an extended class of DataGrid
or in the code-behind of the hosting control. But you definitely shouldn't handle the column or row containers in the view model.
The idea is to add DataGridColumn
elements to the DataGrid.Columns
collection manually. The following example draws only all cells of the newly added columns at once.
The following example uses a Button
to add new column dynamically on each time it is pressed (after the DataGrid
was initialized with 1,048,576 rows):
MainWindow.xaml
<Window>
<StackPanel>
<Button Content="Add Column" Click="AddColumn_OnClick"/>
<DataGrid x:Name="DataGrid1"
AutoGenerateColumns="False"
ItemsSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=local:MainWindow}, Path=DataTableRowCollection}"/>
</StackPanel>
</Window>
MainWindow.xaml.cs
private async void BackgroundWorker2_DoWork(System.Object sender, System.ComponentModel.DoWorkEventArgs e)
{
// Create 7 columns in the view
for (int columnIndex = 0; columnIndex < 7; columnIndex++)
{
await Application.Current.Dispatcher.InvokeAsync(
() =>
{
var textColumn = new DataGridTextColumn
{
Header = $"Column {columnIndex + 1}",
Binding = new Binding($"ColumnItems[{columnIndex}].Value")
};
this.DataGrid1.Columns.Add(textColumn);
},
DispatcherPriority.Background);
}
// Create the data models for 1,048,576 rows with 7 columns
for (int rowCount = 0; rowCount < 1048576; rowCount++)
{
int count = rowCount;
await Application.Current.Dispatcher.InvokeAsync(() =>
{
var rowItem = new RowItem();
for (; count < 7 + rowCount; count ++)
{
rowItem.ColumnItems.Add(new ColumnItem((int) Math.Pow(2, count)));
}
this.DataTableRowCollection.Add(rowItem);
}, DispatcherPriority.Background);
}
}
private void AddColumn_OnClick(object sender, RoutedEventArgs e)
{
int newColumnIndex = this.DataTableRowCollection.First().ColumnItems.Count;
this.DataGrid1.Columns.Add(
new DataGridTextColumn()
{
Header = $"Dynamically Added Column {newColumnIndex}",
Binding = new Binding($"ColumnItems[{newColumnIndex}].Value")
});
int rowCount = 0;
// Add a new column data model to each row data model
foreach (RowItem rowItem in this.DataTableRowCollection)
{
var columnItem = new ColumnItem((int) Math.Pow(2, newColumnIndex + rowCount++);
rowItem.ColumnItems.Add(columnItem);
}
}
RowItem.cs
public class RowItem
{
public RowItem()
{
this.ColumnItems = new ObservableCollection<ColumnItem>();
}
public ObservableCollection<ColumnItem> ColumnItems { get; }
}
ColumnItem.cs
public class ColumnItem
{
public ColumnItem(int value)
{
this.Value = value;
}
public int Value { get; }
}
Upvotes: 2
Reputation: 7325
In your case I think the problem is a generating the DefaultView
on the GUI thread. Move it to the BG-Worker:
private void BackgroundWorker2_DoWork(System.Object sender, System.ComponentModel.DoWorkEventArgs e)
{
for (int i = 1; i <= 7; i++)
DataTable1.Columns.Add();
for (int i = 1; i <= 1048576; i++)
DataTable1.Rows.Add(i);
var dv = DataTable1.DefaultView; //generating the default view takes ~ 2-3 sec.
Dispatcher.Invoke(() =>
{
DataGrid1.ItemsSource = dv;
});
}
Do not forget to set EnableRowVirtualization="True"
and MaxHeight
in XAML for the DataGrid
.
Upvotes: 1
Reputation: 1275
When the one and only UI thread is busy binding data to the DataGrid
, the UI will appear to freeze. There is nothing much to fix there other than to avoid binding huge amount of data or using data virtualization. However you can still optimize this code by making things asynchronous.
private Task<DataView> GetDataAsync()
{
return Task.Run(() =>
{
for (int i = 1; i <= 7; i++)
DataTable1.Columns.Add();
for (int i = 1; i <= 1048576; i++)
DataTable1.Rows.Add(i);
return DataTable1.DefaultView;
});
}
private void BackgroundWorker2_DoWork(System.Object sender, System.ComponentModel.DoWorkEventArgs e)
{
Dispatcher.Invoke((Action)(async () =>
{
DataGrid1.ItemsSource = await GetDataAsync();
}));
}
Upvotes: 1
Reputation: 2224
Try putting the items into an ObservableCollection first. The pause is much more brief. It don't know that you can totally eliminate it since grid needs to be bound on the UI thread.
private void BackgroundWorker2_DoWork(System.Object sender, System.ComponentModel.DoWorkEventArgs e)
{
for (int i = 1; i <= 7; i++)
DataTable1.Columns.Add();
for (int i = 1; i <= 1048576; i++)
DataTable1.Rows.Add(i);
var col = new ObservableCollection<MyItem>();
foreach (DataRow row in DataTable1.Rows) col.Add(new MyItem(row));
Dispatcher.Invoke(() =>
{
DataGrid1.ItemsSource = col;
});
}
public class MyItem
{
public MyItem() { }
public MyItem(DataRow row)
{
int.TryParse(row[0].ToString(),out int item1);
int.TryParse(row[1].ToString(), out int item2);
int.TryParse(row[2].ToString(), out int item3);
int.TryParse(row[3].ToString(), out int item4);
int.TryParse(row[4].ToString(), out int item5);
int.TryParse(row[5].ToString(), out int item6);
int.TryParse(row[6].ToString(), out int item7);
}
public int item1 { get; set; }
public int item2 { get; set; }
public int item3 { get; set; }
public int item4 { get; set; }
public int item5 { get; set; }
public int item6 { get; set; }
public int item7 { get; set; }
}
Upvotes: 1