In order to solve this problem , it is recommended that the development team has
its targets for WPF application in order to keep in check its performance and
do the performance tuning iteratively throughout the development phase.
With the application growing , code refactoring and and performance tuning becomes
the essential part of the development life cycle.
First and the foremost important point is Data Virtualization. In large enterprise
applications , the data application is dealing with is very large and for better
UI, we use heavy controls like datagrids which in itself includes convertors
, images etc which we use for better graphical representation and i.e not wrong
as major reason for opting WPF is usually due to its rich ability to provide
rich graphics. But to optimise performance we should take care of small things
, use hardcoded width and height wherever possible as it will boost performance
in comparison to dynamic width and height where it calculates again and again.
Why is data virtualization important? When there are hundreds and thousands of records
in data source which needs to bind with data grids , listviews or stackpanels
etc , the control will process the complete datasource with it even when it is
displaying 10 records at a time and will change on scrolling or any other option
your application might have given to the user.
So if the developers enable virtualization then the control will process only the
viewable portion of data and hence will significantly improve the performance
of the application. Only controls that use the ItemsControl class to manage data
items can take advantage of the VirtualizingStackPanel. Try to minimise the use
of heavy controls inside the items template of grids data layout controls.
For example :- StackPanels , ListBox and Listview supports virtualization by default. As
it implements Visualizing Panel class in WPF.Virtualizing Panel @MSDN
If you want to compare , try out the below snippets of code:
MainWindow.xaml
<Window x:Class="WpfApplication1.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>
<ListBox Name="ListBoxWithVirtualization" Height="100"
Margin="5"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling" />
<ListBox Name="ListBoxWithoutVirtualization" Height="100"
Margin="5"
VirtualizingStackPanel.IsVirtualizing="False" />
</Grid>
</Window>
MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
BindListBoxWithoutVirualization();
}
public void BindListBoxWithVirualization()
{
ListBoxWithoutVirtualization.Visibility = Visibility.Hidden ;
for (int i = 0; i< 1000000; i++)
ListBoxWithVirtualization.Items.Add("Item - " + i.ToString());
}
public void BindListBoxWithoutVirualization()
{
ListBoxWithVirtualization.Visibility = Visibility.Hidden ;
for (int i = 0; i < 1000000; i++)
ListBoxWithoutVirtualization.Items.Add("Item - " + i.ToString());
}
}
}
To check what happens when virtualization is enabled , in the constructor call ,
BindListBoxWithVirualization and run the project , check the CPU usage and then
call BindListBoxWithoutVirualization and run the project and see the difference
yourself.
The next point is keep the visual tree simple as much as possible. Always give emphasis
while designing your xaml , make sure and take maximum care that you keep the
visual tree simple. Bind the data source only to the required controls not all
in the hierarchy. This scenario occurs usually when we do databinding with controls
having item templates.
Tip - Try not to make visual tree heavy in order to reduce your lines of code behind.
Heavy Visual Tree will be more costly.
For example :-
<Border>
<Grid>
<Grid.RowDefinitions >
<Row Definition />
<Row Definition />
< /Grid.RowDefinitions >
<StackPanel Grid.Row=0>
<TextBlock />
</StackPanel>
<StackPanel Grid.Row=1>
<TextBlock />
</StackPanel>
</Grid>
</Border>
In the example above StackPanels are extra , so if we remove them, we have better
optimised code.
The next thing I am going to discuss is about Dispatcher.BeginInvoke. Every WPF Developer
reading this article is familiar about this and when as a developer we start
with WPF , it comes as a best practise to use Dispatcher.BeginInvoke to avoid
your application to hang and better user experience.But use of multiple Dispatcher.BeginInvokes
increases the CPU usage so better batch up your Dispatcher.BeginInvokes in a
single call.
To understand this better , we need to understand that each time a Dispatcher.BeginInvoke
is called , it gives mainUI thread an asynchronous call to handle the drawing
in it. Definitely best practise would be batching up your points in a queue
and then drawing them to the screen at once, rather than trying to have multiple
draws running asynchronously on the same thread.
For example, consider the following scenario where in a window you have 3 datagrids
(dg1,dg2,dg3) displaying 3 sets of large data from server1, server2, server3
respectively.. Now let us compare :
Code Snippet1 :-
private void Binddata()
{
App.Current.Dispatcher.BeginInvoke(() =>
{
dg1.ItemsSource = null ;
dg1.ItemSource = //GetDataFromServer1;
});
App.Current.Dispatcher.BeginInvoke(() =>
{
dg2.ItemsSource = null ;
dg2.ItemSource = //GetDataFromServer1;
});
App.Current.Dispatcher.BeginInvoke(() =>
{
dg3.ItemsSource = null ;
dg3.ItemSource = //GetDataFromServer1;
});
}
Now better approach would be
private void Binddata()
{
List<T> data1 = //GetDataFromServer1;
List<T> data2 = //GetDataFromServer2;
List<T> data3 = //GetDataFromServer3;
App.Current.Dispatcher.BeginInvoke(() =>
{
dg1.ItemsSource = null ;
dg1.ItemSource = data1;
App.Current.Dispatcher.BeginInvoke(() =>
{
dg2.ItemsSource = null ;
dg2.ItemSource = data2;
});
App.Current.Dispatcher.BeginInvoke(() =>
{
dg3.ItemsSource = null ;
dg3.ItemSource = data3;
});
}
Code Snippet 3:
best would be
private void Binddata()
{
List<T> data1 = //GetDataFromServer1;
List<T> data2 = //GetDataFromServer2;
List<T> data3 = //GetDataFromServer3;
App.Current.Dispatcher.BeginInvoke(() =>
{
dg1.ItemsSource = null ;
dg1.ItemSource = data1;
dg2.ItemsSource = null ;
dg2.ItemSource = data2;
dg3.ItemSource=null;
dg3.ItemSource = data3;
});
}
Last but not the least beware of memory leaks in your application. In order to find
the points of memory leaks in your application using any of WPF profiling tool and follow the instructions and possible causes and refactor your code accordingly.