Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF Application DataGrid Control Window Switching Lag when lots of Rows in Non-Virtualized DataGrid

We have a small .NET 4.0 DataGrid WPF demo with the code posted below. It consists of a non-virtualized DataGrid with 30 columns and 3000 rows. It is non-virtualized, due to us requiring grouping capabilities, which do not support virtualization.

When you run this app, and switch between it and other windows, there is a noticeable lag (of about 1 second). This only occurs when the window is reactivated - once activated, clicking around inside has no associated delay.

We have profiled this lag that occurs on window reactivation using the performance analyzer and have found that there are a lot of dependency property notifications being triggered when the window is brought back into focus. We do not know why this happens, and it seems unnecessary.

We find this delay is proportional to the number of rows in the DataGrid. Does anyone know how we can eliminate or reduce this lag?

UPDATE: It seems like the focus lag occurs even when staying inside the application but focusing on another control such as a textbox outside the grid. Therefore we now know it's not a Window switching problem, but one caused by change in focus, but are still unsure of the exact cause.

(MainWindow.xaml)

<Window x:Class="WpfApplication20.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">
    <Grid>
        <DataGrid Name="dataGrid" VirtualizingStackPanel.IsVirtualizing="False" AutoGenerateColumns="True"/>
    </Grid>
</Window>

(MainWindow.xaml.cs)

using System.Collections.Generic;
using System.Windows;

namespace WpfApplication20
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            List<Row> rows = new List<Row>();

            for(int i = 0; i < 3000; i++)
            {
                Row row = new Row(i);
                rows.Add(row);
            }

            dataGrid.ItemsSource = rows;
        }
    }

    public class Row
    {
        public double Double1 { get; set; }
        public double Double2 { get; set; }
        public double Double3 { get; set; }
        public double Double4 { get; set; }
        public double Double5 { get; set; }
        public double Double6 { get; set; }
        public double Double7 { get; set; }
        public double Double8 { get; set; }
        public double Double9 { get; set; }
        public double Double10 { get; set; }
        public double Double11 { get; set; }
        public double Double12 { get; set; }
        public double Double13 { get; set; }
        public double Double14 { get; set; }
        public double Double15 { get; set; }
        public double Double16 { get; set; }
        public double Double17{ get; set; }
        public double Double18 { get; set; }
        public double Double19 { get; set; }
        public double Double20 { get; set; }
        public double Double21 { get; set; }
        public double Double22 { get; set; }
        public double Double23 { get; set; }
        public double Double24 { get; set; }
        public double Double25 { get; set; }
        public double Double26 { get; set; }
        public double Double27 { get; set; }
        public double Double28 { get; set; }
        public double Double29 { get; set; }
        public double Double30 { get; set; }

        public Row(double d)
        {
            Double1 = d;
            Double2 = d + 1;
            Double3 = d + 2;
            Double4  = d + 3;
            Double5  = d + 4;
            Double6  = d + 5;
            Double7  = d + 6;
            Double8  = d + 7;
            Double9  = d + 8;
            Double10 = d + 9;
            Double11 = d + 10;
            Double12 = d + 11;
            Double13 = d + 12;
            Double14 = d + 13;
            Double15 = d + 14;
            Double16 = d + 15;
            Double17 = d + 16;
            Double18 = d + 17;
            Double19 = d + 18;
            Double20 = d + 19;
            Double21 = d + 20;
            Double22 = d + 21;
            Double23 = d + 22;
            Double24 = d + 23;
            Double25 = d + 24;
            Double26 = d + 25;
            Double27 = d + 26;
            Double28 = d + 27;
            Double29 = d + 28;
            Double30 = d + 29;
        }             
    }                 
}

(Group Style - optionally enabled by putting inside DataGrid XML):

<!--<DataGrid.GroupStyle>
                <GroupStyle>
                    <GroupStyle.ContainerStyle>
                        <Style TargetType="{x:Type GroupItem}">
                            <Setter Property="Template">
                                <Setter.Value>
                                    <ControlTemplate TargetType="{x:Type GroupItem}">
                                        <Border BorderBrush="DarkGray" BorderThickness="1" Padding="4,0" >
                                            <Expander VerticalContentAlignment="Center" IsExpanded="True">
                                                <Expander.Header>
                                                    <Canvas>
                                                        <StackPanel Orientation="Horizontal" Canvas.Top="-11" Canvas.Left="4">
                                                            <Label Content="{Binding Name}" Visibility="{Binding DataContext.ShowGroupHeaderVisibility, RelativeSource={RelativeSource AncestorType=DataGrid}}"/>
                                                            <Label Content="{Binding ItemCount}" Visibility="{Binding DataContext.ShowGroupCountVisibility, RelativeSource={RelativeSource AncestorType=DataGrid}}"/>
                                                        </StackPanel>
                                                    </Canvas>
                                                </Expander.Header>
                                                <ItemsPresenter/>
                                            </Expander>
                                        </Border>
                                    </ControlTemplate>
                                </Setter.Value>
                            </Setter>
                        </Style>
                    </GroupStyle.ContainerStyle>
                </GroupStyle>
            </DataGrid.GroupStyle>-->
like image 278
Gaurav Sharma Avatar asked Jun 23 '11 09:06

Gaurav Sharma


3 Answers

I would try using Dependecy properties instead of regular CLR properties in the row class, (or a rowViewModel class) especially since there are so many columns.

Are you using any styles/templates? i assume the code you've posted is simplified somewhat :) also, you're saying you dont see this delay as the window opens? only when its brought back into focus?

-edit-

Ok ive done some tests and also conclude that the binding is not the issue. I did seem to be able to reduce the cpu peak when the window gets focus by defining a custom cell style (and setting it to shared):

<Window>
  <Grid>
    <Grid.Resources>
      <Style TargetType="DataGridCell"
             x:Key="cs"
             x:Shared="True">
        <Setter Property="Content"
                Value="{Binding}" />
      </Style>
    </Grid.Resources>
    <DataGrid Name="dataGrid"
              VirtualizingStackPanel.IsVirtualizing="False"
              CellStyle="{StaticResource cs}">
    </DataGrid>
  </Grid>
</Window>

Still, the program uses massive amounts of memory, almost 1 gb.. also, scrolling sideways is slow on my machine for some reason, but scrolling downwards is fine. i dont think the built in datagrid is really suitable for this amount of data, atleast in non virualized mode. still you can try out the cell style and see if that improves things

like image 156
aL3891 Avatar answered Nov 16 '22 20:11

aL3891


Solution is quite simple. Use a grid inherited from DataGrid, override OnIsKeyboardFocusWithinChanged() and DO NOT call base.OnIsKeyboardFocusWithinChangedand():

public class MyGrid : DataGrid
{
    protected override void OnIsKeyboardFocusWithinChanged(DependencyPropertyChangedEventArgs e)
    {
        // Do not call base.OnIsKeyboardFocusWithinChanged(e);
    }
}

Performance should be much better. At least for your sample code :).

The following stack trace is the bottleneck reason:

0012dd90 58cd5818 System.Windows.DependencyObject.LookupEntry(Int32)
0012ddb0 58cd923b System.Windows.DependencyObject.GetValueSource(System.Windows.DependencyProperty, System.Windows.PropertyMetadata, Boolean ByRef, Boolean ByRef, Boolean ByRef, Boolean ByRef, Boolean ByRef)
0012dde0 58cd91f2 System.Windows.DependencyObject.GetValueSource(System.Windows.DependencyProperty, System.Windows.PropertyMetadata, Boolean ByRef)
0012de04 56c78477 System.Windows.VisualStateManager.GetVisualStateGroupsInternal(System.Windows.FrameworkElement)
0012de14 56c504e7 System.Windows.VisualStateManager.GoToStateCommon(System.Windows.FrameworkElement, System.Windows.FrameworkElement, System.String, Boolean)
0012de38 56c504ac System.Windows.VisualStateManager.GoToState(System.Windows.FrameworkElement, System.String, Boolean)
0012de50 5753506a System.Windows.Controls.DataGridCell.ChangeVisualState(Boolean)
0012de64 56c5012c System.Windows.Controls.Control.UpdateVisualState(Boolean)
0012de74 575353fa System.Windows.Controls.DataGridCell.NotifyPropertyChanged(System.Windows.DependencyObject, System.String, System.Windows.DependencyPropertyChangedEventArgs, System.Windows.Controls.DataGridNotificationTarget)
0012debc 5753b4d0 System.Windows.Controls.Primitives.DataGridCellsPresenter.NotifyPropertyChanged(System.Windows.DependencyObject, System.String, System.Windows.DependencyPropertyChangedEventArgs, System.Windows.Controls.DataGridNotificationTarget)
0012df04 5752a1d5 System.Windows.Controls.DataGridRow.NotifyPropertyChanged(System.Windows.DependencyObject, System.String, System.Windows.DependencyPropertyChangedEventArgs, System.Windows.Controls.DataGridNotificationTarget)
0012df4c 5751becd System.Windows.Controls.DataGrid.NotifyPropertyChanged(System.Windows.DependencyObject, System.String, System.Windows.DependencyPropertyChangedEventArgs, System.Windows.Controls.DataGridNotificationTarget)
0012dfa4 57512d98 System.Windows.Controls.DataGrid.OnIsKeyboardFocusWithinChanged(System.Windows.DependencyObject, System.Windows.DependencyPropertyChangedEventArgs)

Visual state manager updates every single cell on IsKeyboardFocusWithinChanged event.

BTW interestingly enough if you use ILSpy to decompile DataGrid.OnIsKeyboardFocusWithinChanged() you won't find it there as if it does not override default behavior from base class. WinDbg shows quite the opposite.

Hope this answers your question, but I can easily anticipate many more problems without virtualization enabled.

UPDATE How to find the cause? Just attach with debugger to your process and every time you see significant slowness break into debugger and check the stacktrace of UI thread. If the same stack trace appears often - that's probably your villain. I used WinDbg to track down this one. VS debugger should be effective as well.

like image 36
Anvaka Avatar answered Nov 16 '22 18:11

Anvaka


For those interested, we solved resolved this issue by defining separate focus scopes within our application. By default the entire Window is the focus scope. Creating separate ones and making sure we set the focus scopeto something other than the grid before the window deactivates and activates again ensures it goes and comes back snappily. We're assuming there is some undocumented functionality in WPF that produces this phenomena.

like image 40
Gaurav Sharma Avatar answered Nov 16 '22 20:11

Gaurav Sharma