Introduction
I know some of you will know that I am a WPF lover, and that I have my own MVVM framework out there called Cinch, and that I not so long ago published a whole series of articles on V2 of Cinch, and you are probably bored to death of it, well, from time to time me too, but someone asked me how easy it would be to get Cinch to work with Prism, and I just had to give it a try.
So this article will demonstrate how easy it is to use my own MVVM framework Cinch V2 with all the good bits and pieces you have grown to love from the Microsoft composite WPF/SL application block A.K.A.: Prism.
Oh, one thing I should mention is that Cinch also relies on a MEF View/ViewModel resolution framework from fellow WPF Disciple Marlon Grech, called MEFedMVVM. This is nothing new, and those of you that have read all the previous Cinch articles will know about this. I know it seems odd for my library to take a dependency on Marlon's, but I have nothing but praise for MEFedMVVM and have not had any problems with it at all, apart from the one time I updated my version of MEFedMVVM I was using with Cinch, where I managed to get the one release where Marlon was "Experimenting" bless. That said, since then, not one problem with MEFedMVVM; I am not worried by this dependency at all, and neither should you be I feel.
Prism V4
I do not know how many of you know what Prism is or keep track of its active releases, but Prism is the composite WPF/SL application block from Microsoft. If you used the Smart Client Software Factory (SCSF) for WinForms, it may look slightly familiar, but Prism is a different beast from SCSF and was written from the ground up to work with WPF and later Silverlight.
It is now in its fourth release, and offers features like:
- Modules (these take the form of separate projects where there is everything you need to create a discrete unit of work, so if you are doing MVVM, this might be a View/ViewModule and DataAccess Layer helpers)
- Regions, which are bits of the UI which are marked up as placeholders (like ASP.NET placeholders) that can take other bits of content at runtime
- Shell (single start window)
- Bootstrapper to get it all wired together
Now, there are a lot of people out there that think Prism is an MVVM framework, but it is not. It might lend itself to doing MVVM, but the truth is even after four releases, it still lacks some of the core bits to the MVVM puzzle. As such, people have written a great many MVVM libraries; this is not because we are all retarded, it is from a genuine need. Prism does not have all the bits you need, so people (myself included) have written MVVM frameworks to try and fill that gap.
That is not to say Prism is bad, it is not bad at all, and its region support alone is very, very attractive, but it still lacks certain things, such as core services you would expect of an MVVM framework, such as MessageBox, Modal windows, etc. Why is this? Well, it's simple: Prism is not an MVVM framework, it is a composite UI block that does have some bells and whistles that make it OK to do MVVM stuff with. The way I see it, Prism should be something you use along side your favourite MVVM framework.
Like I say, Prism is now in its fourth release. Previous releases have all used the Unity application block for Dependency Injection/IOC Container. Version 4 is different, it is also able to use MEF. As luck would have it, Cinch V2 also uses MEF, so this article will show you that you can use Cinch V2 with Prism (v4) with absolutely no issues what so ever.
In fact, I would go as far as to say they are an excellent pair of complimentary frameworks.
Demo 1: Architectural overview
Demo 1 is available as CinchV2AndPrismRegions.zip at the top of this article.
This demo does not use Prism modules but it does show you how to use Prism regions along side Cinch V2 with no bother at all. It also shows the user how to create a custom Prism region adaptor.
There is a single shell window that has a single TabControl
region (using the standard Prism TabControl region adaptor) which will get two views loaded into it at runtime.
There is a welcome view which simply shows a simple bit of rich text, and utilises a Cinch V2 MEF injected ViewModel.
There is an Image view which shows a list of images that match a keyword driven Google search, and allows the user to click on one of the retrieved images, and have it show as a larger image in a custom region adaptor. The Image view also utilises a Cinch V2 MEF injected ViewModel.
Demo 1: What does it look like?
This demo is a bit more elaborate than the second one, but then again, the whole idea of regions/modules is more complicated, so I kept the second demo app quite simple. As previously stated, this first demo has a welcome view, which is pre-loaded in the Shell at startup and looks like this within the Shell.
The demo also allows multiple instances of another random Google image search view to be loaded using the button just below the banner (top left), but there is a pre-loaded one of these random image views loaded into the Shell on startup that is set to search Google for "Flowers", and this looks like this:
Note: This is half way through transitioning from one element to another, more on this later.
I have included a little threading helper control, so this is what it will look like while it is busy fetching data from Google:
To load a new instance of this view, and load some more random Google images, you can use the following button:
Demo 1: How does it all work?
These next sections will walk you through how it all works.
Demo 1: The Shell
The first step I carried out when building this demo was to create the Shell, which obviously meant getting all the relevant DLL references, but you can see that from the actual attached demo code. The Shell is a very simple Window
which has the following XAML, and it really is just a region container for the other views to be loaded into.
Here is the entire Shell XAML:
<Window x:Class="CinchV2AndPrismRegions.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cal="clr-namespace:Microsoft.Practices.Prism.Regions;
assembly=Microsoft.Practices.Prism"
xmlns:regions="clr-namespace:CinchV2AndPrismRegions.Regions"
xmlns:CinchV2="clr-namespace:Cinch;assembly=Cinch.WPF"
xmlns:meffed="clr-namespace:MEFedMVVM.ViewModelLocator;assembly=MEFedMVVM.WPF"
xmlns:i="clr-namespace:System.Windows.Interactivity;
assembly=System.Windows.Interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
Icon="/CinchV2AndPrismRegions;component/Images/CinchIcon.png"
Title="Shell"
WindowState="Maximized"
WindowStyle="ThreeDBorderWindow"
WindowStartupLocation="CenterScreen"
Width="800"
Height="600"
meffed:ViewModelLocator.ViewModel="ShellViewModel">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="87"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" Height="87"
HorizontalAlignment="Stretch">
<Image Height="86" Width="462"
Source="/CinchV2AndPrismRegions;component/Images/BannerLeft.png"
HorizontalAlignment="Left"
VerticalAlignment="Top"/>
<Image Height="86" Width="244"
Source="/CinchV2AndPrismRegions;component/Images/BannerRight.png"
HorizontalAlignment="Right"
VerticalAlignment="Top"/>
<Rectangle Fill="Black" VerticalAlignment="Bottom"
Height="7" HorizontalAlignment="Stretch"/>
</Grid>
<DockPanel LastChildFill="True" Grid.Row="1"
Background="{StaticResource verticalTabHeaderBackground}">
<Image Height="32" Width="32"
Margin="10,2,2,2" DockPanel.Dock="Top"
Source="/CinchV2AndPrismRegions;component/Images/google.png"
HorizontalAlignment="left"
VerticalAlignment="Center"
ToolTip="Add New Google Image Search View">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonUp">
<CinchV2:EventToCommandTrigger
Command="{Binding AddNewGoogleCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<Image.Effect>
<DropShadowEffect ShadowDepth="0"
Color="White" BlurRadius="10" />
</Image.Effect>
</Image>
<TabControl Margin="0"
Style="{StaticResource TabControlStyleVerticalTabs}"
ItemContainerStyle="{StaticResource TabItemStyleVerticalTabs}"
cal:RegionManager.RegionName="{x:Static regions:RegionNames.MainRegion}"/>
</DockPanel>
</Grid>
</Window>
And here is its code-behind; note that is marked with a MEF ExportAttrtibute
which allows the MEF CompositionContainer
(we will see that next) to be able to resolve the Shell
type and any MEF Import
s the Shell
may require. It should be noted that in this demo, the Shell
does not need to satisfy any other Import
s, but in real life code, it more than likely would, so it is good practice to Export
the Shell
; it is also the defacto way that Prism works in V4.
[Export("CinchV2AndPrismRegions.Shell", typeof(Shell))]
public partial class Shell : Window
{
public Shell()
{
InitializeComponent();
}
}
It should also be noted that the Shell
is using MEFedMVVM to resolve its ViewModel, which is of type ShellViewModel
. ShellViewModel
basically starts up and immediately populates the TabControl
region "MainRegion" with a single WelcomeView
and a single GoogleImageSearchView
; this is shown below. ShellViewModel
also allows new instances of a GoogleImageSearchView
to be created where the GoogleImageSearchView
will have some contextual data set by the ShellViewModel
which will dictate the name of the region in the new GoogleImageSearchView
just created. Care needs to be taken to ensure region names are unique within the same parent scope. More on this below.
For now, here is the full listing of ShellViewModel
:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MEFedMVVM.ViewModelLocator;
using System.ComponentModel.Composition;
using Cinch;
using System.ComponentModel;
using Microsoft.Practices.Prism.Regions;
using System.Windows;
using CinchV2AndPrismRegions.Regions;
using CinchV2AndPrismRegions.Views;
using System.ComponentModel.Composition.Primitives;
namespace CinchV2AndPrismRegions.ViewModels
{
[ExportViewModel("ShellViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class ShellViewModel : Cinch.ViewModelBase
{
#region Data
private int searchViewsInstanceCounter = 0;
private int searchViewsCounter = 0;
private String[] randomSearchTerms = new string[]
{
"alien","robots","shoebill",
"girls","lazer","martians","manga",
"anime","lizard","elf","lizard",
"dog","gun","gangsta","soldier",
"monsters","Zombie","goat"
};
private Random rand = new Random();
private IViewAwareStatus viewAwareStatus;
private IMessageBoxService messageBoxService;
#endregion
[ImportingConstructor]
public ShellViewModel(IViewAwareStatus viewAwareStatus,
IMessageBoxService messageBoxService)
{
this.viewAwareStatus = viewAwareStatus;
this.messageBoxService = messageBoxService;
this.viewAwareStatus.ViewLoaded += ViewAwareStatus_ViewLoaded;
AddNewGoogleCommand =
new SimpleCommand<Object, Object>(ExecuteAddNewGoogleCommand);
Mediator.Instance.Register(this);
}
public SimpleCommand<Object, Object> AddNewGoogleCommand { get; private set; }
#region Private Methods
private void ViewAwareStatus_ViewLoaded()
{
IRegionManager regionManager =
RegionManager.GetRegionManager((DependencyObject)viewAwareStatus.View);
IRegion region = regionManager.Regions[RegionNames.MainRegion];
WelcomeView welcomeView = ViewModelRepository.Instance.Resolver
.Container.GetExport<WelcomeView>().Value;
region.Add(welcomeView, "preloadedWelcomeView");
GoogleImageSearchView googleImageSearchView =
ViewModelRepository.Instance.Resolver
.Container.GetExport<GoogleImageSearchView>().Value;
googleImageSearchView.ContextualData =
new Model.GoogleImageSearchInfo("Flower",
string.Format("Imageregion_{0}", searchViewsCounter++));
region.Add(googleImageSearchView, "preloadedGoogleImageSearchView");
searchViewsInstanceCounter++;
region.Activate(welcomeView);
}
[MediatorMessageSinkAttribute("DecrementSearchCount")]
public void OnDecrementSearchCount(bool dummy)
{
searchViewsInstanceCounter--;
}
private void ExecuteAddNewGoogleCommand(Object args)
{
if (searchViewsInstanceCounter >= 5)
{
messageBoxService.ShowError(
"This demo only supports 5 search views to be " +
"open at once\r\nPlease close one or more instances");
return;
}
IRegionManager regionManager = RegionManager.GetRegionManager(
(DependencyObject)viewAwareStatus.View);
IRegion region = regionManager.Regions[RegionNames.MainRegion];
GoogleImageSearchView googleImageSearchView =
ViewModelRepository.Instance.Resolver.Container.
GetExport<GoogleImageSearchView>().Value;
googleImageSearchView.ContextualData =
new Model.GoogleImageSearchInfo(
randomSearchTerms[rand.Next(randomSearchTerms.Length)],
string.Format("Imageregion_{0}", searchViewsCounter++));
region.Add(googleImageSearchView);
region.Activate(googleImageSearchView);
searchViewsInstanceCounter++;
}
#endregion
}
}
Demo 1: Custom region adaptor
One of the nice things that Prism lets you do is create a custom region adaptor, to work with a control that there may not already be a Prism region adaptor for.
As I stated, this demo makes use of a custom region adaptor. I have created a special region adaptor that works with the fabulous TransitionElement
from the excellent Transitionals CodePlex project.
The actual custom region adaptor is declared like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Practices.Prism.Regions;
using System.Windows.Controls;
using System.Windows;
using System.ComponentModel.Composition;
using Transitionals.Controls;
namespace CinchV2AndPrismRegions.Regions
{
[Export("CinchV2AndPrismRegions.Regions.TransitionElementAdaptor",
typeof(TransitionElementAdaptor))]
public class TransitionElementAdaptor : RegionAdapterBase<TransitionElement>
{
[ImportingConstructor]
public TransitionElementAdaptor(IRegionBehaviorFactory behaviorFactory) :
base(behaviorFactory)
{
}
protected override void Adapt(IRegion region, TransitionElement regionTarget)
{
region.Views.CollectionChanged += (s, e) =>
{
if (e.Action ==
System.Collections.Specialized.NotifyCollectionChangedAction.Add)
foreach (FrameworkElement element in e.NewItems)
regionTarget.Content = element;
if (e.Action ==
System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
foreach (FrameworkElement element in e.OldItems)
{
regionTarget.Content = null;
GC.Collect();
}
};
}
protected override IRegion CreateRegion()
{
return new AllActiveRegion();
}
}
}
And we make use of this custom region adaptor in GoogleImageSearchView
as follows; we use the XAML:
<transitionalsControls:TransitionElement Margin="10,20,20,20"
x:Name="transitionElement" Transition="{Binding TransitionToUse}">
</transitionalsControls:TransitionElement>
And we also have the following code-behind which sets the region name to a dynamically changing string such that there are no two regions with the same name in the app ever. With Prism, if you want to make sure your content gets put into the correct region, that name needs to be unique, and as the demo allows multiple instances of the same GoogleImageSearchView
to be opened, we need to make sure the region name is generated and assigned on the fly. That is what the code-behind does, along with some code in a command handler in the ShellViewModel
which is run whenever a new GoogleImageSearchView
is asked to be shown.
This is the code-behind for GoogleImageSearchView
that sets the TransitionElement
region name:
public GoogleImageSearchInfo ContextualData
{
get
{
return contextualData;
}
set
{
contextualData = value;
transitionElement.SetValue(RegionManager.RegionNameProperty,
contextualData.RegionName);
}
}
Which we can see being set via the ShellViewModel
command handler code shown below.
private void ExecuteAddNewGoogleCommand(Object args)
{
if (searchViewsInstanceCounter >= 5)
{
messageBoxService.ShowError("This demo only supports 5 search views " +
"to be open at once\r\nPlease close one or more instances");
return;
}
IRegionManager regionManager =
RegionManager.GetRegionManager((DependencyObject)viewAwareStatus.View);
IRegion region = regionManager.Regions[RegionNames.MainRegion];
GoogleImageSearchView googleImageSearchView =
ViewModelRepository.Instance.Resolver.
Container.GetExport<GoogleImageSearchView>().Value;
googleImageSearchView.ContextualData =
new Model.GoogleImageSearchInfo(
randomSearchTerms[rand.Next(randomSearchTerms.Length)],
string.Format("Imageregion_{0}", searchViewsCounter++));
region.Add(googleImageSearchView);
region.Activate(googleImageSearchView);
searchViewsInstanceCounter++;
}
This region gets used whenever one of the smaller images in GoogleImageSearchView
is clicked, but more on this later.
This is what the TransitionElement
based region looks like mid-transition:
Demo 1: Bootstrapper
The next thing one must do when creating a Prism V4 application is to create a bootstrapper which inherits from MefBootstrapper
; this is where the Shell
is created and other Prism related overrides should be done.
For the demo application, the bootstrapper looks like this:
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition.Hosting;
using System.Linq;
using System.Text;
using System.Windows;
using Microsoft.Practices.Prism.MefExtensions;
using Microsoft.Practices.Prism.Regions;
using CinchV2AndPrismRegions.Regions;
using Cinch;
using System.Reflection;
using MEFedMVVM.ViewModelLocator;
using CinchV2AndPrismRegions.Views;
using System.Windows.Controls;
using Transitionals.Controls;
using System.ComponentModel.Composition.Primitives;
namespace CinchV2AndPrismRegions
{
public class CinchV2AndPrismRegionsBootstrapper : MefBootstrapper, IComposer, IContainerProvider
{
public override void Run(bool runWithDefaultConfiguration)
{
base.Run(runWithDefaultConfiguration);
}
#region Overrides of Bootstrapper
protected override void ConfigureAggregateCatalog()
{
this.AggregateCatalog.Catalogs.Add(new AssemblyCatalog(typeof(App).Assembly));
this.AggregateCatalog.Catalogs.Add(
new AssemblyCatalog(typeof(Cinch.WPFMessageBoxService).Assembly));
}
protected override void InitializeShell()
{
base.InitializeShell();
MEFedMVVM.ViewModelLocator.LocatorBootstrapper.ApplyComposer(this);
CinchBootStrapper.Initialise(new List<Assembly> { typeof(App).Assembly });
Application.Current.MainWindow = (Shell)this.Shell;
Application.Current.MainWindow.Show();
}
protected override CompositionContainer CreateContainer()
{
var exportProvider = new MEFedMVVMExportProvider(MEFedMVVMCatalog.CreateCatalog(AggregateCatalog));
_compositionContainer = new CompositionContainer(exportProvider);
exportProvider.SourceProvider = _compositionContainer;
return _compositionContainer;
}
protected override RegionAdapterMappings ConfigureRegionAdapterMappings()
{
RegionAdapterMappings mappings = base.ConfigureRegionAdapterMappings();
mappings.RegisterMapping(typeof(TransitionElement),
Container.GetExportedValue<TransitionElementAdaptor>());
return mappings;
}
protected override DependencyObject CreateShell()
{
return this.Container.GetExportedValue<Shell>();
}
#endregion
#region Implementation of IComposer (For MEFedMVVM)
public ComposablePartCatalog InitializeContainer()
{
return this.AggregateCatalog;
}
public IEnumerable<ExportProvider> GetCustomExportProviders()
{
return null;
}
#endregion
#region Implementation of IContainerProvider(For MEFedMVVM)
CompositionContainer IContainerProvider.CreateContainer()
{
return _compositionContainer;
}
#endregion
}
}
There are a few things to note in there to do with getting Cinch V2 to work nicely with Prism; these things of note are:
- That the MEFedMVVM (therefore Cinch V2)
IComposer
interface is implemented such that we can instruct MEFedMVVM to use the same CompositionContainer
and Parts (Export
s/Import
s) as Prism. You can literally follow the example in this demo; that is all you need to do.
- That we add the relevant catalogs to make Cinch V2/MEFedMVVM work in the
ConfigureAggregateCatalog()
override.
- That the following line is run in the
CreateShell
override MEFedMVVM.ViewModelLocator.LocatorBootstrapper.ApplyComposer(this);
.
- That I am also running the Cinch V2 bootstrapper to get it resolve any popups / workspace views that are attributed up with the relevant Cinch V2 attributes (such as
PopupNameToViewLookupKeyMetadata
/ ViewnameToViewLookupKeyMetadata
).
- That we also override the
ConfigureRegionAdapterMappings()
method to add in our custom region adaptors.
That is all you need to do to get Cinch V2 (and remember Cinch V2 makes use of MEFedMVVM for ViewModel resolution) /Prism to work together. Simple, isn't it?
Now that we have a bootstrapper, we need to make sure it is called, which is typically done in a Prism application in the App.xaml.cs code as follows:
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
CinchV2AndPrismRegionsBootstrapper bootstrapper =
new CinchV2AndPrismRegionsBootstrapper();
bootstrapper.Run();
this.ShutdownMode = ShutdownMode.OnMainWindowClose;
}
}
Demo 1: Welcome View
The welcome view is pretty simple and just looks like this:
In fact, the XAML for the WelcomeView is dead simple too; here it is:
<UserControl x:Class="CinchV2AndPrismRegions.Views.WelcomeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:meffed="clr-namespace:MEFedMVVM.ViewModelLocator;assembly=MEFedMVVM.WPF"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
meffed:ViewModelLocator.ViewModel="WelcomeViewModel">
<Grid Background="White" Margin="5,0,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Border Height="30" Margin="0,10,10,0"
VerticalAlignment="Bottom" CornerRadius="5">
<Border.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="Black" Offset="1"/>
<GradientStop Color="#FF7C7C7C"/>
<GradientStop Color="#FF3D3D3D" Offset="0.5"/>
</LinearGradientBrush>
</Border.Background>
</Border>
<StackPanel Orientation="Horizontal"
Grid.Row="0" Grid.Column="0"
Grid.ColumnSpan="2">
<Image HorizontalAlignment="Center" Margin="10,-3,0,-17"
Source="/CinchV2AndPrismRegions;component/Images/blackInfo.png"
Stretch="Fill" Width="60"
Height="60" VerticalAlignment="Center"/>
<Label Content="Information About This App" FontSize="15"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center" Foreground="White"
FontFamily="Verdana" FontWeight="Bold"
Padding="0" Margin="10,10,0,0"
HorizontalAlignment="Left"
d:LayoutOverrides="Height, GridBox"/>
</StackPanel>
<TextBlock Margin="5,30,5,5" Grid.Row="1"
TextWrapping="Wrap"
FontFamily="Verdana"><Run Language="en-gb"
Text="This small "/>
<Run Foreground="#FF020202" FontWeight="Bold"
Language="en-gb" Text="Cinch"/>
<Run Foreground="#FFF15C23" FontWeight="Bold"
Language="en-gb" Text=" V2 "/>
<Run Language="en-gb" Text="demo shows just how you easy it is to use "/>
<Run FontWeight="Bold" Language="en-gb" Text="Cinch "/>
<Run Foreground="#FFF15C23" FontWeight="Bold" Language="en-gb" Text="V2"/>
<Run Foreground="#FFF15C23" Language="en-gb" Text=" "/>
<Run Language="en-gb"
Text="along side Microsofts Composite WPF block
(Aka Patterns and Practices "/>
<Run FontWeight="Bold" Language="en-gb" Text="PRISM"/>
<Run Language="en-gb" Text="). "/><LineBreak/>
<Run Language="en-gb"/><LineBreak/>
<Run Language="en-gb" Text="This is largely down to the fact that "/>
<Run FontWeight="Bold" Language="en-gb" Text="PRISM "/>
<Run Language="en-gb" Text="and "/>
<Run FontWeight="Bold" Language="en-gb" Text="Cinch "/>
<Run Foreground="#FFF15C23" FontWeight="Bold" Language="en-gb" Text="V2 "/>
<Run Language="en-gb"
Text="both use MEF to assemble their UI's,
so it really could not be easier to use "/>
<Run FontWeight="Bold" Language="en-gb" Text="PRISM"/>
<Run Language="en-gb" Text="s excellent region support alongside "/>
<Run FontWeight="Bold" Language="en-gb" Text="Cinch "/>
<Run Foreground="#FFF15C23" FontWeight="Bold" Language="en-gb" Text="V2 "/>
<Run Language="en-gb" Text="other classes, should you wish to do so."/>
<LineBreak/>
<Run Language="en-gb"/><LineBreak/>
<Run Language="en-gb"
Text="This demo is WPF based, but the same
rules will apply when working with "/>
<Run FontWeight="Bold" Language="en-gb" Text="Cinch "/>
<Run Foreground="#FFF15C23" FontWeight="Bold" Language="en-gb" Text="V2"/>
<Run Foreground="#FFF15C23" Language="en-gb"
Text=" "/><Run Language="en-gb" Text="for "/>
<Run Foreground="#FF46B7E7" FontWeight="Bold"
Language="en-gb" Text="Silverlight "/>
<Run Language="en-gb" Text="and making use of "/>
<Run FontWeight="Bold" Language="en-gb"
Text="PRISM "/><Run Language="en-gb"
Text="for "/>
<Run Foreground="#FF46B7E7" FontWeight="Bold"
Language="en-gb" Text="Silverlight"/></TextBlock>
</Grid>
</UserControl>
The only thing of note here is that the WelcomeView resolves its ViewModel using MEFedMVVM, where the WelcomeViewModel
looks like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MEFedMVVM.ViewModelLocator;
using System.ComponentModel.Composition;
namespace CinchV2AndPrismRegions.ViewModels
{
[ExportViewModel("WelcomeViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class WelcomeViewModel : Cinch.ViewModelBase
{
public WelcomeViewModel()
{
base.IsCloseable = false;
}
public string ViewName
{
get { return "Welcome"; }
}
}
}
It can be seen that it inherits from the Cinch.ViewModelBase
class and as such is able to set the IsCloseable
property, which is used when styling the TabItem
s for the Shell TabControl
region (see the demo's AppStyle.xaml resources for that).
Demo 1: Image View
The second view in this first demo is slightly more sophisticated, and uses a freely available Google image search which you will find in the attached code that searches Google's images using a random keyword. It presents these images in a ItemsControl
and allows the user to use a standard Cinch V2 event to command Action to fire a command in the GoogleImageSearchViewModel
when the user MouseDowns over one of the images in the ItemsControl
. The Google searching can take some time to do, so this calling is wrapped up in a new UI service to fetch the image URLs, which uses a Task Parallel Library Task
to conduct this work.
It also hosts the special TranstionElement
region adaptor we discussed earlier.
When the command is run in response to a MouseDown in GoogleImageSearchViewModel
, a new view is requested to be put into the TranstionElement
region adaptor. As you would expect, this new view transitions into place using the currently selected transition type in the GoogleImageSearchViewModel
.
Let's start with the GoogleImageSearchView
; here is the most relevant parts of its XAML:
<UserControl x:Class="CinchV2AndPrismRegions.Views.GoogleImageSearchView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:CinchV2="clr-namespace:Cinch;assembly=Cinch.WPF"
xmlns:meffed="clr-namespace:MEFedMVVM.ViewModelLocator;assembly=MEFedMVVM.WPF"
xmlns:i="clr-namespace:System.Windows.Interactivity;
assembly=System.Windows.Interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:model="clr-namespace:CinchV2AndPrismRegions.Model"
xmlns:transitions="clr-namespace:Transitionals.Transitions;assembly=Transitionals"
xmlns:transitionals="clr-namespace:Transitionals;assembly=Transitionals"
xmlns:transitionalsControls="clr-namespace:Transitionals.Controls;
assembly=Transitionals"
xmlns:controls="clr-namespace:CinchV2AndPrismRegions.Controls"
mc:Ignorable="d"
x:Name="theView"
d:DesignHeight="300" d:DesignWidth="300"
meffed:ViewModelLocator.ViewModel="GoogleImageSearchViewModel">
<UserControl.Resources>
<DataTemplate x:Key="googleImageTemplate" DataType="{x:Type model:ImageInfo}">
<Image Margin="5" HorizontalAlignment="Center"
VerticalAlignment="Center"
Source="{Binding ImageUrl}"
ToolTip="{Binding Title}"
Width="80" Height="80"
Stretch="UniformToFill">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonUp">
<CinchV2:EventToCommandTrigger
Command="{Binding ElementName=theView,
Path=DataContext.SelectImageCommand}"
CommandParameter="{Binding}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Image>
</DataTemplate>
</UserControl.Resources>
<controls:AsyncHost AsyncState="{Binding Path=AsyncState, Mode=OneWay}">
<Grid controls:AsyncHost.AsyncContentType="Content"
Background="White">
<ItemsControl Margin="10,20,10,10" VerticalAlignment="Top"
ItemsSource="{Binding GoogleImageResults}"
ItemTemplate="{StaticResource googleImageTemplate}"
Grid.Row="1"
BorderBrush="{x:Null}"
MinHeight="350" MaxHeight="350">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<DockPanel Grid.Row="1" Grid.Column="1" Margin="10"
LastChildFill="True">
<transitionalsControls:TransitionElement Margin="10,20,20,20"
x:Name="transitionElement"
Transition="{Binding TransitionToUse}">
</transitionalsControls:TransitionElement>
</DockPanel>
</Grid>
<controls:AsyncBusyUserControl
controls:AsyncHost.AsyncContentType="Busy"
AsyncWaitText="{Binding Path=WaitText, Mode=OneWay}"
Visibility="Hidden" />
<controls:AsyncFailedUserControl
controls:AsyncHost.AsyncContentType="Error"
Error="{Binding Path=ErrorText, Mode=OneWay}"
Visibility="Hidden" />
</controls:AsyncHost>
</UserControl>
It's all pretty standard stuff, apart from two things, one being the TransitionElement
control from the Transitionals.dll, and the other being a special threading control which I have developed which has three items in it, where only one is shown at any one time. It can show either the Content or a Busy control, or a Failed control; this showing/hiding is handled from the GoogleImageSearchViewModel
And here is its code-behind. Note the use of the IViewContext
that we saw earlier when we were looking at the ShellViewModel
, where the ShellViewModel
sets the random keyword and region name for the GoogleImageSearchView
.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ComponentModel.Composition;
using CinchV2AndPrismRegions.Regions;
using Microsoft.Practices.Prism.Regions;
using CinchV2AndPrismRegions.Model;
namespace CinchV2AndPrismRegions.Views
{
[Export("CinchV2AndPrismRegions.Views.GoogleImageSearchView",
typeof(GoogleImageSearchView))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public partial class GoogleImageSearchView :
UserControl, IViewContext<GoogleImageSearchInfo>
{
private GoogleImageSearchInfo contextualData;
public GoogleImageSearchView()
{
InitializeComponent();
}
#region IViewContext<GoogleImageSearchInfo> Members
public GoogleImageSearchInfo ContextualData
{
get
{
return contextualData;
}
set
{
contextualData = value;
transitionElement.SetValue(RegionManager.RegionNameProperty,
contextualData.RegionName);
}
}
#endregion
}
}
Now let's have a look at the most relevant parts of GoogleImageSearchViewModel
, which is shown below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MEFedMVVM.ViewModelLocator;
using System.ComponentModel.Composition;
using System.ComponentModel;
using Cinch;
using CinchV2AndPrismRegions.Views;
using MEFedMVVM.Common;
using Microsoft.Practices.Prism.Regions;
using System.Windows;
using CinchV2AndPrismRegions.Regions;
using CinchV2AndPrismRegions.Services.Contracts;
using CinchV2AndPrismRegions.Model;
using System.Windows.Data;
using Transitionals;
using Transitionals.Transitions;
using System.Windows.Controls;
using CinchV2AndPrismRegions.Enums;
namespace CinchV2AndPrismRegions.ViewModels
{
[ExportViewModel("GoogleImageSearchViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class GoogleImageSearchViewModel : Cinch.ViewModelBase
{
private string contentTitle = "GoogleImageSearch";
private IMessageBoxService messageBoxService;
private IViewAwareStatus viewAwareStatus;
private IGoogleSearchProvider googleSearchProvider;
private IEnumerable<ImageInfo> googleImageResults=null;
private IRegion imageRegion = null;
private Transition transitionToUse = new FadeAndBlurTransition();
private TransitionType selectedTransitonType = TransitionType.FadeAndBlur;
private Dictionary<TransitionType, Transition>
transitionsLookup = new Dictionary<TransitionType, Transition>();
private string uniqueRegionNameForThisInstance;
private string waitText;
private string errorMessage;
private AsyncType asyncState = AsyncType.Content;
[ImportingConstructor]
public GoogleImageSearchViewModel(
IMessageBoxService messageBoxService,
IGoogleSearchProvider googleSearchProvider,
IViewAwareStatus viewAwareStatus)
{
base.IsCloseable = true;
this.messageBoxService = messageBoxService;
this.googleSearchProvider = googleSearchProvider;
this.viewAwareStatus = viewAwareStatus;
this.viewAwareStatus.ViewLoaded += ViewAwareStatus_ViewLoaded;
transitionsLookup.Add(TransitionType.FadeAndBlur,
new FadeAndBlurTransition());
......
......
......
......
......
transitionsLookup.Add(TransitionType.VerticalWipeTransition,
new VerticalWipeTransition());
CloseViewCommand =
new SimpleCommand<Object, Object>(ExecuteCloseViewCommand);
SelectImageCommand =
new SimpleCommand<Object, Object>(ExecuteSelectImageCommand);
Mediator.Instance.Register(this);
}
private void ViewAwareStatus_ViewLoaded()
{
string keyword = "";
if (!Designer.IsInDesignMode)
{
IViewContext<GoogleImageSearchInfo> view =
(IViewContext<GoogleImageSearchInfo>)viewAwareStatus.View;
if (view.ContextualData != null && googleImageResults == null)
{
ContentTitle = string.Format("Searching using keyword : {0}",
view.ContextualData.KeyWord);
uniqueRegionNameForThisInstance = view.ContextualData.RegionName;
keyword = view.ContextualData.KeyWord;
}
}
AsyncState = AsyncType.Busy;
WaitText = string.Format("Fetching random Google " +
"images for keyword : {0}", keyword);
googleSearchProvider.GetAll(keyword, ShowGoogleResults,
ShowGoogleException);
}
private void ShowGoogleResults(IEnumerable<ImageInfo> results)
{
googleImageResults = results;
NotifyPropertyChanged(googleImageResultsArgs);
AsyncState = AsyncType.Content;
}
private void ShowGoogleException(Exception ex)
{
ErrorMessage = ex.Message;
AsyncState = AsyncType.Error;
}
public SimpleCommand<Object, Object> CloseViewCommand { get; private set; }
public SimpleCommand<Object, Object> SelectImageCommand { get; private set; }
public Array TransitionTypes
{
get
{
return Enum.GetValues(typeof(TransitionType));
}
}
static PropertyChangedEventArgs asyncStateArgs =
ObservableHelper.CreateArgs<GoogleImageSearchViewModel>(x => x.AsyncState);
public AsyncType AsyncState
{
get { return asyncState; }
private set
{
asyncState = value;
NotifyPropertyChanged(asyncStateArgs);
}
}
static PropertyChangedEventArgs waitTextArgs =
ObservableHelper.CreateArgs<GoogleImageSearchViewModel>(x => x.WaitText);
public string WaitText
{
get { return waitText; }
private set
{
waitText = value;
NotifyPropertyChanged(waitTextArgs);
}
}
static PropertyChangedEventArgs errorMessageArgs =
ObservableHelper.CreateArgs<GoogleImageSearchViewModel>(
x => x.ErrorMessage);
public string ErrorMessage
{
get { return errorMessage; }
private set
{
errorMessage = value;
NotifyPropertyChanged(errorMessageArgs);
}
}
static PropertyChangedEventArgs selectedTransitonTypeArgs =
ObservableHelper.CreateArgs<GoogleImageSearchViewModel>(
x => x.SelectedTransitonType);
public TransitionType SelectedTransitonType
{
get { return selectedTransitonType; }
set
{
selectedTransitonType = value;
NotifyPropertyChanged(selectedTransitonTypeArgs);
TransitionToUse = transitionsLookup[value];
}
}
static PropertyChangedEventArgs transitionToUseArgs =
ObservableHelper.CreateArgs<GoogleImageSearchViewModel>(
x => x.TransitionToUse);
public Transition TransitionToUse
{
get { return transitionToUse; }
private set
{
transitionToUse = value;
NotifyPropertyChanged(transitionToUseArgs);
}
}
static PropertyChangedEventArgs googleImageResultsArgs =
ObservableHelper.CreateArgs<GoogleImageSearchViewModel>(
x => x.GoogleImageResults);
public IEnumerable<ImageInfo> GoogleImageResults
{
get { return googleImageResults; }
}
public string ViewName
{
get { return "GoogleImageSearch"; }
}
static PropertyChangedEventArgs contentTitleArgs =
ObservableHelper.CreateArgs<GoogleImageSearchViewModel>(
x => x.ContentTitle);
public string ContentTitle
{
get { return contentTitle; }
private set
{
contentTitle = value;
NotifyPropertyChanged(contentTitleArgs);
}
}
#region Comand Handlers
private void ExecuteCloseViewCommand(Object args)
{
CustomDialogResults result =
messageBoxService.ShowYesNo("Are you sure you want to close this tab?",
CustomDialogIcons.Question);
if (result == CustomDialogResults.Yes)
{
IRegionManager regionManager =
RegionManager.GetRegionManager((DependencyObject)viewAwareStatus.View);
IRegion region = regionManager.Regions[RegionNames.MainRegion];
region.Remove(args);
Mediator.Instance.NotifyColleagues<bool>("DecrementSearchCount", true);
}
}
private void ExecuteSelectImageCommand(Object args)
{
Image selectedImageInfo = (Image)((EventToCommandArgs)args).Sender;
if (imageRegion == null)
{
IRegionManager regionManager =
RegionManager.GetRegionManager(
(DependencyObject)viewAwareStatus.View);
imageRegion =
regionManager.Regions[uniqueRegionNameForThisInstance];
}
ImageView imageView = ViewModelRepository.Instance.Resolver.
Container.GetExport<ImageView>().Value;
((IViewContext<ImageInfo>)imageView).ContextualData =
new ImageInfo(selectedImageInfo.ToolTip.ToString(),
selectedImageInfo.Source);
var view = imageRegion.GetView("ImageView");
if (view != null)
{
imageRegion.Remove(view);
}
imageRegion.Add(imageView, "ImageView");
}
#endregion
}
}
I think most of that code is pretty self-explanatory; most of it is using Cinch V2 Core UI Services, namely IMessageBoxService
and IViewAwareStatus
.
But GoogleImageSearchViewModel
also makes use of a special service (of type ItemRepository
) specifically for the GoogleImageSearchViewModel
. This service is the one that is using the Task Parallel Library Task
; as such, when we use this potentially long running service, we can signal the threading control in the view to show a busy indicator while we are doing the work and to show either content or the failed control when we either finish getting the results or get an Exception
. This should be clear enough from the code within the GoogleImageSearchViewModel
.
Where the service contract looks like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using CinchV2AndPrismRegions.Model;
namespace CinchV2AndPrismRegions.Services.Contracts
{
public class SearchResult<T>
{
readonly T package;
readonly Exception error;
public T Package { get { return package; } }
public Exception Error { get { return error; } }
public SearchResult(T package, Exception error)
{
this.package = package;
this.error = error;
}
}
public interface IGoogleSearchProvider
{
void GetAll(
string keyword,
Action<IEnumerable<ImageInfo>> resultCallback,
Action<Exception> errorCallback);
}
}
where the runtime version of this service looks like this, where we use the Task Parallel Library to conduct this fetching using a Task
(as I say, this takes no time at all, but it does show you how you might call something using a Task
that would take a long time):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition;
using MEFedMVVM.ViewModelLocator;
using CinchV2AndPrismRegions.Services.Contracts;
using CinchV2AndPrismRegions.Model;
using System.Threading;
using System.Threading.Tasks;
using Gapi.Search;
namespace CinchV2AndPrismRegions.Services.Runtime
{
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.Runtime, typeof(IGoogleSearchProvider))]
public class ItemRepository : IGoogleSearchProvider
{
void IGoogleSearchProvider.GetAll(string keyword,
Action<IEnumerable<ImageInfo>> resultCallback,
Action<Exception> errorCallback)
{
Task<SearchResult<IEnumerable<ImageInfo>>> task =
Task.Factory.StartNew(() =>
{
try
{
List<ImageInfo> items = new List<ImageInfo>();
SearchResults searchResults =
Searcher.Search(SearchType.Image, keyword);
if (searchResults.Items.Count() > 0)
{
foreach (var searchResult in searchResults.Items)
{
items.Add(new ImageInfo(
searchResult.Title, searchResult.Url));
}
}
return new SearchResult<IEnumerable<ImageInfo>>(items, null);
}
catch (Exception ex)
{
return new SearchResult<IEnumerable<ImageInfo>>(null, ex);
}
});
task.ContinueWith(r =>
{
if (r.Result.Error != null)
{
errorCallback(r.Result.Error);
}
else
{
resultCallback(r.Result.Package);
}
}, CancellationToken.None, TaskContinuationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
}
}
}
And here is a design time service, which is actually in an entirely different project, that does not even have to be referenced by any other project. Basically, as long as the project that holds the design time service references MEFedMVVM.WPF, it should be resolved at design time and show up in Blend.
One important note is that I am using Windows 7, so the design time service shown below uses paths found to sample images on Windows 7. You may have to alter these paths if you are not using Windows 7.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition;
using MEFedMVVM.ViewModelLocator;
using CinchV2AndPrismRegions.Services.Contracts;
using CinchV2AndPrismRegions.Model;
namespace DesignTimeServices
{
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.DesignTime, typeof(IGoogleSearchProvider))]
public class DesignTimeItemRepository : IGoogleSearchProvider
{
void IGoogleSearchProvider.GetAll(string keyword,
Action<IEnumerable<ImageInfo>> resultCallback,
Action<Exception> errorCallback)
{
List<ImageInfo> results = new List<ImageInfo>();
results.Add(new ImageInfo("robot1",
@"C:\Users\Public\Pictures\Sample Pictures\Chrysanthemum.jpg"));
results.Add(new ImageInfo("robot2",
@"C:\Users\Public\Pictures\Sample Pictures\Tulips.jpg"));
resultCallback(results);
}
}
}
And to prove that this all works OK, let's go back to our original screenshot of this Cinch V2 with Prism solution in Blend 4.
See that all is good. I prefer the way that MEFedMVVM deals with design time data, as opposed to how the Blend d: design time tags work, as they assume a default constructor is available for your ViewModel. I have to say in my production code, I rarely find that I have ViewModels which have a default constructor; they normally always rely on some context or services. Also, using the d: Blend design tags means you mock the whole ViewModel. It should be the services that provide the data, so they should be mocked, not the entire ViewModel. Also, Blend requires all get/set properties to do this mocking, which I think encourages bad design.
But hey, that is just my 2 cents.
Demo 2: Architectural overview
Demo 2 is available in CinchV2AndPrismModulesRegions.zip at the top of this article.
The second demo article included in this article is designed in a more traditional Prism style, with separate Modules (projects) which are all brought into a single application at runtime.
Personally, I am not a massive fan of Modules, and prefer my solutions to be structured something like this:
- Shell project
- Views project
- Controls project
- ViewModels project
I think, for me, that shows much better separation of concerns (SOC) than that used by adopting the Prism Modules paradigm. I mean, if you follow the separate ViewModels project, and it has no knowledge of any of the WPF/SL specific DLLs, such as PresentationCore, and someone goes to add something like that, they are more likely to think, hang on here, why am I adding this DLL to the ViewModels project? But I know I am more than likely in the minority here, so as I say, this demo goes about showing you how to use Cinch V2 with all the standard Prism goodies like Modules/Regions etc.
There is a single shell window that has a single TabControl
region (using the standard Prism TabControl region adaptor) which will get two views loaded into it at runtime. The two views are located within two separate modules.
There is a welcome view/module which simply shows a simple text item, and utilises a Cinch V2 MEF injected ViewModel, and it also makes use of several Cinch V2 Core UI Services, namely the IMessageBoxService
and IViewAwareStatus
.
There is a list view/module which simply shows a list of items, and utilises a Cinch V2 MEF injected ViewModel, and it also makes use of several Cinch V2 Core UI Services, namely the IMessageBoxService
and IViewAwareStatus
and also shows the use of a multithreaded custom service, where there is also a design time service available.
Demo 2: What does it look like?
Well, recall I said there was a simple Shell Window with two Tab
s inside a TabControl
region, where there were two modules, a welcome view and a list view. The demo 2 screenshot is shown below. Granted it will not win any prizes in a beauty contest, but it does show that Prism/Cinch V2 work together with no problems.
And here is screenshot of it in Blend 4, where in the demo 2 application, I have provided a default design time data access service for the list module to use.
Demo 2: How does it all work?
These next sections will walk you through how it all works.
Demo 2: The Shell
The first step I carried out when building this demo was to create the Shell
, which obviously meant getting all the relevant DLL references, but you can see that from the actual attached demo code. The Shell
is a very simple Window
which has the following XAML, and it really is just a region container for the two other module views to be loaded into. Like I say, in a production system, your Shell
would obviously do a lot more and look a lot better than this; this is just a demo.
Here is the entire Shell's XAML:
<Window x:Class="CinchV2AndPrismModulesRegions.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cal="clr-namespace:Microsoft.Practices.Prism.Regions;
assembly=Microsoft.Practices.Prism"
xmlns:ApplicationCommon="clr-namespace:ApplicationCommon;
assembly=ApplicationCommon"
Title="MainWindow" Height="350" Width="525">
<TabControl
cal:RegionManager.RegionName=
"{x:Static ApplicationCommon:Regions.MainRegion}"
Grid.Column="1" Margin="0,0,5,0" />
</Window>
And here is its code-behind; note that it is marked with a MEF ExportAttrtibute
, which allows the MEF CompositionContainer
(we will see that next) to be able to resolve the Shell
type and any MEF Import
s the Shell
may require. It should be noted that in this demo, the Shell
does not need to satisfy any other Import
s, but in real life code, it more than likely would, so it is good practice to Export
the Shell
; it is also the defacto way that Prism works in V4.
namespace CinchV2AndPrismModulesRegions
{
[Export]
public partial class Shell : Window
{
public Shell()
{
InitializeComponent();
}
}
}
Demo 2: Bootstrapper
The next thing you must do when creating a Prism V4 application is to create a bootstrapper which inherits from MefBootstrapper
. This is where the Shell
is created and other Prism related overrides should be done.
For the demo application, the bootstrapper looks like this:
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.Primitives;
using System.Diagnostics;
using System.Windows;
using Microsoft.Practices.Prism.Logging;
using Microsoft.Practices.Prism.MefExtensions;
using MEFedMVVM.ViewModelLocator;
using Cinch;
namespace CinchV2AndPrismModulesRegions
{
public class Bootstrapper : MefBootstrapper, IComposer, IContainerProvider
{
private CompositionContainer _compositionContainer;
protected override void ConfigureAggregateCatalog()
{
this.AggregateCatalog.Catalogs.Add(
new AssemblyCatalog(typeof(Bootstrapper).Assembly));
this.AggregateCatalog.Catalogs.Add(
new DirectoryCatalog("Modules"));
this.AggregateCatalog.Catalogs.Add(
new AssemblyCatalog(typeof(ViewModelBase).Assembly));
this.AggregateCatalog.Catalogs.Add(
new AssemblyCatalog(typeof(ViewModelLocator).Assembly));
}
protected override void InitializeShell()
{
base.InitializeShell();
Application.Current.MainWindow = (Shell)this.Shell;
Application.Current.MainWindow.Show();
}
#region Overrides of Bootstrapper
protected override DependencyObject CreateShell()
{
MEFedMVVM.ViewModelLocator.LocatorBootstrapper.ApplyComposer(this);
return this.Container.GetExportedValue<Shell>();
}
protected override CompositionContainer CreateContainer()
{
var exportProvider = new MEFedMVVMExportProvider(MEFedMVVMCatalog.CreateCatalog(AggregateCatalog));
_compositionContainer = new CompositionContainer(exportProvider);
exportProvider.SourceProvider = _compositionContainer;
return _compositionContainer;
}
#endregion
#region Implementation of IComposer (For MEFedMVVM)
public ComposablePartCatalog InitializeContainer()
{
return this.AggregateCatalog;
}
public IEnumerable<ExportProvider> GetCustomExportProviders()
{
return null;
}
#endregion
#region Implementation of IContainerProvider(For MEFedMVVM)
CompositionContainer IContainerProvider.CreateContainer()
{
return _compositionContainer;
}
#endregion
}
}
There are a few things to note in there to do with getting Cinch V2 to work nicely with Prism; these things of note are:
- That the MEFedMVVM (therefore Cinch V2)
IComposer
interface is implemented such that we can instruct MEFedMVVM to use the same CompositionContainer
and Parts (Export
s/Import
s) as Prism. You can literally follow the example in this demo and that is all you need to do.
- That we add the relevant catalogs to make Cinch V2/MEFedMVVM work in the
ConfigureAggregateCatalog()
override.
- That the following line is run in the
CreateShell
override MEFedMVVM.ViewModelLocator.LocatorBootstrapper.ApplyComposer(this);
.
That is all you need to do to get Cinch V2 (and remember Cinch V2 makes use of MEFedMVVM for ViewModel resolution) /Prism to work together. Simple, isn't it?
Now that we have a bootstrapper, we need to make sure it is called, which is typically done in a Prism application in the App.xaml.cs code, as follows:
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
Bootstrapper b = new Bootstrapper();
b.Run();
}
}
Demo 2: Welcome Module
WelcomeModule
is a very simple module that has a single view/Cinch V2 based ViewModel, and as such, it must reference the MEFedMVVM.WPF DLL and Cinch.WPF DLL, and the Prism DLLs, as shown below.
There is one special thing you must adhere to when referencing the Cinch.WPF and MEFedMVVM.WPF DLLs. They must be set with Copy Local set to false. This is to do with the way the Bootstrapper
's MEF AggregateCatlog
builds up the Import
s/Export
s. If you do not set "Copy Local" to false, then more than one Import/Export
may be found for a particular MEF part contact name, which causes issues for MEFedMVVM.
Simply make sure that you have "Copy Local = False" for the two references, Cinch.WPF and MEFedMVVM.WPF for any custom module you create, and all will be cool.
Now that you have the references sorted, all you need to do is create a module, which is done as follows:
using System.ComponentModel.Composition;
using ApplicationCommon;
using Microsoft.Practices.Prism.MefExtensions.Modularity;
using Microsoft.Practices.Prism.Modularity;
using Microsoft.Practices.Prism.Regions;
namespace Modules.WelcomeModule
{
[ModuleExport(typeof(WelcomeModule))]
public class WelcomeModule : IModule
{
readonly IRegionManager _regionManager;
[ImportingConstructor]
public WelcomeModule(IRegionManager regionManager)
{
_regionManager = regionManager;
}
#region Implementation of IModule
public void Initialize()
{
var view = new WelcomeView();
IRegion region = _regionManager.Regions[Regions.MainRegion];
region.Add(view, "WelcomeView");
region.Activate(view);
}
#endregion
}
}
There is one thing that you must ensure for each module that you include, which is to make sure it is outputting to a special folder that was included in the Bootstrapper
code to scan for any custom modules. This is shown below for the WelcomModule
, but is the case for any custom module you write.
It can be seen that this WelcomeModule
simply shows a new instance of a WelcomeView
in a region that was declared in the Shell
. So let's have a look at the WelcomeView
.
Here it is, it's not that exciting:
<UserControl x:Class="Modules.WelcomeModule.WelcomeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
xmlns:mefed="http:\\www.codeplex.com\MEFedMVVM"
mefed:ViewModelLocator.ViewModel="WelcomeViewModel">
<Grid>
<Label Content="{Binding WelcomeText}"/>
</Grid>
</UserControl>
The important thing of note here is the use of the MEFedMVVM ViewModel DependencyProperty
, which uses MEFedMVVM to locate the WelcomeViewModel
.
The WelcomeViewModel
is a very simple Cinch V2 ViewModel which allows us to make use of the standard Cinch V2 UI Services/ViewModel base classes. The WelComeViewModel
is shown below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MEFedMVVM.ViewModelLocator;
using ApplicationCommon;
using System.ComponentModel.Composition;
using Cinch;
namespace Modules.WelcomeModule.ViewModels
{
[ExportViewModel("WelcomeViewModel")]
public class WelcomeViewModel : ViewModelBase
{
private IMessageBoxService messageBoxService;
private IViewAwareStatus viewAwareStatus;
private bool initialised = false;
[ImportingConstructor]
public WelcomeViewModel(
IMessageBoxService messageBoxService,
IViewAwareStatus viewAwareStatus)
{
WelcomeText = "hello";
this.messageBoxService = messageBoxService;
this.viewAwareStatus = viewAwareStatus;
this.viewAwareStatus.ViewLoaded += ViewAwareStatus_ViewLoaded;
}
void ViewAwareStatus_ViewLoaded()
{
if (!initialised)
{
initialised = true;
messageshow BoxService.ShowInformation(
string.Format("WelcomeViewModel says {0}", WelcomeText));
}
}
public string WelcomeText { get; set; }
}
}
Like I say, the WelcomeModule
is not that complicated. And as such, all its WelcomeViewModel
does is use one of the standard Cinch V2 services, in this case the IMessageBoxService/IViewAwareStatus
to show a message when the view loads (which it knows using the IViewAwareStatus
to tell the ViewModel the View has loaded).
Demo 2: List Module
The second module in this demo uses a runtime multithreaded service which simulates fetching a list of data from somewhere that would take a long time (OK, in the demo, it does not take a long time, but it shows you how to do it), and it also shows how to use a design time service to provide design time data.
The way you must set the MEFedMVVM.WPF and Cinch.WPF references to have "Copy Local=False" applies here as well, as does changing the build output path to be the "Modules" folder of the overall solution.
The module itself is trivial and looks like this:
using System.ComponentModel.Composition;
using ApplicationCommon;
using Microsoft.Practices.Prism.MefExtensions.Modularity;
using Microsoft.Practices.Prism.Modularity;
using Microsoft.Practices.Prism.Regions;
namespace Modules.DisciplesListModule
{
[ModuleExport(typeof(DisciplesListModule))]
public class DisciplesListModule : IModule
{
readonly IRegionManager _regionManager;
[ImportingConstructor]
public DisciplesListModule(IRegionManager regionManager)
{
_regionManager = regionManager;
}
#region Implementation of IModule
public void Initialize()
{
var view = new DisciplesListView();
IRegion region = _regionManager.Regions[Regions.MainRegion];
region.Add(view, "DisciplesListView");
region.Activate(view);
}
#endregion
}
}
Whilst the DisciplesListView
is only slightly more complicated than WelcomeView
, in that this time we are displaying a list of items, and there is a simple DataTemplate
to style the data items in the list. Here is the full XAML for the DisciplesListView
; again, notice that DisciplesListViewModel
is wired up using MEFededMVVM.
<UserControl x:Class="Modules.DisciplesListModule.DisciplesListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:models="clr-namespace:ApplicationCommon.Models;
assembly=ApplicationCommon"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300"
xmlns:mefed="http:\\www.codeplex.com\MEFedMVVM"
mefed:ViewModelLocator.ViewModel="DisciplesListViewModel">
<UserControl.Resources>
<DataTemplate x:Key="discTemplate" DataType="{x:Type models:DiscipleInfo}">
<StackPanel Orientation="Horizontal" Background="WhiteSmoke">
<Label Content="FirstName: "/>
<Label Content="{Binding FirstName}"/>
<Label Margin="10,0,0,0" Content="LastName: "/>
<Label Content="{Binding LastName}"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid Background="Cyan">
<ItemsControl Margin="10"
Background="CornflowerBlue"
BorderBrush="Black" BorderThickness="2"
ItemsSource="{Binding DisciplesResults}"
ItemTemplate="{StaticResource discTemplate}"/>
</Grid>
</UserControl>
And DisciplesListViewModel
looks like this, where it can be seen that it not only uses some of the core Cinch V2 services, but also makes use of a special service (of type IDisciplesListProvider
) specifically for the DisciplesListViewModel
.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MEFedMVVM.ViewModelLocator;
using ApplicationCommon;
using System.ComponentModel.Composition;
using Cinch;
using ApplicationCommon.Models;
using System.ComponentModel;
using UIServiceContracts;
namespace Modules.DisciplesListModule.ViewModels
{
[ExportViewModel("DisciplesListViewModel")]
public class DisciplesListViewModel : ViewModelBase
{
private IMessageBoxService messageBoxService;
private IViewAwareStatus viewAwareStatus;
private IDisciplesListProvider disciplesListProvider;
private IEnumerable<DiscipleInfo> disciplesResults = null;
[ImportingConstructor]
public DisciplesListViewModel(
IMessageBoxService messageBoxService,
IViewAwareStatus viewAwareStatus,
IDisciplesListProvider disciplesListProvider)
{
this.messageBoxService = messageBoxService;
this.disciplesListProvider = disciplesListProvider;
this.viewAwareStatus = viewAwareStatus;
this.viewAwareStatus.ViewLoaded += new Action(viewAwareStatus_ViewLoaded);
}
private void viewAwareStatus_ViewLoaded()
{
if (disciplesResults == null)
disciplesListProvider.GetAll(ShowResults, ShowException);
}
private void ShowResults(IEnumerable<DiscipleInfo> results)
{
disciplesResults = results;
NotifyPropertyChanged(disciplesResultsArgs);
messageBoxService.ShowInformation("got results");
}
private void ShowException(Exception ex)
{
messageBoxService.ShowError(
string.Format("there was a problem fetching the " +
"Disciples list\r\nThis is the exception{0}",
ex.ToString()));
}
static PropertyChangedEventArgs disciplesResultsArgs =
ObservableHelper.CreateArgs<DisciplesListViewModel>(x => x.DisciplesResults);
public IEnumerable<DiscipleInfo> DisciplesResults
{
get { return disciplesResults; }
}
}
}
Where the service contract looks like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ApplicationCommon.Models;
namespace UIServiceContracts
{
public class SearchResult<T>
{
readonly T package;
readonly Exception error;
public T Package { get { return package; } }
public Exception Error { get { return error; } }
public SearchResult(T package, Exception error)
{
this.package = package;
this.error = error;
}
}
public interface IDisciplesListProvider
{
void GetAll(
Action<IEnumerable<DiscipleInfo>> resultCallback,
Action<Exception> errorCallback);
}
}
Where the runtime version of this service looks like this, where we use the Task Parallel Library to conduct this fetching using a Task
(as I say, this takes no time at all, but it does show you how you might call something using a Task
that would take a long time):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
using MEFedMVVM.ViewModelLocator;
using ApplicationCommon.Models;
using UIServiceContracts;
namespace DisciplesListModule.Services.Runtime
{
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.Runtime, typeof(IDisciplesListProvider))]
public class DisciplesListProvider : IDisciplesListProvider
{
void IDisciplesListProvider.GetAll(
Action<IEnumerable<DiscipleInfo>> resultCallback,
Action<Exception> errorCallback)
{
Task<SearchResult<IEnumerable<DiscipleInfo>>> task =
Task.Factory.StartNew(() =>
{
try
{
List<DiscipleInfo> items = new List<DiscipleInfo>();
items.Add(new DiscipleInfo("marlon", "grech"));
items.Add(new DiscipleInfo("sacha", "barber"));
items.Add(new DiscipleInfo("josh", "smith"));
items.Add(new DiscipleInfo("karl", "shifflett"));
items.Add(new DiscipleInfo("daniel", "vaughan"));
items.Add(new DiscipleInfo("jeremiah", "morrill"));
return new SearchResult<IEnumerable<DiscipleInfo>>(items, null);
}
catch (Exception ex)
{
return new SearchResult<IEnumerable<DiscipleInfo>>(null, ex);
}
});
task.ContinueWith(r =>
{
if (r.Result.Error != null)
{
errorCallback(r.Result.Error);
}
else
{
resultCallback(r.Result.Package);
}
}, CancellationToken.None, TaskContinuationOptions.None,
TaskScheduler.FromCurrentSynchronizationContext());
}
}
}
And here is a design time service, which is actually in an entirely different project, that does not even have to be referenced by any other project. Basically, as long as the project that holds the design time service references MEFedMVVM.WPF, it should be resolved at design time and show up in Blend:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
using MEFedMVVM.ViewModelLocator;
using UIServiceContracts;
using ApplicationCommon.Models;
namespace DesignTimeServices
{
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.DesignTime, typeof(IDisciplesListProvider))]
public class DisciplesListProvider : IDisciplesListProvider
{
void IDisciplesListProvider.GetAll(
Action<IEnumerable<DiscipleInfo>> resultCallback,
Action<Exception> errorCallback)
{
List<DiscipleInfo> items = new List<DiscipleInfo>();
items.Add(new DiscipleInfo("lorem", "ipsum1"));
items.Add(new DiscipleInfo("slovvy", "toads"));
items.Add(new DiscipleInfo("jabber", "wocky"));
resultCallback(items);
}
}
}
And to prove that this all works OK, let's go back to our original screenshot of this Cinch V2 with Prism solution in Blend 4.
See, all good? I prefer the way that MEFedMVVM deals with design time data, as opposed to how the Blend d: design time tags work, as they assume a default constructor is available for your ViewModel. I have to say, in my production code, I rarely find that I have ViewModels which have a default constructor, they normally always rely on some context or services. Also, using the d: Blend design tags means you mock the whole ViewModel. It should be the services that provide the data, so they should be mocked, not the entire ViewModel. Also, Blend requires all get/set properties to do this mocking, which I think encourages bad design.
But hey, that is just my 2 cents.
That's all folks
I hope that I have shown you in this article that you really can use Cinch V2 with Prism with absolute ease and get the best out of both frameworks. You know, if you like Prism's regions/modules, you can use them whilst still taking advantage of Cinch V2s services, ViewModel base classes, and extra utilities. They just work together seamlessly, largely thanks to MEF.
As always, if you like what you have seen, please spare some time to add a comment and a vote, they are always welcome.