WPF DataGrid Scrolling Performance

by clovett15. July 2012 12:07

I was investigating some sluggish scrolling performance in one of my apps that was using WPF DataGrid.  I used the Visual Studio Profiler to find some hot spots and found that “PrepareContainerForItemOverride” was a good place to instrument where most of the work was happening.  So I overrode this method, and wrapped it in an ETW event.  ETW events are awesome, they have very low overhead and there's lots of tools out there to process the logs they create.  I have a tool that produced the following graph while scrolling my grid:

Notice there is not much gap between each green block, which means scrolling is maxed out, which is why it seems sluggish.  If we can shrink the green blocks scrolling will get smoother.   I’ve noticed considerable degradation when my rows were smaller – probably because it has to “prepare” a lot more items while scrolling which means a lot more data binding overhead. 

I made one change and the time dropped considerably, the average time around PrepareContainerForItemOverride dropped from 2.1ms to 1.4ms (about 33% improvement) and scrolling does seem smoother.  Before the fix it took 20 seconds to page scroll to the bottom of my grid with 4121 rows; after the fix it took about 13 seconds (which is also about 33% improvement as expected).

 

The fix was to replace the following expensive data binding DataGridCell Template:

 

<Border x:Name="CellBorder">

    <ContentPresenter SnapsToDevicePixels="True"/>

</Border>

<ControlTemplate.Triggers>

    <DataTrigger Binding="{Binding Path=IsReconciling}" Value="True">                               

        <Setter Property="Background" Value="{DynamicResource ReconciledRowBackgroundBrush}" TargetName="CellBorder"/>

    </DataTrigger>

    <Trigger Property="IsSelected" Value="true">

         <Setter Property="Background" Value="{DynamicResource RowSelectedBrush}" TargetName="CellBorder"/>

         <Setter Property="Foreground" Value="{DynamicResource RowSelectedTextBrush}"/>

     </Trigger>

     <DataTrigger Binding="{Binding Path=Unaccepted}" Value="True">

         <Setter Property="FontWeight" Value="Bold"/>              

     </DataTrigger>

     <DataTrigger Binding="{Binding Path=Unaccepted}" Value="False">

         <Setter Property="FontWeight" Value="Normal"/>

     </DataTrigger>

     <DataTrigger Binding="{Binding Path=IsDown}" Value="True">

         <Setter Property="Foreground" Value="Red"/>

     </DataTrigger>

     <DataTrigger Binding="{Binding Path=IsReadOnly}" Value="True">

         <Setter Property="Foreground" Value="Gray"/>

     </DataTrigger>

</ControlTemplate.Triggers>

 

With this custom control:

 

<views:TransactionCell >

    <ContentPresenter SnapsToDevicePixels="True"/>

</views:TransactionCell>

 

Where all the binding above is done in code instead without using BindingExpressions.  The code unfortunately is quite verbose and not much fun to write, but it is pretty mechanical: 

 

    ///<summary>

    /// This class is here for performance reasons only.  The DataGridCell Template was too slow otherwise.

    ///</summary>   

    publicclassTransactionCell : Border

    {

        public TransactionCell()

        {

            this.DataContextChanged += newDependencyPropertyChangedEventHandler(OnDataContextChanged);

        }

 

        Transaction context;

        DataGridCell cell;

 

        protectedoverridevoid OnVisualParentChanged(DependencyObject oldParent)

        {

            DataGridCell cell = this.GetParentObject() asDataGridCell;

            SetCell(cell);

            base.OnVisualParentChanged(oldParent);

        }

 

        void SetCell(DataGridCell newCell)

        {

            if (this.cell != null)

            {

                cell.Selected -= newRoutedEventHandler(OnCellSelectionChanged);

                cell.Unselected -= newRoutedEventHandler(OnCellSelectionChanged);

            }

            this.cell = newCell;

            if (cell != null)

            {

                cell.Selected += newRoutedEventHandler(OnCellSelectionChanged);

                cell.Unselected += newRoutedEventHandler(OnCellSelectionChanged);

            }

        }

 

        void OnCellSelectionChanged(object sender, RoutedEventArgs e)

        {

            UpdateBackground();

            UpdateForeground();

        }

 

        void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)

        {

            ClearContext();

            Transaction t = e.NewValue asTransaction;

            if (t != null)

            {

                SetContext(t);

            }

           

            UpdateBackground();

            UpdateForeground();

            UpdateFontWeight();

        }

 

        void ClearContext()

        {

            if (this.context != null)

            {

                this.context.PropertyChanged -= newPropertyChangedEventHandler(OnPropertyChanged);

            }

        }

 

        void SetContext(Transaction transaction)

        {

            this.context = transaction;

            if (this.context != null)

            {

                this.context.PropertyChanged += newPropertyChangedEventHandler(OnPropertyChanged);

            }

        }

 

        void OnPropertyChanged(object sender, PropertyChangedEventArgs e)

        {

            switch (e.PropertyName)

            {

                case"IsReconciling":

                    UpdateBackground();

                    break;

                case"Unaccepted":

                    UpdateFontWeight();

                    break;

                case"IsDown":

                    UpdateForeground();

                    break;

                case"IsReadOnly":

                    UpdateForeground();

                    break;

            }

        }

 

        void UpdateBackground()

        {

            /*

               <DataTrigger Binding="{Binding Path=IsReconciling}" Value="True">

                    <Setter Property="Background" Value="{DynamicResource ReconciledRowBackgroundBrush}" TargetName="CellBorder"/>

                </DataTrigger

                <Trigger Property="IsSelected" Value="true">

                    <Setter Property="Background" Value="{DynamicResource RowSelectedBrush}" TargetName="CellBorder"/>

                </Trigger>

             */

            bool isReconciling = false;

            bool isSelected = IsSelected;

 

            if (this.context != null)

            {

                isReconciling = this.context.IsReconciling;

            }

            if (isSelected)

            {

                this.SetResourceReference(Border.BackgroundProperty, "RowSelectedBrush");

            }

            elseif (isReconciling)

            {

                this.SetResourceReference(Border.BackgroundProperty, "ReconciledRowBackgroundBrush");

            }

            else

            {

                this.ClearValue(Border.BackgroundProperty);               

            }

        }

 

        bool IsSelected

        {

            get

            {

                return (cell != null) ? cell.IsSelected : false;

            }

        }

 

        void UpdateForeground()

        {

            /*

               <Trigger Property="IsSelected" Value="true">

                    <Setter Property="Foreground" Value="{DynamicResource RowSelectedTextBrush}"/>

                </Trigger>

                <DataTrigger Binding="{Binding Path=IsDown}" Value="True">

                    <Setter Property="Foreground" Value="Red"/>

                </DataTrigger>

 

                <DataTrigger Binding="{Binding Path=IsReadOnly}" Value="True">

                    <Setter Property="Foreground" Value="Gray"/>

                </DataTrigger>

             *

            bool isSelected = IsSelected;

            bool isDown = false;

            bool isReadOnly = false;

 

            if (this.context != null)

            {

                isDown = this.context.IsDown;

                isReadOnly = this.context.IsReadOnly;

            }

            if (isReadOnly)

            {

                if (cell != null)

                {

                    cell.SetValue(DataGridCell.ForegroundProperty, Brushes.Gray);

                }

            }

            elseif (isDown)

            {               

                if (cell != null)

                {

                    cell.SetValue(DataGridCell.ForegroundProperty, Brushes.Red);

                }

            }

            elseif (isSelected)

            {               

                if (cell != null)

                {

                    cell.SetResourceReference(DataGridCell.ForegroundProperty, "RowSelectedTextBrush");

                }

            }

            else

            {               

                if (cell != null)

                {

                    cell.ClearValue(DataGridCell.ForegroundProperty);

                }

            }

        }

 

        void UpdateFontWeight()

        {

            /*

                <DataTrigger Binding="{Binding Path=Unaccepted}" Value="True">

                    <Setter Property="TextBlock.FontWeight" Value="Bold"/>

                </DataTrigger>

                <DataTrigger Binding="{Binding Path=Unaccepted}" Value="False">

                    <Setter Property="TextBlock.FontWeight" Value="Normal"/>

                </DataTrigger>

             */

 

            bool unaccepted = false;

 

            if (this.context != null)

            {

                unaccepted = this.context.Unaccepted;

            }

            if (unaccepted)

            {

                this.SetValue(TextBlock.FontWeightProperty, FontWeights.Bold);

            }

            else

            {

                ClearValue(TextBlock.FontWeightProperty);

            }

        }

    }

 

So what can we conclude from this little experiement? This was a relatively small change to my app.  I still have mountains of Styles in my Themes, I have 10-15 columns, each with complex DataTemplates, and complex controls in each cell from DatePickers to intellisense ComboBoxes.  So for this small change to make a 33% improvement in scrolling means that WPF DataBinding is a pig. Anything you can do to remove XAML binding will improve performance overall even though that is somewhat contrary to the XAML UI separation design point. 

 

Tags:

Add comment

  Country flag

biuquote
  • Comment
  • Preview
Loading

About the author

Chris Lovett is a Software Engineer at Microsoft working on Windows Phone.

See Resume in SVG

Month List