I’ve been on hiatus for a while working on a windows forms application using Composite Application Block (CAB) and the Model-View-Presenter pattern. This has inspired me in my Silverlight efforts as the modularity of the CAB architecture is enticing.
My first attempt involved trying to use Silverlight 3’s navigation application to navigate to pages defined in modules loaded at runtime but the navigation namespace does not lend itself to this. Instead, I decided to use PRISM’s (the new CAB for WPF and Silverlight) modularity namespace with a custom navigation implementation.
Prism
Prism is the code name for Patterns and Practice: Composite WPF and Silverlight application guidance (CompositeWPF). That’s quite a mouthful so Prism it is.
Similar to CAB, it provides core libraries for several design patterns like modularity, commanding, event aggregation and dependency injection. Unlike CAB, it is less intrusive and allows you to mix and match pieces as you wish to use them.
In a modular application, the main window (shell) contains regions that are placeholders for views. The bulk of the application is contained in modules (assemblies), which group code according to common functionality. The trick is that the modules are not referenced by the shell and do not reference each other, everything is abstracted. This allows modules to be developed and delivered independently of each other.
The infrastructure assembly contains utility classes and interfaces common to all assemblies in the solution. I like to use solution folders to organize my projects as follows:
Referencing the source code from the Prism libraries facilitates debugging and understanding. Again, the shell project does not contain references to the module projects, nor do the modules reference each other. All communication between these libraries is abstracted through Commands or the Event Aggregator. In the case of synchronous communication, a good approach is the register a service with Unity and exposing the interface in a common library.
Model-View-ViewModel
Model-View-ViewModel is a design pattern that allows us to separate the view from the business logic. The goal here is to minimize the amount of untestable code in the view. I go a bit further and put absolutely no code in my views.
I have added a Navigation namespace to my MVVM library to handle all things navigable. The navigator class allows modules to register pages and provides navigation functions for the shell. The interface is as follows:
public interface INavigator
{
void Clear();
void Goto(string pageName);
INavigator RegisterPage(string pageName, Type viewModelType, ICommand targetComand);
INavigator RegisterPage<TViewModel>(string pageName, ICommand targetComand) where TViewModel : ViewModel;
PageItemList PageItems { get; }
}
Modules can register their pages by calling the appropriate method:
public INavigator RegisterPage(string pageName, Type viewModelType)
{
PageItem pageItem = new PageItem(pageName, viewModelType);
_PageItems.Add(pageItem);
Container.RegisterType(viewModelType);
if (pageName == HtmlPage.Window.CurrentBookmark)
{
Goto(pageName);
}
return this;
}
This method stores the metadata for the page in a managed collection (internally a dictionary), and registers the ViewModel type with Unity. A page can be navigated by calling Goto:
public void Goto(string pageName)
{
PageItem navigatedPage = _PageItems[pageName];
if (navigatedPage == null)
{
throw new KeyNotFoundException("Page " + pageName + " not found in page list.");
}
Clear();
ViewModel viewModel = Container.Resolve(navigatedPage.ViewModelType) as ViewModel;
viewModel.Show(_mainRegionName);
_activeViewModel = viewModel;
HtmlPage.Window.NavigateToBookmark(pageName);
}
This finds the appropriate ViewModel, resolves an instance with Unity and adds it to the main region of the page. This also sets the “hash” value on the url to the name of the page to provide deep linking functionality.
Commands
In order for the shell to display a page, it has to tell the module controller (a class that initializes and manages the module) to display the page. The shell has no reference to the module, so it cannot instantiate a ViewModel or a view directly. All it knows is the page name. To provide a communication bridge, I chose commands (the event aggregator was the other choice). I wrapped the command into the Page metadata to simplify the logic and reduce repetitive code.
The page metadata is defined in a PageItem class and apart from storing the name of the page and the ViewModelType it also implements all necessary commanding functionality:
public PageItem(string name, Type viewModelType)
{
Active = true;
Name = name;
……
Command = new DelegateCommand<string>(Navigate, p => { return Active; });
}
In order to control the availability of a page, we can tap into the CanExecute event of the command:
public bool Active
{
get
{
return _Active;
}
set
{
_Active = value;
if (_Command != null)
{
_Command.RaiseCanExecuteChanged();
}
}
}
Modules
The module is loaded at runtime by the shell using a module catalog. The bootstrapper loads up the ModuleCatalog from a xaml file called ModulesCatalog.xaml:
protected override IModuleCatalog GetModuleCatalog()
{
return ModuleCatalog.CreateFromXaml(new Uri(“ModulesCatalog.xaml”, UriKind.Relative));
}
The catalog file lists the modules to load:
<Modularity:ModuleCatalog xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:sys="clr-namespace:System;assembly=mscorlib"xmlns:Modularity="clr-namespace:Microsoft.Practices.Composite.Modularity;assembly=Microsoft.Practices.Composite">
<Modularity:ModuleInfoGroup Ref="Navantis.Sports.Roster.xap"InitializationMode="WhenAvailable">
<Modularity:ModuleInfo ModuleName="Navantis.Sports.Roster"
ModuleType="Navantis.Sports.Roster.RosterModule, Navantis.Sports.Roster, Version=1.0.0.0" />
</Modularity:ModuleInfoGroup>
<Modularity:ModuleInfoGroup Ref="Navantis.Sports.Schedule.xap"InitializationMode="WhenAvailable">
<Modularity:ModuleInfo ModuleName="Navantis.Sports.Schedule"
ModuleType="Navantis.Sports.Schedule.ScheduleModule, Navantis.Sports.Schedule, Version=1.0.0.0" />
</Modularity:ModuleInfoGroup>
</Modularity:ModuleCatalog>
Each module project contains a class that derives from ModuleInit which acts as a controller for the module:
public class RosterModule : IModule
{
……
public void Initialize()
{
Navigator.RegisterPage<HelloViewModel>("Hello", SayHelloCommand);
Navigator.RegisterPage<GoodByeViewModel>("GoodBye", SayGoodByeCommand);
}
#endregion
}
In the initialize method, the pages are registered on the Navigator. All that’s left is to set up a menu to allow the user to navigate to these pages. I have created a simple UserControl called menu which fills a stackpanel with a button for each page registered on the Navigator:
private void Build(IList pageList)
{
foreach (PageItem page in pageList)
{
Button button = new Button() { Content = new TextBlock() { Text = page.Name } };
Click.SetCommand(button, page.Command);
Click.SetCommandParameter(button, page.Name);
LayoutRoot.Children.Add(button);
}
}
The command parameter is the name of the page to navigate to.
Deep Linking
In order to enable deep linking, we need to do two things:
- Update the url whenever we select a new page
- Read the url when the app loads to select a specific page.
The first part is handled in the Goto method of the navigator:
HtmlPage.Window.NavigateToBookmark(pageName);
This will update the url by adding a hash (#page) to the end.
The second part is accomplished by checking the hash at startup. Unfortunately the modules are loaded as they become available (downloaded from the server) so we need to take a passive approach. As each page is registered, its name is compared to the hash, and when we have a match, we display it:
if (pageName == HtmlPage.Window.CurrentBookmark)
{
Goto(pageName);
}
What’s next
Currently the only menu possible is a single level one, no hiearachy. This could be accomplished by adding a group value to each page item indicating which node the page belongs to and letting the shell deal with constructing the hierarchy.
Also there is currently no way to indicate which page is selected on the menu. A binding would need to be established to allow this.
I plan on posting the source code to the MVVM library once I have cleaned it up and finished unit testing it.