Fast switching between ViewModels in Caliburn.Micro

We use Caliburn.Micro on several of our projects and we are quite satisfied with it. The framework is pretty much based on what is described in Coproject series.
As one of our applications grew, we realised that changing between screens takes longer and longer time (blocking whole UI thread). The more screens had been opened the longer it took. Having profiled the application, I found out that the delay is caused by calling MeasureOverride of DataGrid and its columns. Since changing the ActiveItem property of a Conductor causes ContentControl bound to it completely change its content (to render view of the new active ViewModel), I understand it might take some time. But as all views are cached in memory, why it takes so much time?

You can see demo here.

I have asked several questions (StackOverflow, C.M forum, Silverlight forum). Found no solution but at least a hint that the problem is in adding the view to visual tree.

My idea was to create a custom ContentControl that would cache all views so that it can quickly switch between them. After few minutes, I came up with this:

public class ContentHost : ContentControl
{
	private Grid _contentGrid;
	private UIElement _currentView;

	public ContentHost()
	{
		_contentGrid = new Grid();
		this.Content = _contentGrid;
	}

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

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

	private void OnCurrentItemChanged()
	{
		var newView = EnsureItem(CurrentItem);
		SendToBack(_currentView);
		_currentView = newView;
	}

	private UIElement EnsureItem(object source)
	{
		if (source == null)
		{
			return null;
		}

		var view = GetView(source);

		if (!_contentGrid.Children.Contains(view))
		{
			SubscribeDeactivation(source);
			_contentGrid.Children.Add(view);
		}

		BringToFront(view);
		return view;
	}

	// logic from Caliburn.Micro
	private UIElement GetView(object viewModel)
	{
		var context = View.GetContext(this);
		var view = ViewLocator.LocateForModel(viewModel, this, context);

		ViewModelBinder.Bind(viewModel, view, context);
		return view;
	}

	private void SubscribeDeactivation(object source)
	{
		var sourceScreen = source as IScreen;
		if (sourceScreen != null)
		{
			sourceScreen.Deactivated += SourceScreen_Deactivated;
		}
	}

	private void SourceScreen_Deactivated(object sender, DeactivationEventArgs e)
	{
		if (e.WasClosed)
		{
			var sourceScreen = sender as IScreen;
			sourceScreen.Deactivated -= SourceScreen_Deactivated;

			var view = GetView(sourceScreen);
			_contentGrid.Children.Remove(view);
		}
	}

	private void BringToFront(UIElement control)
	{
		control.Visibility = System.Windows.Visibility.Visible;
	}

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

If you now compare performance of ContentControl with ContentHost, you will see that it works.

I still don’t know why more DataGrids causes other DataGrids to render slower.

What do you think?

Source code.

Tags: Caliburn, Ria, Silverlight, Coproject

3 Comments

  • Bruno Samardzic said

    Hey, great work, but the contentControl example is not working for some reason

  • Augustin Šulc said

    Both samples work on my machine. What error and where do you get?

  • tibel said

    Updated version that also works with ViewAware.CacheViewsByDefault=false;
    is now part of Caliburn.Micro.Extras

Add a Comment