This post explores how to manage multiple view models across modules in a Prism-based Silverlight application.
One powerful feature of Prism is the ability to dynamically load modules. This allows reduction of the XAP file size, as well as encourages a smaller memory footprint as certain modules are not brought into the application until they are needed. A common issue I find developers struggle with is the consistent use of view models. I've seen some elegant solutions that involve storing values in isolated storage to move the values between modules (elegant, but overkill) when in fact a common view model shared between modules would have been fine.
I'm going to assume you are familiar with Prism and know how to build an application that dynamically loads modules as they are accessed. You may want to refer to my article Dynamic Module Loading with Silverlight Navigation if you need more background information.
Assume you have a project that is dynamically loading modules and injecting them into the shell. For my example, I'm going to assume the application does not require deep linking, so it is not based on the navigation framework (it could be fit easily). In order to facilitate my own navigation, I create a navigation service. The navigation service is a singleton and has the module manager injected to it. It contains an enumeration of controls to display, and a "module map" that maps the control to the module the control lives in. It also exposes a "view changed" event that it fires with the enumeration whenever a new view is navigated to.
The logic is quite simple (in this example, my enumeration is passed in as view, and the value of the enumeration maps nicely to the index of an array of modules that host views). MODULEINIT is just a shell based on the module naming convention, for example "MyApp.Module{0}.ModuleInitializer" or similar.
The module manager is injected in the constructor. Each time we navigate to a view, we call this so the module can be loaded if it hasn't been already. Prism keeps track of loaded modules and won't try to pull the XAP across the wire more than once.
_moduleManager.LoadModule(string.Format(MODULEINIT,_moduleMap[(int)view]));
EventHandler<ViewChangedEventArgs> handler = ViewChanged;
if (handler != null)
{
ViewChangedEventArgs args = new ViewChangedEventArgs(_currentView, view);
handler(this, args);
}
As you can see, very straightforward. We remember the current view and send that with the new view to the event. The views themselves can register to the event. If the views are built with a VisualStateGroup
to handle swapping them into and out of view, then the logic is simply: if I am the old view, go to my hidden state, else if I am the new view, go to my visible state. Then the views can live in an ItemsControl
with only one view showing at a time.
For my views, I create a ViewBase
class that is, in turn, based on UserControl
. This lets me manage some common housekeeping for the views that will be bound to view models. First, I exposed a protected method called _ViewBaseInitialize
to call after the component is initialized. This takes in the view model and binds it, as well as wires into the navigation change event. I know some people won't like injecting the view model and there are certainly other ways to marry the view and its model, but for our example this will do.
Our logic looks simply like:
protected void _ViewBaseInitialize(object viewModel)
{
Loaded += (o, e) =>
{
MasterModel model = DataContext as MasterModel;
model.Navigation.ViewChanged += new EventHandler<ViewChangedEventArgs>(nm_ViewChanged);
if (viewModel != null)
{
model.ModuleViewModel = viewModel;
}
};
if (currentView.Equals(e.OldView))
{
VisualStateManager.GoToState(this, "HideState", true);
}
else if (page.Equals(e.NewView))
{
VisualStateManager.GoToState(this, "ShowState", true);
}
}
You'll note the introduction of the MasterModel
. This view model is bound at the shell level, so it is available to all of the views hosted in that shell. Typically, a shell as a Grid
or similar item as the root layout panel, so in my bootstrapper I handle wiring up the master view model:
protected override DependencyObject CreateShell()
{
Container.RegisterType<NavigationManager>(new ContainerControlledLifetimeManager());
NavigationManager nm = Container.Resolve<NavigationManager>();
Container.RegisterType<MasterModel>(new ContainerControlledLifetimeManager());
Shell shell = Container.Resolve<Shell>();
Application.Current.RootVisual = shell;
nm.NavigateToView(NavigationManager.NavigationPage.Login);
return shell;
}
Because the constructor of the master model takes in a "navigation manager" instance, it receives the instance we just configured. Likewise, the shell will receive the instance of the view model and bind that to the data context of its main grid.
For all practical purposes, the MasterModel
definition looks like this:
public class MasterModel : IViewModel { public NavigationManger Navigation { get; set; } public MasterModel(NavigationManager nm) { Navigation = nm; } public IViewModel SubModel { get; set; } }
You can see that it takes the navigation manager as well as a sub model where needed. This is key: it doesn't have knowledge about the modules it will be interacting with, so it only knows about the IViewModel
. We'll get more specific in a bit. The master module can hold things like settings, static lists, authentication, etc, to pass down to sub modules as needed.
Now let's get down to an actual module that will use this. Let's say I have a module called ModuleUserManager
and it has a view model called UserManagerModel
. It needs a token for security that is stored in the master module.
First, let's extend the ViewBase
to make it easier to grab a model. We can't type the view base because we are basing our controls on UserControl
, which isn't typed. We can, however, type methods. I added this method as a simple helper:
protected T _GetViewModel<T>() where T: IViewModel { MasterModel model = DataContext as MasterModel; return model == null ? null : (typeof(T).Equals(typeof(MasterModel)) ? model as T : model.ModuleViewModel as T); }
Remember, we bound the master to the shell, so the hierarchical nature of data-binding means the data context will continue to be that until I override it. We'll override it in our view, but at this level we still see the master model. The method simply casts the data context and then either returns it the master model is being requested, or access the ModuleViewModel property and returns that (also typed). This was set earlier in the call to the _ViewBaseInitialize
.
Now my view can be based on ViewBase
. Simply add a user control, go into the XAML, reference the namespace for the view base and then switch from UserControl
to ViewBase
. The XAML will look like this:
<vw:ViewBase x:Class="MyApp.ModuleUserManager.Views.UserView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cal="clr-namespace:Microsoft.Practices.Composite.Presentation.Commands;assembly=Microsoft.Practices.Composite.Presentation"
xmlns:vw="clr-namespace:MyApp.Common.Base;assembly=MyApp.Common"
>
<Grid DataContext="{Binding ModuleViewModel}"/>
</vw:ViewBase>
In this example, we've taken the user control and exposed it as a ViewBase because the partial class must have the same base in both XAML and code behind. I've also bound the grid to the "module view model" so it has access to the local view model. In the code behind, we pass the module-specific view model to the initializer:
public partial class UserView : ViewBase
{
public UserView()
{
InitializeComponent();
_ViewBaseInitialize(new UserManagerModel());
}
}
Note that the master model is going to be shared. Even though I dynamically loaded the module and the view with Prism, the data context for the control itself remains bound to the "master" (shell) and therefore can reference all of the properties I've set in previous screens. My new, local view model gets bound as a property to this master view so the local properties can be accessed in the XAML. What if I have a property called Token
in my master that I need to reference in my local view model? I can simply add a hook into the loaded event (so I'm sure all of the binds, etc have taken place) and then use my new helper method. I'll add this after the _ViewBaseInitialize
call:
Loaded += (o, e) => { MasterModel master = _GetViewModel<MasterModel>(); UserManagerModel userModel = _GetViewModel<UserManagerModel>(); userModel.Token = master.Token; };
That's it. Obviously there are more elegant ways to bind the view models together other than the code-behind, but I tend not to be the fanatic purist some others might be when it comes to simple actions and tasks that coordinate views and view models. If that task has "awareness" of an event triggered in the view and the view model, I don't see the issue with tapping into that action to glue things together.
The final thought I'll leave you with is the possibility of the sub modules being a stack. In this scenario, each view would push the view model onto the stack. Then, if you clicked the back button, the previous view could pop its view from the stack and restore the state it was in. This way the master model would help coordinate undo/redo functionality without having knowledge the specific models it collects.
Oh wait, even better...
After posting this, I realized there was even a better way to class the base view. If I do this:
protected void _ViewBaseInitialize<T>(Action<MasterModel,T> onLoaded) where T: IViewModel, new() { // must fire when loaded, as this is when it will be in the region // and ready to inherit the view model Loaded += (o, e) => { MasterModel model = DataContext as MasterModel; model.Navigation.ViewChanged += new EventHandler<ViewChangedEventArgs>(nm_ViewChanged); if (typeof(T) != typeof(MasterModel)) { model.ModuleViewModel = new T(); } if (onLoaded != null) { onLoaded(model, model.ModuleViewModel as T); } }; }
Then I simply can simply change my derived view to this:
public UserView() { InitializeComponent(); _ViewBaseInitialize<UserManagerModel>((masterModel, userModel) => { userModel.Token = masterModel.Token; }); }
Even better - now I just call the base method with the type I want and let it new it up, then get a strongly typed delegate to move my data when needed.