Part 2
Background
Yesterday I came back from my customers’ place. He had performance issues with his WPF composite application. Each time he navigates away from a page, he pays about 1400ms! – Playing around we’d found that each time a page was changed, the composite framework he uses replaces the old page with the new page by simply updating the ContentControl.Content (placeholder) with the new page. This operation solely took about 800ms!
placeholder.Content = newPage;
In this post I would like to talk about:
- Performance problems you may encounter by using ContentControl incorrectly or not using it by the book.
- How to intercept them.
- How to solve them using ContentControl best practices.
First I want to talk about well-known techniques for displaying views in a composite application, using Prism or other libraries such as CAB/SCSF and MEF.
Working with patterns such as MVP, Presentation Model or MVVM, the view is separated from the logic by having at least two parts: View and Presenter. The view is usually injected/composed into the composite area (Region in Prism, Zone in SCSF and Import in MEF) directly or indirectly, where the place holder (or view host) is usually a ContentControl.
By saying directly, the ContentControl representing the Region/Zone is initialized with the view by setting the Content property with the instance of the view, where instance of the view is usually a UserControl or other WPF element.
By saying indirectly, the ContentControl is initialized with the view by setting the Content property with the instance of the presenter, where instance of the presenter is a simple CLR class (sometimes DependencyObject), and the view is a side-effect of the DataTemplate provided with the presenter (usually merged with the application or shell/window resources).
IMHO the second approach (indirectly) is much better, and there are several reasons for that. The most persuading reason is: performance.
So how performance is related to the ContentControl.Content issue?
This is exactly what I want to talk about. Let me start with a little demonstration.
The shell presenting the view using a ContentControl
<Window x:Class="ContentControlSweets.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Shell" Height="300" Width="300">
<Grid>
<TabControl>
<TabItem Header="View">
<ContentControl x:Name="_placeholder" x:FieldModifier="private"
Margin="16"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center" />
</TabItem>
<TabItem Header="Other View"
Content="Other View Content"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center" />
</TabControl>
</Grid>
</Window>
public partial class Shell : Window
{
public Shell()
{
InitializeComponent();
}
public object Placeholder
{
set { _placeholder.Content = value; }
}
}
The presenter
public class Presenter
{
public string Description { get; set; }
}
Direct view
DirectView.xaml
<UserControl x:Class="ContentControlSweets.DirectView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<TextBlock Text="{Binding Description}"
FontSize="24"
Foreground="Green"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
</UserControl>
DirectView.cs
public partial class DirectView : UserControl
{
public DirectView()
{
InitializeComponent();
}
}
Direct view loading
private void ButtonDirect_Click(object sender, RoutedEventArgs e)
{
new Shell()
{
Placeholder = new DirectView()
{
DataContext = new Presenter()
{
Description = "Direct View"
}
}
}.ShowDialog();
}
Indirect view
IndirectView.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ContentControlSweets">
<DataTemplate DataType="{x:Type local:Presenter}">
<Grid>
<TextBlock Text="{Binding Description}"
FontSize="24"
Foreground="Blue"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
</DataTemplate>
</ResourceDictionary>
Indirect view loading
private void ButtonIndirect_Click(object sender, RoutedEventArgs e)
{
var shell = new Shell()
{
Placeholder = new Presenter()
{
Description = "Indirect View"
}
};
// This can be done from XAML.
shell.Resources.MergedDictionaries.Add(
new ResourceDictionary()
{
Source = new Uri("IndirectView.xaml", UriKind.Relative)
});
shell.ShowDialog();
}
Performance test
Measuring the unload time of both techniques described above (using my machine), by setting the ContentControl.Content property with null, here are the results: direct-view 0ms, indirect-view 0ms.
As you may guess, it’s exactly the same!
Well, lets try to mimic a more realistic application by adding more controls to both views.
After adding 100 buttons and 100 text blocks to both views, and measuring the unload scenario again, here are the new results: direct-view 15ms, indirect-view 0ms.
After adding another 100 buttons and 100 text blocks to both views, and measuring the unload scenario again, here are the new results: direct-view 30ms, indirect-view 0ms.
Wow! It looks like working with direct views, the bigger the logical-tree is, the more time it takes to unload the view. Working with an indirect view takes ZERO time to unload the view.
How’s that?
The best way to know what’s happening there is to take a closer look at the ContentControl using Reflector.
Lets start by looking at the ContentControl.OnContentChanged method. This method is activated each time the ContentControl.Content property changes with a different value (in our case, setting the content to null, or just changing views).
protected virtual void OnContentChanged(object oldContent, object newContent)
{
base.RemoveLogicalChild(oldContent);
if (!this.ContentIsNotLogical)
{
if (base.TemplatedParent != null)
{
DependencyObject current = newContent as DependencyObject;
if ((current != null) && (LogicalTreeHelper.GetParent(current) != null))
{
return;
}
}
base.AddLogicalChild(newContent);
}
}
Commenting the RemoveLogicalChild method call significantly reduces the unloading time!
Lets take a closer look at this method.
protected internal void RemoveLogicalChild(object child)
{
if (child != null)
{
if (this.IsLogicalChildrenIterationInProgress)
{
throw new InvalidOperationException(SR.Get("CannotModifyLogicalChildrenDuringTreeWalk"));
}
FrameworkObject obj2 = new FrameworkObject(child as DependencyObject);
if (obj2.Parent == this)
{
obj2.ChangeLogicalParent(null);
}
IEnumerator logicalChildren = this.LogicalChildren;
if (logicalChildren == null)
{
this.HasLogicalChildren = false;
}
else
{
this.HasLogicalChildren = logicalChildren.MoveNext();
}
}
}
Keep digging inside, you’ll find that ContentControl recursively changes the logical parent of each element all the way down the tree. Having a very simple content such as our Presenter, this won’t happen, since in such case we don’t have a tree.
Conclusion
The best way to work with WPF ContentControl is to set its Content property with a simple CLR object (can be DependencyObject), which is not part of a logical tree, having a DataTemplate for displaying it.
Resources
You can download my test application from here.
Feel free to register my blog RSS feed to see more WPF performance sweets.