Performance Optimisation for WPF Applications – Part 1

by | 26.01.2019

“Premature optimisation is the root of all evil” Donald Knuth once said. Whenever I deal with the topic of performance optimisation, I remember this well-known sentence. Basically, it is recommended to only optimise the application if there is a performance problem. Performance optimisation can have the character that one has to accept architecture breaks, increased memory consumption or redundancies in the program code as a compromise in favour of the speed advantage. Unfortunately, this is sometimes the price you may have to pay. This is often referred to as a “trade-off” – in fact, optimisations can literally create new problems for you, especially if existing features are complex.

In the following, I would like to focus primarily on optimisation possibilities that occur in a WPF application and explain when they can be useful. Part 1 today deals with the optimisation of ObservableCollections in ItemsControl-based views, binding overheads and the ICommand.CanExecute().

Optimising ObservableCollections

A common problem is controls that bind to ObservableCollections to dynamically respond to newly added or removed objects and update the interface. If you insert a very large number of elements, each element of the view is notified individually and a refresh of the interface including measure/arrange and invalidate is performed. Depending on the use case, this can be very slow.

There are various solution scenarios for this:

  • Replace ObservableCollection with a List<> and rebuild the complete collection
    If you want to rebuild the entire collection for updates or discard and reinsert most of the elements, then it makes sense. The performance advantage also depends on the control, but can be very noticeable.
  • Derive ObservableCollection and implement AddRange()
    From the interface point of view, the NotifyCollectionChanged() event supports the insertion of multiple elements, but there is no AddRange() method that does exactly that. The same applies to methods like RemoveRange().

 

Reduce binding overhead

Data bindings can cause a performance problem if too many PropertyChanges are triggered and thus views have to be updated. Each data binding that updates itself potentially triggers the update of layout calculations and character calls, which results in a considerable time consumption, especially when updating WPF elements.

Trigger OnPropertyChanged() only if the property really changes

Often such code is found in projects:

public string TextContent 
{
  get => return _textContent;
  set 
  {
    _textContent = value;
    OnPropertyChanged("TextContent");
  }
}

Besides the fact that you should use nameof(TextContent) instead of “TextContext” – C# 6.0 assumed – the binding is updated here regardless of whether _textContent has changed or not. So the following would be better:

public string TextContent 
{
  get => return _textContent;
  set 
  {
    if (_textContent != value) 
    {
      _textContent = value;
      OnPropertyChanged(nameof(TextContent));
    }
  }
}

Or even better, you create a template method that you provide in a base class, making it much easier to follow the rule:

// In Basisklasse auslagern.
protected void SetField<T>(string propertyName, ref T backingField, T value) 
{
  if (Equals(backingField, value)) return;
  backingField = value;
  OnPropertyChanged(propertyName);
}

// Konkrete Verwendung:
public string TextContent 
{
  get => return _textContent;
  set => SetField(nameof(TextContent), ref _textContent, value);
}

I have deliberately omitted the parameter attribute [CallerMemberName] to focus on the property changes.

Do not swap bound objects, but update properties

It’s much easier if you get data from the business layer, convert it to a ViewModel using a simple Linq Select expression, and then pack it into the collection. However, WPF usually has to rebuild the entire visual tree at this point; if many visuals are involved, this can take quite a long time.

Instead, if you just update the properties of the objects and add new objects or remove missing objects, you can potentially reduce the cost of updating the interface significantly. I have achieved a speed advantage of 5 times the refresh speed by these optimizations alone – of course, this depends entirely on the use case.

Remove Binding Errors

Binding errors occur when the binding path cannot be resolved to a property on the bound object. This happens, for example, when you rename properties and forget to update the binding path in the corresponding Xaml file. Many binding errors can slow down the application and should be cleaned up. Binding errors are particularly expensive when you work with Visual Studio in the debugger.

To find binding errors, you only need to look at the Visual Studio output, where such problems are displayed very precisely so that you can look for them in the code and fix them.

If the binding error occurs because different ViewModel types are bound at this point, where one ViewModel type has the property and the other does not, the error may be a little harder to fix. The correct solution would be to make sure that for each ViewModel type separate templates with their own bindings are used. If this is not possible because of the structure of the application, you could alternatively work with FallbackValue or with the so-called priority binding.

ICommand.CanExecute() lean and efficient

The method ICommand.CanExecute() is called by WPF to check if a bound command is available on the view. Typical examples are context menu entries or buttons that call ICommand.Execute() when clicked. If a command is not available, the corresponding button or context menu entry is disabled and cannot be pressed by the user. It becomes problematic if a large number of commands are bound and there is code in the method ICommand.CanExecute() that performs intensive operations, e.g. a query to the database, the file system or a server.

CanExecute() is called very often, also when building and updating interfaces, so expensive CanExecute() calls can make the interface very sluggish. You can see this best by using a performance profiler. Here it depends on the application how you optimise the performance. For example, you can

  • cache database or network queries,
  • expensive request first at Execute() and then, if necessary, issue a message to the user,
  • or perform runtime optimisation of the test algorithm in general.

In part 2 of the performance optimisation of WPF applications I take a look at unnecessary visuals and the optimisation of the VisualTree by the custom control with OnRender(). Furthermore, it is about reducing ResourceDictionary lookups and relieving UI threads. Click here for part 2.

 

Notes:

Peter Friedland has published more articles here in the t2informatik Blog, including

t2informatik Blog: CI/CD pipeline on a Raspberry Pi

CI/CD pipeline on a Raspberry Pi

t2informatik Blog: Avalonia UI - Cross-Platform WPF application development with .NET Core

Avalonia UI

t2informatik Blog: Why I ended up with Godot

Why I ended up with Godot

Peter Friedland
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.