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?
5 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
Suresh said
Nice article, good work
uday said
good work