Performance Optimisation for WPF Applications – Part 2
After looking at ObservableCollections in item control-based views, binding overheads and the ICommand.CanExecute() in part 1 of the performance optimisation of WPF applications, we now take a look at visuals, the reduction of ResourceDictionary lookups and the relief of UI threads.
Optimising the number of visuals
The more Visuals WPF has to manage in an application, the slower the interface becomes. This includes layout updates with Measure()/Arrange of the UI thread and drawing through the render thread. So it’s worth taking a look at the number of visuals with tools like the Visual Studio Live Visual Tree or third party tools like Snoop:
Remove Unnecessary Visuals
Sometimes a refactoring leaves unnecessary layouters and controls that, like dead code, have no function at all. There are some typical cases that can occur if you do not remove the unnecessary visuals:
- Using TextBlock instead of labels: TextBlock is only a single visual, labels are more complex.
- Use TextBlock.set text directly instead of a TextBlock with a run.
- Visibility=”Collapsed” removes the element from the VisualTree, Hidden does not and will therefore still be calculated in Measure calls.
For pure representations optimise the VisualTree by custom control with OnRender()
If you have a large number of visuals, you can significantly improve performance by replacing the Xaml template with a single control that receives a ViewModel as DataContext and draws everything manually in OnRender(). A good example is very complex diagrams or coordinate-based forms for pure viewing.
In such cases, I have been able to optimise the user interface for these elements from a perceived one or two seconds to 40ms.
Of course the effort is high here because you have to do the layout yourself based on coordinates. But for a very critical part of the application the user experience can be improved sustainably.
Reduce ResourceDictionary Lookups
If you have a user control or a window that loads many resource dictionaries, loading and unloading the control can take a relatively long time. In one case, I had a user control that loaded five ResourceDictionaries with a handful of DataTemplates each. This caused the control to build up in about three to four seconds and took about the same amount of time to fade out again. After I had stored all ResourceDictionaries directly in the control as resources, the control opened and closed again almost without delay.
Unload UI Thread
In the true sense of the word, the unloading of UI threads is not performance optimisation, but it does noticeably increase the reactivity of the system. There are often operations that are triggered directly by the reaction of a WPF event, ICommand.Execute() or a property change from the view into the ViewModel. If these operations potentially take longer than a few 100 ms, the application appears sluggish.
Executing Potentially Longer Operations in the Background
A good, simple example is updating a list of records from a server. When the user presses a button, the list is updated. Usually this takes about 500ms, with a bad network connection or increased load on the servers and depending on the amount of data sometimes even several seconds – not to mention a re-trial if the server is temporarily unavailable.
To do this, you can load the request to the server asynchronously in the command handler, for example, using a task, and then return the result in the UI thread using the dispatcher, so that the interface is updated. While the data is being loaded asynchronously, you should display information that the data is currently being loaded and lock the interface accordingly so that no further asynchronous requests or inconsistent states can be generated.
Another advantage besides increased reactivity is that other tasks dispersed in the UI thread have a better chance of being processed, which can also accelerate the processing of other tasks in the UI thread – e.g. dispatcher-begin-invoke() calls of low priority.
Debouncing based on DispatcherTimer
A debouncer is an instrument with which an event is only triggered if it is not triggered again at least within a certain period of time. A fantastic example is the search on Google: As soon as I start typing text, search results are retrieved asynchronously. However, if I type too fast, the search will not start again until I stop typing. This way, new search queries are not constantly sent out and the search results are entered, which prevents unnecessary and expensive operations.
For example, suppose you have a selection in a list. If you have selected an element from the list, you want to display detailed information that unfortunately has to be calculated in the UI thread and takes some time. If you now move quickly with the keyboard from one element to the next, the application gets a bit choppy because you can only jump to the next element when the calculation and display for the details is finished. Here you could use a debouncer to ensure that the details are only loaded if the selection has not been changed for a moment, e.g. after 500ms.
Example¹:
public class MyViewModel : ViewModel
{
private MyDetailViewModel _details;
private readonly IDetailService _detailService;
public MyDetailViewModel Details
{
get => _details;
set => SetValue(nameof(Details), ref _details, value);
}
public MyViewModel(IDetailService detailService)
{
_detailService = detailService;
}
public void OnSelectedElementChanged(MyElement selectedElement)
{
LoadDetails(selectedElement);
}
private void LoadDetails(MyElement selectedElement)
{
var detailData = _detailService.Load(element.Id);
Details = new MyDetailViewModel(detailData);
}
}
The following class represents a ViewModel, which prepares data and creates a ViewModel when OnSelectedElementChanged() is called via a service class. The service class potentially takes a long time and must be executed in the UI thread due to its implementation and therefore cannot be loaded asynchronously.
The debouncer uses the dispatcher timer to call an action after a time interval using a configurable priority:
public class Debouncer
{
private readonly DispatcherPriority _dispatcherPriority;
private readonly Dispatcher _dispatcher;
private DispatcherTimer _timer;
public Debouncer(DispatcherPriority priority = DispatcherPriority.ApplicationIdle,
Dispatcher disp = null)
{
_dispatcher = disp ?? Dispatcher.CurrentDispatcher;
}
public void Debounce(TimeSpan interval, Action<object> action, object parameter = null)
{
_timer?.Stop();
_timer = null;
_timer = new DispatcherTimer(
interval,
_dispatcherPriority,
(s, e) => ExecuteDebouncedAction(action, parameter),
_dispatcher);
_timer.Start();
}
private void ExecuteDebouncedAction(Action<object> action, object parameter)
{
if (_timer == null) return;
_timer?.Stop();
_timer = null;
action.Invoke(parameter);
}
}
The action will not be executed until the timer has expired and in the meantime the Debounce() method has not been called again. If the debounce method is called before the timer expires, the timer will be restarted.
With this class, customisation to our ViewModel is very easy:
public class MyViewModel : ViewModel
{
private MyDetailViewModel _details;
private readonly IDetailService _detailService;
private readonly Debouncer _debouncer;
public MyDetailViewModel Details
{
get => _details;
set => SetValue(nameof(Details), ref _details, value);
}
public MyViewModel(IDetailService detailService)
{
_detailService = detailService;
_debouncer = new Debouncer();
}
public void OnSelectedElementChanged(MyElement selectedElement)
{
_debouncer.Debounce(TimeSpan.FromMilliseconds(500), o => LoadDetails((MyElement)o), selectedElement);
}
private void LoadDetails(MyElement selectedElement)
{
var detailData = _detailService.Load(element.Id);
Details = new MyDetailViewModel(detailData);
}
}
Do you have any questions or comments regarding the optimisation of WPF applications? I would be very pleased about feedback.
Notes:
[1] The example is based on the publication at https://weblog.west-wind.com/posts/2017/Jul/02/Debouncing-and-Throttling-Dispatcher-Events and has been extended by some lines of code to create the desired effect.
Peter Friedland has published more articles here in the t2informatik Blog, including
Peter Friedland
Software Consultant at t2informatik GmbH
Peter Friedland works at t2informatik GmbH as a software consultant. In various customer projects he develops innovative solutions in close cooperation with partners and local contact persons. And from time to time he also blogs.