Click here to Skip to main content
15,867,330 members
Articles / Desktop Programming / WPF

Showcasing Cinch MVVM framework / Prism 4 interoperability

Rate me:
Please Sign up or sign in to vote.
4.98/5 (38 votes)
14 Mar 2012CPOL22 min read 178.8K   3.6K   70   74
Shows you how to use CinchV2 with Prism 4 with ease.

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.

Image 1

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:

Image 2

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:

Image 3

To load a new instance of this view, and load some more random Google images, you can use the following button:

Image 4

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:

XML
<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 Imports the Shell may require. It should be noted that in this demo, the Shell does not need to satisfy any other Imports, 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.

C#
[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:

C#
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;

            //Commands
            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:

C#
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) =>
            {
                //Add
                if (e.Action == 
                    System.Collections.Specialized.NotifyCollectionChangedAction.Add)
                    foreach (FrameworkElement element in e.NewItems)
                        regionTarget.Content = element;

                //Removal
                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:

XML
<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:

C#
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.

C#
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:

Image 5

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:

C#
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()
        {
            // The Prism call to create a container
            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 the same catalog as the PRISM one
            return this.AggregateCatalog;
        }

        public IEnumerable<ExportProvider> GetCustomExportProviders()
        {
            //In case you want some custom export providers
            return null;
        }

        #endregion


        #region Implementation of IContainerProvider(For MEFedMVVM)
        CompositionContainer IContainerProvider.CreateContainer()
        {
            // The MEFedMVVM call to create a container
            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:

  1. That the MEFedMVVM (therefore Cinch V2) IComposer interface is implemented such that we can instruct MEFedMVVM to use the same CompositionContainer and Parts (Exports/Imports) as Prism. You can literally follow the example in this demo; that is all you need to do.
  2. That we add the relevant catalogs to make Cinch V2/MEFedMVVM work in the ConfigureAggregateCatalog() override.
  3. That the following line is run in the CreateShell override MEFedMVVM.ViewModelLocator.LocatorBootstrapper.ApplyComposer(this);.
  4. 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).
  5. 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:

C#
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:

Image 6

In fact, the XAML for the WelcomeView is dead simple too; here it is:

XML
<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:

C#
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 TabItems 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:

XML
<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.

C#
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:

C#
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;

            //add transition lookups
            transitionsLookup.Add(TransitionType.FadeAndBlur, 
                                  new FadeAndBlurTransition());
            ......
            ......
            ......
            ......
            ......
            transitionsLookup.Add(TransitionType.VerticalWipeTransition, 
                                  new VerticalWipeTransition());

            //Commands
            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; }

        /// <summary>
        /// TransitionTypes
        /// </summary>
        public Array TransitionTypes
        {
            get
            {
                return Enum.GetValues(typeof(TransitionType));
            }
        }

        /// <summary>
        /// AsyncState
        /// </summary>
        static PropertyChangedEventArgs asyncStateArgs =
            ObservableHelper.CreateArgs<GoogleImageSearchViewModel>(x => x.AsyncState);

        public AsyncType AsyncState
        {
            get { return asyncState; }
            private set
            {
                asyncState = value;
                NotifyPropertyChanged(asyncStateArgs);
            }
        }

        /// <summary>
        /// WaitText
        /// </summary>
        static PropertyChangedEventArgs waitTextArgs =
            ObservableHelper.CreateArgs<GoogleImageSearchViewModel>(x => x.WaitText);
       
        public string WaitText
        {
            get { return waitText; }
            private set
            {
                waitText = value;
                NotifyPropertyChanged(waitTextArgs);
            }
        }

        /// <summary>
        /// ErrorMessage
        /// </summary>
        static PropertyChangedEventArgs errorMessageArgs =
            ObservableHelper.CreateArgs<GoogleImageSearchViewModel>(
            x => x.ErrorMessage);

        public string ErrorMessage
        {
            get { return errorMessage; }
            private set
            {
                errorMessage = value;
                NotifyPropertyChanged(errorMessageArgs);
            }
        }

        /// <summary>
        /// SelectedTransitonType
        /// </summary>
        static PropertyChangedEventArgs selectedTransitonTypeArgs =
            ObservableHelper.CreateArgs<GoogleImageSearchViewModel>(
            x => x.SelectedTransitonType);

        public TransitionType SelectedTransitonType
        {
            get { return selectedTransitonType; }
            set
            {
                selectedTransitonType = value;
                NotifyPropertyChanged(selectedTransitonTypeArgs);
                TransitionToUse = transitionsLookup[value];
            }
        }

        /// <summary>
        /// TransitionToUse
        /// </summary>
        static PropertyChangedEventArgs transitionToUseArgs =
            ObservableHelper.CreateArgs<GoogleImageSearchViewModel>(
            x => x.TransitionToUse);

        public Transition TransitionToUse
        {
            get { return transitionToUse; }
            private set
            {
                transitionToUse = value;
                NotifyPropertyChanged(transitionToUseArgs);
            }
        }

        /// <summary>
        /// GoogleImageResults
        /// </summary>
        static PropertyChangedEventArgs googleImageResultsArgs =
            ObservableHelper.CreateArgs<GoogleImageSearchViewModel>(
            x => x.GoogleImageResults);

        public IEnumerable<ImageInfo> GoogleImageResults
        {
            get { return googleImageResults; }
        }

        /// <summary>
        /// ViewName
        /// </summary>
        public string ViewName
        {
            get { return "GoogleImageSearch"; }
        }

        /// <summary>
        /// ShowContextMenu
        /// </summary>
        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 user did not want to cancel, keep workspace open
            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)
        {
            //NOTE : I am breaking one of my own cardinal MVVM
            //rules here, and using UI element in my ViewModel
            //but this is just for the sake of the transitions
            //in the demo, it make use of the internet downloading 
            //images, load using the already loaded images.
            //I could have used the CommandParameter, and downloaded
            //image again, but decided against it for sake of demo.
            //If I was doing this for production code
            //I would have threaded eacj google searched image,
            //downloaded it locally and then used that downloaded file
            //location as ImageSource. But that would have distracted
            //from the demo too much. So I cheated

            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:

C#
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):

C#
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.

C#
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.

Image 7

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 Tabs 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.

Image 8

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.

Image 9

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:

XML
<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 Imports the Shell may require. It should be noted that in this demo, the Shell does not need to satisfy any other Imports, 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.

C#
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:

C#
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"));
                // add all assemblies in the modules
            this.AggregateCatalog.Catalogs.Add(
                new AssemblyCatalog(typeof(ViewModelBase).Assembly));
        
            //add a reference to the <a href="mefedmvvm.codeplex.com">MEFedMVVM services
            this.AggregateCatalog.Catalogs.Add(
                new AssemblyCatalog(typeof(ViewModelLocator).Assembly));
                // reference the xml data access
        }

        protected override void InitializeShell()
        {
            base.InitializeShell();

            Application.Current.MainWindow = (Shell)this.Shell;
            Application.Current.MainWindow.Show();
        }

        #region Overrides of Bootstrapper

        protected override DependencyObject CreateShell()
        {
            //init <a href="mefedmvvm.codeplex.com">MEFedMVVM composed
            MEFedMVVM.ViewModelLocator.LocatorBootstrapper.ApplyComposer(this);

            return this.Container.GetExportedValue<Shell>();
        }


        protected override CompositionContainer CreateContainer()
        {
            // The Prism call to create a container
            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 the same catalog as the PRISM one
            return this.AggregateCatalog;
        }

        public IEnumerable<ExportProvider> GetCustomExportProviders()
        {
            //In case you want some custom export providers
            return null;
        }

        #endregion

        #region Implementation of IContainerProvider(For MEFedMVVM)

        CompositionContainer IContainerProvider.CreateContainer()
        {
            // The MEFedMVVM call to create a container
            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:

  1. That the MEFedMVVM (therefore Cinch V2) IComposer interface is implemented such that we can instruct MEFedMVVM to use the same CompositionContainer and Parts (Exports/Imports) as Prism. You can literally follow the example in this demo and that is all you need to do.
  2. That we add the relevant catalogs to make Cinch V2/MEFedMVVM work in the ConfigureAggregateCatalog() override.
  3. 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:

C#
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.

Image 10

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 Imports/Exports. 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.

Image 11

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:

C#
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();

            // Add it to the region
            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.

Image 12

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:

XML
<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:

C#
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:

C#
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();

            // Add it to the region
            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.

XML
<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.

C#
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()));
        }

        /// <summary>
        /// DisciplesResults
        /// </summary>
        static PropertyChangedEventArgs disciplesResultsArgs =
            ObservableHelper.CreateArgs<DisciplesListViewModel>(x => x.DisciplesResults);


        public IEnumerable<DiscipleInfo> DisciplesResults
        {
            get { return disciplesResults; }
        }
    }
}

Where the service contract looks like this:

C#
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):

C#
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:

C#
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.

Image 13

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)

- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence

Both of these at Sussex University UK.

Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2016
  • Codeproject MVP 2016
  • Microsoft C# MVP 2015
  • Codeproject MVP 2015
  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
GeneralRe: Top Stuff again Pin
Sacha Barber12-Jan-11 0:42
Sacha Barber12-Jan-11 0:42 
GeneralRe: Top Stuff again Pin
Member 456543312-Jan-11 0:56
Member 456543312-Jan-11 0:56 
GeneralRe: Top Stuff again Pin
Sacha Barber12-Jan-11 1:50
Sacha Barber12-Jan-11 1:50 
GeneralRe: Top Stuff again Pin
Member 456543312-Jan-11 2:37
Member 456543312-Jan-11 2:37 
GeneralRe: Top Stuff again Pin
Sacha Barber12-Jan-11 3:34
Sacha Barber12-Jan-11 3:34 
GeneralRe: Top Stuff again Pin
Member 456543312-Jan-11 5:27
Member 456543312-Jan-11 5:27 
GeneralRe: Top Stuff again Pin
Sacha Barber12-Jan-11 5:39
Sacha Barber12-Jan-11 5:39 
GeneralRe: Top Stuff again Pin
Sacha Barber12-Jan-11 2:31
Sacha Barber12-Jan-11 2:31 
GeneralRe: Top Stuff again Pin
ACanadian21-Jan-11 12:16
ACanadian21-Jan-11 12:16 
GeneralRe: Top Stuff again Pin
Sacha Barber21-Jan-11 22:20
Sacha Barber21-Jan-11 22:20 
GeneralRe: Top Stuff again Pin
ACanadian22-Jan-11 8:13
ACanadian22-Jan-11 8:13 
GeneralRe: Top Stuff again Pin
ACanadian22-Jan-11 10:02
ACanadian22-Jan-11 10:02 
GeneralRe: Top Stuff again Pin
Sacha Barber22-Jan-11 11:50
Sacha Barber22-Jan-11 11:50 
GeneralRe: Top Stuff again Pin
Sacha Barber22-Jan-11 11:58
Sacha Barber22-Jan-11 11:58 
GeneralRe: Top Stuff again Pin
ACanadian23-Jan-11 17:22
ACanadian23-Jan-11 17:22 
GeneralRe: Top Stuff again Pin
Sacha Barber21-Jan-11 22:22
Sacha Barber21-Jan-11 22:22 
GeneralRe: Top Stuff again Pin
ACanadian22-Jan-11 8:14
ACanadian22-Jan-11 8:14 
GeneralRe: Top Stuff again Pin
Sacha Barber22-Jan-11 11:52
Sacha Barber22-Jan-11 11:52 
GeneralRe: Top Stuff again Pin
ACanadian23-Jan-11 17:21
ACanadian23-Jan-11 17:21 
GeneralRe: Top Stuff again Pin
Sacha Barber23-Jan-11 19:07
Sacha Barber23-Jan-11 19:07 
GeneralBrilliant... Pin
Gary Noble11-Jan-11 21:17
Gary Noble11-Jan-11 21:17 
GeneralRe: Brilliant... Pin
Sacha Barber11-Jan-11 21:31
Sacha Barber11-Jan-11 21:31 
GeneralMy vote of 5 Pin
linuxjr11-Jan-11 14:10
professionallinuxjr11-Jan-11 14:10 
GeneralRe: My vote of 5 Pin
Sacha Barber11-Jan-11 19:20
Sacha Barber11-Jan-11 19:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.