Introduction
It has been a while since I wrote anything about frameworks, and the MVVM pattern in particular. I have found myself yearning to get back into do a sizable project; as such, I am just about to embark on updating my Cinch MVVM framework to bring it in line with the forthcoming .NET 4.0 / VS2010 release. However, just before I started work on that, I wanted to show just how easy it is to extend Cinch, to include certain areas of PRISM/CAL, in particular PRISM/CALs region support, as this is one area that PRISM/CAL has that Cinch does not.
PRISM Smorgasbord (Never Thought I Would Find a Need for That Word)
Now, I do not know how many of you have used PRISM/CAL, or have worked with similar code such as the Smart Client Software Factory, which although quite different to PRISM/CAL, still has some similarities.
Anyway, that is by the by; let's concentrate on what PRISM/CAL can do for us.
PRISM/CAL is billed as being "Composite WPF and Silverlight". So as such, it offers developers the ability to create composite UIs which get constructed into one UI application. So how does PRISM/CAL do this? Well, it does have a fairly good set of arsenal that aids in this, such as:
Shell
There is typically a single Window which acts as the application shell. This is actually a pretty common approach to doing WPF apps, and has been for quite some time. Some of the best WPF apps are all single Window applications; just look at Expression Blend:
That is a single Window application. Anyway, within PRISM/CAL, the shell is only really responsible for creating regions.
BootStrapper
The bootstrapper is responsible for loading up all the disparate (and seemingly unconnected) modules such that they can be used within a single shell Window. This typically also creates the shell window and runs it.
Modules
PRISM/CAL offers things called modules to aid in the well, er, modularity of building a composite system. Each module can have various classes, such as perhaps a View (typically UserControl), and maybe a ViewModel. These modules are understood as units of code that the PRISM/CAL core code knows what to do with in order to get them to be compiled such that they can be used within the main application shell.
Event Aggregator
Is a disconnected messaging system that uses WeakReference
objects such that the sender/receiver are free to be GCd. The Event Aggregator also offers the ability to have sent messages to be invoked on the UI thread. You can think of this as a communication channel where the sender and receiver do not know about each other but rather go through some middle man. This is conceptually very similar to using the Mediator pattern, which is what my own Cinch MVVM framework uses.
Dependency Injection
PRISM/CAL makes very heavy use of DI/IOC, and it does this by using the Unity application block, which is Microsoft's own IOC container. It works quite like most other IOC containers more or less; OK, there are some that have less syntax to get to grips with, but conceptually, they all work roughly the same, they are all able to store Types and resolve Type dependencies by injecting the required Types in at constructor/property level, and they can all manage object instance lifecycles (singleton, new instance, etc.).
Basically, all we really care about is that PRISM/CAL is using DI/IOC to resolve various Types.
Region Support
The last piece in the PRISM/CAL puzzle is region support. Some of you may know what this is while others may not; well, put simply, regions are a control or area of the screen which will accept content to be added to them. PRISM/CAL comes with a very handy way of working with regions; all that you are required to do is use an attached property on one of the supported region controls, which are the following controls as far as I know:
ContentControl
Grid
StackPanel
TabControl
There may be more that I have missed off, apologies.
Here is an example of how to setup a region on a control that is part of the shell's XAML:
<TabControl cal:RegionManager.RegionName="MainRegion"/>
Now you are able to dynamically add new content into the TabControl
using a very easy to use syntax; you can do something like this:
View1 view = new View1();
regionManager.RegisterViewWithRegion("Someregion", () => view);
regionManager.Regions["Someregion"].Activate(view);
RegionAdaptors
Going slightly off subject here, but just for your information, if your UI requirements warrant slightly more complex controls that you want to use as regions, then do not worry, you can also do that by the use of a custom RegionAdaptor
.
You can find examples of this at various places, but all you really have to do is inherit from RegionAdapterBase<T>
and override the Adapt()
and CreateRegion()
methods, and override the ConfigureRegionAdapterMappings()
method in the bootStrapper
class.
John Papa has a good example of how to create a custom RegionAdaptor
, which you can find at this URL: Fill My region Please, or for a slightly more adventurous RegionAdaptor
, have a look at this one: WindowRegionAdapter for CompositeWPF.
Take What You Want
Now, some of you may be thinking, yeah so what Sacha, you have not told me much, or even shown any code yet. Well, this is a very small article, but I did not want to get into any code until I could quickly go into PRISM/CAL a bit so at least those that have not used it would get a very, very quick run down on how it works.
I have to say, I am not a massive fan of PRISM/CAL, but I really, really like the region support. I do not know how many of you have read my Cinch MVVM framework articles; heck, some of you may even be using my Cinch MVVM framework I guess. Well, over the past months (since I let the framework out there, I have been contacted by various people talking about PRISM/CAL's excellent region support, and I tell you what, I agree, I love PRISM/CAL's region stuff, it is just that I am not that keen on the rest; don't get me wrong, the P&P folks are smart hombres, it is just a bit too constraining for me at times; you have to do things a certain way. Or do you?).
Well, actually, no, you don't. You see, the other great thing about PRISM/CAL is that you can just use the bits you want to use and leave the rest. I like regions and do not care much for the rest, so the rest of this article will be dedicated to showing you how to use PRISM/CAL with another MVVM framework. Naturally, I will be using my own Cinch MVVM framework, but where it's appropriate, I will state an alternative way of doing something if you do not wish to use my Cinch MVVM framework.
OK, so enough banging on about it; let's continue to see how I have made my Cinch MVVM framework be able to work with RISM/CAL's excellent region support.
So How Can We Use Regions in Another MVVM Framework
It is actually not that hard to get region support to work outside PRISM/CAL. At least, it was not that hard for me to get it to work with my Cinch MVVM framework. All I had to do was follows these easy steps:
Step 1: Make sure I had all the required references in place
I simply had to make sure that the attached demo project had the correct assembly references, which for me equated to this:
As I say, the demo app is written for use with my Cinch MVVM framework, so if you are not using that, you will not have to add any references for the assemblies in the Cinch folder.
Step 2 : Project Structure
As I stated above, PRISM/CAL uses a bootStrapper
and also normally works with modules. The bootStrapper
is not optional, as it sets up the Unity IOC container. However, modules are actually optional; we do not have to use modules at all, but we do have to override the GetModuleCatalog()
method to state that there will, in fact, be no modules to load into the shell.
One thing I should mention is that the demo app included with this article obviously assumes that all files are part of the same project. The project structure for the attached demo project looks like this:
If your project requires things a little different from this, such as ViewModels in a separate assembly, just make sure that all your own projects have the correct references, as outlined in the previous subsection.
Step 3: The BootStrapper
As I stated earlier on, PRISM/CAL uses a bootstrapper file to ensure various things such as the shell are setup. We too must do that if we wish to use any of the PRISM/CAL bits and pieces in our own apps. Here is the complete code for the modified bootStrapper
file:
using System.Windows;
using Microsoft.Practices.Composite.Modularity;
using Microsoft.Practices.Composite.UnityExtensions;
using Microsoft.Practices.Composite.Regions;
using Microsoft.Practices.Unity;
namespace CinchAndPrismRegions
{
class Bootstrapper : UnityBootstrapper
{
protected override DependencyObject CreateShell()
{
#region Setup Cinch with PRISM/CAL supporting service
IRegionManager regionManager = base.Container.Resolve<IRegionManager>();
Cinch.ViewModelBase.ServiceProvider.Add(typeof(IViewManager),
new ViewManager(regionManager));
#endregion
Shell shell = new Shell();
shell.Show();
return shell;
}
protected override IModuleCatalog GetModuleCatalog()
{
ModuleCatalog catalog = new ModuleCatalog();
return catalog;
}
}
}
From this code, there are really only two important things to observe, which are:
- That we are creating (or creating and storing within the Unity IOC container, in case the
IViewManager
service is needed elsewhere, strictly, this is not required, but I just thought I would show you how to add services to the Unity IOC container) an instance of the IViewManager
(which is a service for working with the regions that I have written). This IViewManager
expects a IRegionManager
as a constructor parameter. The IRegionManager
service is a PRISM/CAL Type, which is available from the Unity IOC container, so you can see in the code above that IRegionManager
is being injected into the IViewManager
, by obtaining an instance of the IRegionManager
service from Unity. You will also note that the IViewManager
service is then stored within the Cinch ViewModelBase ServiceProvider
instance. This allows all Cinch ViewModels to make use of the IViewManager
service. This step is specific to Cinch. So if you not using Cinch, you could simply store the IViewManager
service in a singleton somewhere, or even on a property within a base ViewModel class, basically just somewhere from where you can use it easily.
- The next step is to tell PRISM/CAL that we do not really want to work with modules. We do this by overriding the
GetModuleCatalog()
method.
Step 4: Creating a Region Supporting UI Service
So if we want to work with regions, we should really be looking to create a reusable object that we can use all over the place. This is the job of services typically. So I went about creating a simple Region supporting service which I have called ViewManager
, which simply looks like this:
Service Definition
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace CinchAndPrismRegions
{
public interface IViewManager
{
void CreateAndShowViewInRegion(String regionName, Type viewType);
void ShowViewInRegion(String regionName, Object viewInstance);
}
}
Service Implementation
And here is a minimal service that works with PRISM/CAL regions:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Practices.Composite.Regions;
using Microsoft.Practices.Unity;
namespace CinchAndPrismRegions
{
public sealed class ViewManager : IViewManager
{
private IRegionManager regionManager;
public ViewManager(IRegionManager regionManager)
{
this.regionManager = regionManager;
}
public void CreateAndShowViewInRegion(String regionName, Type viewType)
{
var content = Activator.CreateInstance(viewType);
regionManager.RegisterViewWithRegion(regionName, () => content);
regionManager.Regions[regionName].Activate(content);
}
public void ShowViewInRegion(String regionName, Object viewInstance)
{
regionManager.RegisterViewWithRegion(regionName, () => viewInstance);
regionManager.Regions[regionName].Activate(viewInstance);
}
}
}
And that is about all you have to know to use PRISM/CAL with your own MVVM framework; obvioulsy, I used Cinch, which already supported UI services, so that is the only thing you will have to watch if you are using your own MVVM framework.
So How About A Small Demo Then?
What use is some code without a small demo? To this end, I have created a small demo app that allows the following:
- There is a single main Window (the shell) which hosts one of the standard PRISM/CAL region enabled controls, a
TabControl
. The shell Window makes use of a small ViewModel called ShellViewModel
.
- That there are two very simple Views that are shown in the shells region. The showing of these views in the shell is done by code in the
ShellViewModel
.
Here is all the XAML code for the Shell Window:
<Window x:Class="CinchAndPrismRegions.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cal="http://www.codeplex.com/CompositeWPF"
Title="Hello World" Height="300" Width="300">
<Window.Resources>
<Style TargetType="{x:Type TabItem}" x:Key="TabItemRegionStyle">
<Setter Property="Header"
Value="{Binding RelativeSource={RelativeSource Self},
Path=Content.DataContext.HeaderText}" />
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row="0">
<Button Margin="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Content="Show View1" Command="{Binding ShowView1InRegionCommand}"/>
<Button Margin="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Content="Show View2" Command="{Binding ShowView2InRegionCommand}"/>
</StackPanel>
<TabControl Grid.Row="1" Name="MainRegion" cal:RegionManager.RegionName="MainRegion"
ItemContainerStyle="{StaticResource TabItemRegionStyle}"/>
</Grid>
</Window>
There are two things to not here:
- The attached
RegionManager
property, which means the TabControl
that uses this attached property can now be used with PRISM/CAL's regions.
- That the
TabControl
uses a ItemContainerStyle
. This ItemContainerStyle
simply shows a piece of text which is the current View name which is fetched from the active View's ViewModel.
OK, so that is the shell's XAML; what about the ViewModel that makes use of the regions? Well, here it is in its entirety:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Cinch;
namespace CinchAndPrismRegions
{
public class ShellViewModel : Cinch.ViewModelBase
{
private SimpleCommand showView1InRegionCommand;
private SimpleCommand showView2InRegionCommand;
public ShellViewModel()
{
showView1InRegionCommand = new SimpleCommand
{
CanExecuteDelegate = x => true,
ExecuteDelegate = x => ExecuteShowView1InRegionCommand()
};
showView2InRegionCommand = new SimpleCommand
{
CanExecuteDelegate = x => true,
ExecuteDelegate = x => ExecuteShowView2InRegionCommand()
};
}
public SimpleCommand ShowView1InRegionCommand
{
get { return showView1InRegionCommand; }
}
public SimpleCommand ShowView2InRegionCommand
{
get { return showView2InRegionCommand; }
}
private void ExecuteShowView1InRegionCommand()
{
this.Resolve<IViewManager>().CreateAndShowViewInRegion(
"MainRegion", typeof(View1));
}
private void ExecuteShowView2InRegionCommand()
{
this.Resolve<IViewManager>().ShowViewInRegion(
"MainRegion", new View2());
}
}
}
And just for completeness, here is the code for one of the ViewModels:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Cinch;
using System.ComponentModel;
namespace CinchAndPrismRegions
{
public class View1ViewModel : Cinch.ViewModelBase
{
private String headerText = String.Empty;
public View1ViewModel()
{
HeaderText = "View1";
}
static PropertyChangedEventArgs headerTextChangeArgs =
ObservableHelper.CreateArgs<View1ViewModel>(x => x.HeaderText);
public String HeaderText
{
get { return headerText; }
set
{
headerText = value;
NotifyPropertyChanged(headerTextChangeArgs);
}
}
}
}
And here is the end result; we can click one of the buttons (which will call the ICommand
s in the ShellViewModel
), and a new View will be shown in the Shell
Window's "mainRegion", which if you recall was the TabControl
.
That's It
Anyway, that is all I wanted to say for now; but as I eluded to in the introduction, I am just about to embark on some big updates to my Cinch MVVM framework to bring it in line with the forthcoming .NET 4.0 / VS2010 release.