Click here to Skip to main content
15,883,798 members
Please Sign up or sign in to vote.
0.00/5 (No votes)
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>  


C#
public partial class QuestionView : UserControl
{
    QuestionViewModel _vm;
    public QuestionView()
    {
        InitializeComponent();
        _vm = new QuestionViewModel(TaskScheduler.FromCurrentSynchronizationContext());
        DataContext = _vm;
    }
}


QuestionModel:

C#
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:

C#
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));
                        }
                       // Models.AddRange(arrayThread);
                    }
                    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:

C#
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:

C#
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;
       // int index = _items.Count;
        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))
            { // do nothing
            }
        }
    }

    #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;
        }
    }
}
Posted
Comments
Richard MacCutchan 1-Sep-22 9:09am    
You need to do the data capture in a background thread. Doing it on the main thread means that all screen updating is halted until the task completes.
Member 15691986 1-Sep-22 10:00am    
the data is captured in a background thred, on main thread i raise OnPropertyChanged and CollectionChanged.Reset only...
I've added a full example
[no name] 1-Sep-22 11:45am    
"I load large data". There's your problem. "Data" is not "information". Random data even less so.
Member 15691986 1-Sep-22 14:33pm    
The loading large data isn't the problem here because i loading the data in background thread.. as you can see in the example code, the problem is case of raising events of OnPropertyChanged ("Count" and "Items[]") that notify to the datagrid to render..
[no name] 2-Sep-22 12:40pm    
In my opinion, you've missed the whole point of an observable collection (OB); you also didn't implement INotifyPropertyChanged "properly". One creates a "data object" that implements INotifyPropertyChanged and adds that to the OB; the OB is assigned to the DataGrid. That's it. The OB events tell you when things happen in the OB. Your BG worker adds to a concurrent queue that another worker adds to the OB at it's leisure; if necessary. All your code is mostly overhead.

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900