Coproject - a RIA Caliburn.Micro demo, part 13

by Augustin Šulc

By now, you are probably using Caliburn.Micro for your applications and hopefully Coproject helped you to learn it. But if you are creating large business applications as we do, you have probably realized that you need to tweak and extend C.M a little bit. That’s why I would like to continue on this Coproject series while focusing on these advanced features.

As you probably know, ordinary ViewModels hierarchy and thus whole application structure is loaded on the application startup. This is OK for small applications but with larger ones, there might be a memory issue having all ViewModels initialized and not being able to close them. In this part, I would like to describe and test an idea about having LazyScreens.

Lazy screen

The core idea is to have LazyScreen – a sort of a wrapper around ordinary Screens that can be seen in application menu but will load the inner screen only after requested. Possibility to close the inner screen would be also useful to free some memory. Yes, you can close ordinary screens too but then, they are removed from their parent screen (and therefore from its menu), which is not what we want here. If these LazyScreens work, they will be used for main application parts – modules, module navigation screens, etc.

ModuleMetadata

So, let’s open the Coproject source code and see what can we do. First of all, since we want our modules (Home, Messages, To Do , Milestones) be lazy loaded only when requested, their titles cannot be set in their code (DisplayName property) but must be set in metadata. So, open IModuleMetadata and add Title property:

(btw: do you know that you can hit CTRL+, to quickly navigate to any class in the solution? You can even use only capital letters, so write IMM to look for IModuleMetadata).

public interface IModuleMetadata
{
	int Order { get; }
	string Title { get; }
}

Next, update ExportModuleAttribute so that it implements the updated interface:

[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class ExportModuleAttribute : ExportAttribute, IModuleMetadata
{
	public int Order { get; private set; }
	public string Title { get; private set; }

	public ExportModuleAttribute(int order, string title)
		: base(typeof(IModule))
	{
		Order = order;
		Title = title;
	}
}

Finally, update all module definitions so that the new attribute constructor is used (Home module example follows):

[ExportModule(10, "Home")]
public class HomeViewModel : Screen, IModule

The solution should now build again.

LazyScreen

Having metadata prepared we have to implement the LazyScreen class. Put it into the Framework folder and make its signature as follows:

public class LazyScreen<TScreen, TMetadata> : Screen

In order to have LazyScreen create a new instance of the inner ViewModel (Screen) when requested, we will use MEF class named ExportFactory. This works just as its name suggests – it gives you an export when you request one. What’s more, it provides metadata without creating a new export. So, let’s add a constructor to the class.

private ExportFactory<TScreen, TMetadata> _factory;

public LazyScreen(ExportFactory<TScreen, TMetadata> factory)
{
	_factory = factory;
}

Next, add the most important functionality:

private TScreen _screen;
private ExportLifetimeContext<TScreen> _export;
private object _lock = new object();

public TMetadata Metadata
{
	get
	{
		return _factory.Metadata;
	}
}

public bool IsScreenCreated
{
	get
	{
		return _export != null;
	}
}

public TScreen Screen
{
	get
	{
		lock (_lock)
		{
			if (!IsScreenCreated)
			{
				_export = _factory.CreateExport();
				_screen = _export.Value;
			}
			return _screen;
		}
	}
}
public void Reset()
{
	if (!IsScreenCreated)
	{
		return;
	}

	lock (_lock)
	{
		_export.Dispose();
		_export = null;
		_screen = default(TScreen);
	}
	NotifyOfPropertyChange(() => IsScreenCreated);
	NotifyOfPropertyChange(() => Screen);
}

Now, let’s check, whether this might work. To use this new class, we must update our Shell. First, open ShellViewModel and update it like this:

[Export(typeof(IShell))]
public class ShellViewModel : Conductor<LazyScreen<IModule, IModuleMetadata>>.Collection.OneActive, IShell
{
	[ImportingConstructor]
	public ShellViewModel([ImportMany]IEnumerable<ExportFactory<IModule, IModuleMetadata>> moduleHandles)
	{
		var modules = from h in moduleHandles orderby h.Metadata.Order 
						select new LazyScreen<IModule, IModuleMetadata>(h);
		Items.AddRange(modules);
	}
}

You should notice that we only updated constructor parameter type (from Lazy to ExportFactory), added new LazyScreens from these exports into Items property, and updated base class generic parameter accordingly.

Finally, we must update ShellView too:

  • Change binding of the TextBlock in the Items ListBox from DisplayName to Metadata.Title
  • Change name of ActiveItem_Description to ActiveItem_Screen_Description
  • Change name of ActiveItem to ActiveItem_Screen

Meaning of these changes should be obvious – since there is another layer between shell and modules (our new lazy screen), we must point the controls to the inner view models.

If you now run the application, it should work just as it did before all these changes. Ok, I know that this does not seem like a great achievement :-) but take it as a proof that the original idea might work.

Simplify generics

There is one line in the code I really don’t like – the repeating of generic parameters in ‘select new LazyScreen…’ in ShellViewModel constructor. We could make a use of generic functions to get rid of it. Add the following class into LazyScreen.cs:

public static class LazyScreen
{
	public static LazyScreen<TScreen, TMetadata> Create<TScreen, TMetadata>(
		ExportFactory<TScreen, TMetadata> factory)
	{
		return new LazyScreen<TScreen, TMetadata>(factory);
	}
}

We can update the ShellViewModel constructor:

var modules = from h in moduleHandles orderby h.Metadata.Order select LazyScreen.Create(h);

No big deal but I like it much better now.

Finish LazyScreen

Having proven that LazyScreen works as expected, we should finalize its implementation (mainly delegate actions called on LazyScreen to its inner screen). Here are functions for screen activation and deactivation:

protected override void OnActivate()
{
	ActivateScreen();
}

protected override void OnDeactivate(bool close)
{
	DeactivateScreen(close);
}

private void ActivateScreen()
{
	var activatableScreen = _screen as IActivate;
	if (activatableScreen != null)
	{
		activatableScreen.Activate();
	}
}

private void DeactivateScreen(bool close)
{
	var deactivatableScreen = _screen as IDeactivate;
	if (deactivatableScreen != null)
	{
		deactivatableScreen.Deactivate(close);
	}
}

There are functions for closing too:

public override void CanClose(Action<bool> callback)
{
	var closableScreen = _screen as IGuardClose;
	if (closableScreen != null)
	{
		closableScreen.CanClose(callback);
	}
	else
	{
		base.CanClose(callback);
	}
}

public new void TryClose()
{
	var closableScreen = _screen as IClose;
	if (closableScreen != null)
	{
		closableScreen.TryClose();
	}
	base.TryClose();
}

And for Parent/Child relationship:

public override object Parent
{
	get
	{
		return base.Parent;
	}
	set
	{
		base.Parent = value;

		if (IsScreenCreated)
		{
			SetScreenParent();
		}
	}
}

private void SetScreenParent()
{
	var childScreen = _screen as IChild;
	if (childScreen != null)
	{
		childScreen.Parent = this.Parent;
	}
}

Then update getter for Screen property:

if (!IsScreenCreated)
{
	_export = _factory.CreateExport();
	_screen = _export.Value;

	SetScreenParent();
	if (IsActive)
	{
		ActivateScreen();
	}
}

And add this line just to the beginning of the lock statement in Reset():

DeactivateScreen(true);

So, our LazyScreen is ready. It loads its content only when needed and the rest of the application works properly. In the next part of this series, we will implement LazyConductor so that we can close (reset) LazyScreens.

What do you think about this solution? Have you already solved similar problems?

Tags: Caliburn, Ria, Silverlight, Coproject

1 Comment

  • Pontus Munck said

    Nice solution! I've done something similar that also loaded the screens from external xap-files on demand, but your solution is much more elegant. Thanks for sharing! :)

Add a Comment