WPF Caching visual elements for better performance

By Mirek on (tags: BitmapCache, CacheMode, custom controls, Gantt, WPF, categories: architecture)

More precisely how can we cache UI elements, since the property I’m going to describe is located at UIElement class, to improve the performance of the WPF control.

Recently I had to implement a custom control in WPF. The goal was to display a list of some processes with steps in a Gantt chart. I decided the control will be based on a set of Grid panels where each cell indicates one day on the timeline per process. The result should have look like follows

ganttchart

Each row represents one process and on the right side grid all steps of the process are located in a timeline.

Let’s focus on the right side grid, because this is the crucial part of the Gantt. The requirement was to

  • have a dotted grid lines for each day
  • have some level of transparency on each step bar and vertical gray column
  • have the possibility to scroll the grid horizontally and vertically
  • and some other which are not important now

The main challenge that raised after implementing the control was the performance of the right side grid. Rendering during scrolling was burdened with significant delays. The first idea was to put everything on a ScrollViewer so the whole content is rendered once and then user can navigate through it. Unfortunately the data on the Gantt could be potentially unlimited vertically and very long horizontally. This would cause the first rendering of the Gantt to be very very long.
Ok so I needed to display only those element which should be visible, which means rendering on every scroll step. Soon I realized the performance of such solution is also not very good. Displaying Gantt on a full screen, filled with steps in every row caused the frame rate during scrolling was less than 10 fps, which is bad.

With use of dotTrace from JetBrains, which is incredibly useful tool for profiling, I tried to eliminate all delays caused by the code I’ve written. This showed me that the most time consuming operations was loading the FrameworkElements from templates and adding them to the grid’s Children collection.

   1: FrameworkElement step = Chart.StepTemplate.LoadContent() as FrameworkElement;
   2: Chart.RightGrid.Children.Add(step);

Ok, caching the FrameworkElement object was the first step of improvement, since LoadContent() method seemed to use some XAML parsing inside. Next was to change the visibility of already added elements instead of removing them and again adding to the grid’s children collection.

   1: if (!step.IsInVisibleRange) 
   2:    step.UI.Visibility = Visibility.Hidden;

Those changes gave some improvement but it was not enough. Moreover I could not find more places in the code which took significantly more time to execute than other.

The only way for improvement was now reducing the amount of rendered elements one each scroll action.
First step was to reorganize the grid lines. Initially I was placing a border in every cell of the grid with special border thickness and brush properties. This had of course enormous overhead. Then I change it to drawing lines horizontally an vertically with appropriate row and column span.

   1: Line line = new Line();
   2: line.StrokeDashCap = PenLineCap.Flat;
   3: line.StrokeDashArray = Chart.GridLineDashArray;
   4: line.StrokeThickness = Chart.GridLineThickness;
   5: line.Stroke = Chart.GridLineBrush;
   6: line.X1 = 0;
   7: line.Y1 = rowHeight;
   8: line.X2 = titleColWidth;
   9: line.Y2 = rowHeight;

Second idea was to freeze the grid lines so they are only changed when the Gantt is resized. So I had to calculate the whole count of cells and rows that fit in current constraints of the chart. The consequence was that the scrolling could be performed only by whole number of days or rows, which was not a problem. Unfortunately this did not improve the performance very much, because in fact the grid lines was still there and even they were not changed they had to be rendered on each chart render.
Then I stumbled upon the CacheMode property of an UIElement and a BitmapCache class. Setting the CacheMode to an instance of BitmapCache causes the content of particular UIEelement is cached as a bitmap whenever this is possible. That means when the content is not changed then the rendered view is cached and redisplayed until content changes.
Ok but how to use it, since the content of my chart changes frequently? I had to isolate those elements which changes rarely and try to stick them to a BitmapCache. I’ve created one additional grid under the main grid and put the grid lines on it. Then I set the cache mode of this grid to BitmapCache.

   1: <Grid x:Name="PART_RightGridLines"
   2:       Grid.Column="2"
   3:       Margin="0,0,0,0"
   4:       HorizontalAlignment="Stretch"
   5:       VerticalAlignment="Stretch"
   6:       Background="{TemplateBinding Background}">
   7:     <Grid.CacheMode>
   8:         <BitmapCache />
   9:     </Grid.CacheMode>
  10: </Grid>

Now, since the main grid has transparent background, all lines on the underlying grid are visible as they would be on the main grid. This was the tricky part which gives the possibility to enable bitmap caching for the underlying grid lines and injected a huge performance improvement for the Gantt control.