I've wpf application, where I load large data (into Wpf DataGrid) and handle multiple events per 1-5 seconds..
I have a performance issue with datagrid - rendering takes too long and freezes my app until it's all rendered.
What I have tried:
QuestionTester.View.QuestionView:
<UserControl x:Class="QuestionTester.View.QuestionView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:QuestionTester.View"
mc:Ignorable="d"
xmlns:Converters="clr-namespace:QuestionTester.Converters"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<Converters:FromDateConverter x:Key="FromDateConv"/>
<Converters:ToDateConverter x:Key="ToDateConv"/>
<Converters:ToTimeConverter x:Key="ToTimeConv"/>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding RecordsCount}"/>
<DataGrid
Grid.Row="1"
ItemsSource="{Binding Models}"
IsReadOnly="True"
AutoGenerateColumns="False"
SnapsToDevicePixels="False"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
EnableRowVirtualization="True"
MaxWidth="2560"
MaxHeight="1600">
<DataGrid.Columns>
<DataGridTemplateColumn Header="CreationTime" ToolTipService.ToolTip="CreationTime">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Margin="4,0,0,0" FlowDirection="LeftToRight" VerticalAlignment="Center">
<ToolTipService.ToolTip>
<TextBlock Text="{Binding Path=CreationTime, Converter={StaticResource ToTimeConv}}"/>
</ToolTipService.ToolTip>
<TextBlock HorizontalAlignment="Left" Text="{Binding Path=CreationTime, Converter={StaticResource ToTimeConv}}"/>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="StartDate" ToolTipService.ToolTip="StartDate">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Margin="4,0,0,0" FlowDirection="LeftToRight" VerticalAlignment="Center">
<ToolTipService.ToolTip>
<TextBlock Text="{Binding Path=StartDate, Converter={StaticResource FromDateConv}}"/>
</ToolTipService.ToolTip>
<TextBlock HorizontalAlignment="Left" Text="{Binding Path=StartDate, Converter={StaticResource FromDateConv}}"/>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="EndDate" ToolTipService.ToolTip="EndDate">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Margin="4,0,0,0" FlowDirection="LeftToRight" VerticalAlignment="Center">
<ToolTipService.ToolTip>
<TextBlock Text="{Binding Path=EndDate, Converter={StaticResource ToDateConv}}"/>
</ToolTipService.ToolTip>
<TextBlock HorizontalAlignment="Left" Text="{Binding Path=EndDate, Converter={StaticResource ToDateConv}}"/>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Header="Guid" Binding="{Binding Path=Guid}"/>
<DataGridTextColumn Header="Key" Binding="{Binding Path=Key}"/>
<DataGridTextColumn Header="Field" Binding="{Binding Path=Field}"/>
<DataGridTextColumn Header="Field1" Binding="{Binding Path=Field1}"/>
<DataGridTextColumn Header="Field2" Binding="{Binding Path=Field2}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>
public partial class QuestionView : UserControl
{
QuestionViewModel _vm;
public QuestionView()
{
InitializeComponent();
_vm = new QuestionViewModel(TaskScheduler.FromCurrentSynchronizationContext());
DataContext = _vm;
}
}
QuestionModel:
public class QuestionModel
{
public static Random _rand = new Random();
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public DateTime CreationTime{ get; set; }
public Guid Guid { get; set; }
public string Key { get; set; }
public int Field { get; set; }
public long Field1 { get; set; }
public double Field2 { get; set; }
public QuestionModel(int num)
{
Key = $"key_{num}";
Field = _rand.Next(1000000);
Field1 = _rand.Next(1000000);
Field2 = _rand.Next(1000000);
Guid = Guid.NewGuid();
CreationTime = DateTime.Now;
if (num % 2 == 0)
{
StartDate = DateTime.Now.AddDays(-1);
EndDate = DateTime.Now.AddDays(1);
}
if (num % 10 == 0)
{
StartDate = DateTime.Now.AddDays(-100);
EndDate = DateTime.Now.AddDays(100);
}
else
{
StartDate = DateTime.Now;
EndDate = DateTime.Now;
}
}
public override string ToString()
{
return $"CreationTime:{CreationTime}";
}
}
QuestionViewModel:
public QuestionViewModel(TaskScheduler currentcontext)
{
_currentcontext = currentcontext;
_cancelToken = Guid.NewGuid();
Models = new BulkObservableCollection<QuestionModel>(_cancelToken, ref _cancelTokensDict, this);
_dispatcherUpdateTimer = new DispatcherTimer(DispatcherPriority.Background);
_dispatcherUpdateTimer.Interval = TimeSpan.FromMilliseconds(1000);
_dispatcherUpdateTimer.Tick += _dispatcherUpdateTimer_Tick;
_dispatcherUpdateTimer.Start();
QuestionModel[] array = new QuestionModel[LENGTH];
for (int i = 0; i < LENGTH; i++)
{
int indx = _rand.Next(RAND);
array[i] = new QuestionModel(indx);
}
Models.AddRange(array);
Models.LoadingFinished();
_thread = new Thread(() =>
{
while (!_cancelTokensDict.Contains(_cancelToken))
{
QuestionModel[] arrayThread = new QuestionModel[LENGTH];
for (int i = 0; i < LENGTH; i++)
{
int indx = _rand.Next(RAND);
arrayThread[i] = new QuestionModel(indx);
}
Task.Factory.StartNew(() =>
{
try
{
for (int i = 0; i < arrayThread.Length; i++)
{
Models.Enqueue(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, arrayThread[i], 0));
}
}
catch (Exception)
{
throw;
}
}, CancellationToken.None, TaskCreationOptions.None, _currentcontext);
Thread.Sleep(1000);
}
});
_thread.IsBackground = true;
_thread.Start();
}
private int Comparison(QuestionModel a, QuestionModel b)
{
if (a == null)
return 1;
if (b == null)
return -1;
var comprand = b.CreationTime.CompareTo(a.CreationTime);
return comprand;
}
private void _dispatcherUpdateTimer_Tick(object sender, EventArgs e)
{
if (Models.IsLoading)
return;
Models.Refresh();
}
int _recordsCound;
public int RecordsCount
{
get => _recordsCound;
set
{
if (_recordsCound != value)
{
_recordsCound = value;
OnPropertyChanged("RecordsCount");
}
}
}
public void UpdateTotalItems(int count)
{
RecordsCount = count;
}
bool _isDisposed = false;
public void Dispose()
{
if (!_isDisposed)
{
_isDisposed = true;
lock (_cancelTokensDict)
{
_cancelTokensDict.Add(_cancelToken);
}
if (_thread != null)
{
try
{
_thread.Join();
}
catch
{
}
_thread = null;
}
if (_dispatcherUpdateTimer != null)
{
_dispatcherUpdateTimer.Stop();
_dispatcherUpdateTimer.Tick -= _dispatcherUpdateTimer_Tick;
_dispatcherUpdateTimer = null;
}
}
}
}
Converters:
public class ToTimeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null)
return null;
DateTime? date = (DateTime)value;
if (date.HasValue)
{
return date.Value.ToString("HH:mm:ss:fff");
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class ToDateConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null)
return null;
DateTime? date = (DateTime)value;
if (date.HasValue)
{
if (date.Value.Date == DateTime.Now.Date)
{
return "Today";
}
else if (date.Value.Date.Date == DateTime.Now.Date.AddDays(1))
{
return "Tomorrow";
}
return date.Value.Date.ToShortDateString();
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
public class FromDateConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value == null)
return null;
DateTime? date = (DateTime)value;
if (date.HasValue)
{
if (date.Value.Date == DateTime.Now.Date)
{
return "Today";
}
else if (date.Value.Date.Date == DateTime.Now.Date.AddDays(-1))
{
return "Yesterday";
}
return date.Value.Date.ToShortDateString();
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
BulkObservableCollection:
public class BulkObservableCollection<T> : IDisposable, INotifyCollectionChanged, INotifyPropertyChanged, IEnumerable<T>
{
ConcurrentQueue<NotifyCollectionChangedEventArgs> _losts = new ConcurrentQueue<NotifyCollectionChangedEventArgs>();
Guid? _cancelToken = null;
private HashSet<Guid> _cancelTokensDict = null;
bool _isInAddRange = false;
bool _isLoading = true;
public bool IsLoading
{
get
{
lock (_locker)
{
return _isLoading;
}
}
}
const string COUNT = "Count";
const string ITEMS = "Item[]";
public event NotifyCollectionChangedEventHandler CollectionChanged;
public event PropertyChangedEventHandler PropertyChanged;
List<T> _items = new List<T>();
IUpdateTotalItems _updateTotalItems = null;
public int Count => _items.Count;
public BulkObservableCollection()
{
}
public BulkObservableCollection(Guid cancelToken, ref HashSet<Guid> cancelTokensDict, IUpdateTotalItems updateTotalItems = null)
{
_cancelToken = cancelToken;
_cancelTokensDict = cancelTokensDict;
_updateTotalItems = updateTotalItems;
}
readonly object _locker = new object();
public void LoadingFinished()
{
lock (_locker)
{
_isLoading = false;
BeginUpdate();
}
}
public void Refresh()
{
EndUpdate();
UpdateTotalItems(_items.Count);
BeginUpdate();
}
#region INotifyPropertyChanged
private void OnPropertyChanged(string propertyName)
{
if (_isDispose)
return;
if (!_isInAddRange)
{
try
{
var ev = this.PropertyChanged;
if (ev != null)
{
ev(this, new PropertyChangedEventArgs(propertyName));
}
}
catch (Exception ex)
{
Trace.WriteLine($"OnPropertyChanged:: Error on FirePropertyChanged propertyName = {propertyName}, Error: {ex.Message}");
}
}
}
#endregion INotifyPropertyChanged
#region INotifyCollectionChanged
private void OnCollectionChanged(NotifyCollectionChangedAction action, object item, int index)
{
if (_isDispose)
return;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, item, index));
}
private void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (_isDispose)
return;
if (!_isInAddRange)
{
try
{
var ev = this.CollectionChanged;
if (ev != null)
{
ev(this, e);
}
}
catch (Exception ex)
{
Trace.WriteLine($"OnPropertyChanged:: Error on CollectionChanged Error: {ex.Message}");
}
}
}
#endregion INotifyCollectionChanged
#region Add
public void Add(T item)
{
if (_isDispose)
return;
if (_cancelTokensDict != null && _cancelToken.HasValue && _cancelTokensDict.Contains(_cancelToken.Value))
return;
InsertItem(0, item);
}
public void Insert(int index, T item)
{
if (_isDispose)
return;
if (_cancelTokensDict != null && _cancelToken.HasValue && _cancelTokensDict.Contains(_cancelToken.Value))
return;
InsertItem(index, item);
}
private void InsertItem(int index, T item)
{
if (_isDispose)
return;
if (_cancelTokensDict != null && _cancelToken.HasValue && _cancelTokensDict.Contains(_cancelToken.Value))
return;
_items.Insert(index, item);
OnPropertyChanged(COUNT);
OnPropertyChanged(ITEMS);
OnCollectionChanged(NotifyCollectionChangedAction.Add, item, index);
}
public void AddRange(T[] array)
{
if (_isDispose)
return;
if (_cancelTokensDict != null && _cancelToken.HasValue && _cancelTokensDict.Contains(_cancelToken.Value))
return;
if (array == null)
throw new ArgumentNullException("array");
BeginUpdate();
for (int i = 0; i < array.Length; i++)
{
if (_cancelTokensDict != null && _cancelToken.HasValue && _cancelTokensDict.Contains(_cancelToken.Value))
break;
var item = array[i];
if (_cancelTokensDict != null && _cancelToken.HasValue && _cancelTokensDict.Contains(_cancelToken.Value))
break;
Add(item);
}
EndUpdate();
}
public void AddRange(List<T> list)
{
if (_isDispose)
return;
if (_cancelTokensDict != null && _cancelToken.HasValue && _cancelTokensDict.Contains(_cancelToken.Value))
return;
if (list == null)
throw new ArgumentNullException("list");
BeginUpdate();
for (int i = 0; i < list.Count; i++)
{
if (_cancelTokensDict != null && _cancelToken.HasValue && _cancelTokensDict.Contains(_cancelToken.Value))
break;
var item = list[i];
if (_cancelTokensDict != null && _cancelToken.HasValue && _cancelTokensDict.Contains(_cancelToken.Value))
break;
Add(item);
}
EndUpdate();
}
#endregion Add
#region Remove
public bool Remove(T item)
{
if (_isDispose)
return false;
if (_cancelTokensDict != null && _cancelToken.HasValue && _cancelTokensDict.Contains(_cancelToken.Value))
return false;
int index = _items.IndexOf(item);
if (index < 0) return false;
RemoveItem(item, index);
return true;
}
private void RemoveItem(T removedItem, int index)
{
_items.RemoveAt(index);
OnPropertyChanged(COUNT);
OnPropertyChanged(ITEMS);
OnCollectionChanged(NotifyCollectionChangedAction.Remove, removedItem, index);
}
#endregion Remove
#region Clear
public void Clear()
{
if (_isDispose)
return;
if (_cancelTokensDict != null && _cancelToken.HasValue && _cancelTokensDict.Contains(_cancelToken.Value))
return;
_items.Clear();
OnPropertyChanged(COUNT);
OnPropertyChanged(ITEMS);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
#endregion Clear
#region GetEnumerator
public IEnumerator<T> GetEnumerator()
{
return _items.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)_items).GetEnumerator();
}
#endregion GetEnumerator
#region Dispose
bool _isDispose = false;
public void Dispose()
{
if (!_isDispose)
{
_isDispose = true;
if (_cancelToken != null && _cancelTokensDict != null)
{
_cancelTokensDict.Add(_cancelToken.Value);
}
if (_items != null)
{
_items.Clear();
}
while (_losts.TryDequeue(out NotifyCollectionChangedEventArgs item))
{
}
}
}
#endregion Dispose
public void BeginUpdate()
{
if (_isDispose)
return;
_isInAddRange = true;
}
public void EndUpdate()
{
if (_isDispose)
return;
_isInAddRange = false;
OnPropertyChanged(COUNT);
OnPropertyChanged(ITEMS);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public void UpdateTotalItems(int count)
{
if (_updateTotalItems != null)
{
_updateTotalItems.UpdateTotalItems(count);
}
}
public void Enqueue(NotifyCollectionChangedEventArgs e)
{
bool sendToHandle = false;
lock (_locker)
{
if (!_isLoading)
{
sendToHandle = true;
}
}
if (sendToHandle)
{
Handle(e);
}
else
{
lock (_losts)
{
_losts.Enqueue(e);
}
}
}
public void PopulateFromLost()
{
if (_isDispose)
return;
if (_cancelTokensDict != null && _cancelToken.HasValue && _cancelTokensDict.Contains(_cancelToken.Value))
return;
lock (_losts)
{
while (_losts.TryDequeue(out NotifyCollectionChangedEventArgs e))
{
if (_cancelTokensDict != null && _cancelToken.HasValue && _cancelTokensDict.Contains(_cancelToken.Value))
break;
Handle(e);
}
}
}
private void Handle(NotifyCollectionChangedEventArgs e)
{
if (_cancelTokensDict != null && _cancelToken.HasValue && _cancelTokensDict.Contains(_cancelToken.Value))
return;
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
{
foreach (var item in e.NewItems)
{
try
{
if (_cancelTokensDict != null && _cancelToken.HasValue && _cancelTokensDict.Contains(_cancelToken.Value))
break;
_items.Insert(e.NewStartingIndex, (T)item);
}
catch (Exception ex)
{
Trace.WriteLine($"Handle:Add, e.NewStartingIndex:{e.NewStartingIndex}, Error:{ex.Message}");
}
}
}
break;
case NotifyCollectionChangedAction.Remove:
{
foreach (var item in e.OldItems)
{
try
{
if (_cancelTokensDict != null && _cancelToken.HasValue && _cancelTokensDict.Contains(_cancelToken.Value))
break;
_items.Remove((T)item);
}
catch (Exception ex)
{
Trace.WriteLine($"Handle:Remove, Error:{ex.Message}");
}
}
}
break;
case NotifyCollectionChangedAction.Move:
{
try
{
if (_cancelTokensDict != null && _cancelToken.HasValue && _cancelTokensDict.Contains(_cancelToken.Value))
break;
var itemMoved = (T)e.OldItems[0];
_items.Remove(itemMoved);
_items.Insert(e.NewStartingIndex, itemMoved);
}
catch (Exception ex)
{
Trace.WriteLine($"Handle:Move, e.OldStartingIndex:{e.OldStartingIndex}, e.NewStartingIndex:{e.NewStartingIndex}, Error:{ex.Message}");
}
}
break;
}
}
}