Performance anxiety

Given the choice I’d happily avoiding doing almost any UI work at all, but working in a small team means being comfortable working on any part of the stack. I recently had some performance problems in WPF, to which my initial reaction was hands-to-head, head-to-desk, groan; but a problem is a problem and that’s a major part of why I chose a career in programming, so it was time to get investigating.

Background

We have a fairly simple list-detail set up using a Caliburn.Micro conductor, a TreeView for the list and <views:Component cal:Bind.ModelWithoutContext="{Binding}"/> for the detail. We were seeing performance problems when moving between certain items, the UI would hang for perhaps a couple of hundred milliseconds: not long enough to be disastrous, but easily enough to be jarring to the user and to make any transitions far from smooth. My immediate candidate for suspicion was the DataGrid. The problems seemed to only occur when moving to a detail view containing a DataGrid, and my suspicions became more founded when the problem worsened as the detail view contained more than one DataGrid (although strangely it seemed to matter little how much data / how many rows each DataGrid actually contained).

It looked as though my problem was that of re-binding each DataGrid as we moved between items.

Idea

The idea was to keep the view for each item alive and bound – but out-of-sight – rather than either destroying and creating a view instance each time or re-using and re-binding.

Whether this turns out to be the best solution in the longer-term remains to be seen as it’s clear to see that we’ve caused ourselves a memory leak by keeping the view alive for every item ever selected; although for the specifics of it’s current usage this won’t be an issue as the number of items is limited to relatively small number.

Implementation

Create a control with a CurrentItem property binding to the selected item and an ItemTemplate property that will be used to create views for new items, caching and re-using them for items that have already been selected. This doesn’t help the performance the first time that an item is selected, but each subsequent selection is essentially instantaneous and provides a much improved user experience.

It actually turned out relatively easy to implement this – as you can see below – and in our production version we’ve helped eliminate and memory issues by hooking into Caliburn’s Deactivated event (when WasClosed == true) to avoid holding on to unnecessary view instances.

Code

public class ContentHost : ContentControl
{
	#region Fields

	public static readonly DependencyProperty CurrentItemProperty =
		DependencyProperty.Register(
			"CurrentItem", 
			typeof(object), 
			typeof(ContentHost),
			new PropertyMetadata(
				null,
				OnCurrentItemChanged));

	public static readonly DependencyProperty ItemTemplateProperty =
		DependencyProperty.Register(
			"ItemTemplate", 
			typeof(DataTemplate), 
			typeof(ContentHost), 
			new UIPropertyMetadata(null));

	private readonly Grid contentGrid;
	private readonly Dictionary<object, ContentPresenter> items;
	private UIElement currentView;

	#endregion

	#region Constructors

	public ContentHost()
	{
		this.contentGrid = new Grid();
		this.items = new Dictionary<object, ContentPresenter>();
		this.Content = contentGrid;
	}

	#endregion

	#region Properties

	public object CurrentItem
	{
		get { return (object)GetValue(CurrentItemProperty); }
		set { SetValue(CurrentItemProperty, value); }
	}

	public DataTemplate ItemTemplate
	{
		get { return (DataTemplate)GetValue(ItemTemplateProperty); }
		set { SetValue(ItemTemplateProperty, value); }
	}

	#endregion

	#region Methods

	private static void OnCurrentItemChanged(
		object sender,
		DependencyPropertyChangedEventArgs args)
	{
		var contentHost = (ContentHost)sender;

		var newItem = args.NewValue;
		var view = contentHost.EnsureItem(newItem);
		contentHost.SwapCurrentViewFor(view);
	}

	private void SwapCurrentViewFor(ContentPresenter view)
	{
		BringToFront(view);
		SendToBack(currentView);
		currentView = view;
	}

	private ContentPresenter EnsureItem(object currentItem)
	{
		ContentPresenter view;
		if (!items.TryGetValue(currentItem, out view))
		{
			view = new ContentPresenter();
			view.Content = currentItem;
			view.ContentTemplate = ItemTemplate;
			
			contentGrid.Children.Add(view);
			items.Add(currentItem, view);
		}

		return view;
	}

	private void BringToFront(UIElement uiElement)
	{
		uiElement.Visibility = Visibility.Visible;
	}

	private void SendToBack(UIElement uiElement)
	{
		if (uiElement != null)
		{
			uiElement.Visibility = Visibility.Collapsed;
		}
	}

	#endregion
}