Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

CinchV2: Version 2 of my Cinch MVVM framework: Part 5 of n

0.00/5 (No votes)
1 Jan 2011 2  
If Jack Daniels made MVVM frameworks.

Table of Contents

Introduction

The last time we talked about what's new and what's stayed the same in Cinch V2. In this article, we will be going through the Cinch V2 WPF demo app, provided with the Cinch V2 codebase over at Cinch's CodePlex site.

As promised, within each article, I shall be showing the Cinch V2 compatibility matrix.

The compatibility matrix shows a list of classes along with their general work area, and whether they are compatible with WPF or SL or both.

Work Area Class Name WPF Silverlight (4 or above) Both
Business objects EditableValidatingObject.cs     Yes
Business objects ValidatingObject.cs     Yes
Business objects DataWrapper.cs     Yes
Commands EventToCommandArgs.cs     Yes
Commands SimpleCommand.cs     Yes
Commands WeakEventHandlerManager.cs     Yes
Events CloseRequestEventArgs.cs     Yes
Events UICompletedEventArgs.cs     Yes
WeakEvents WeakEvent.cs     Yes
WeakEvents WeakEventHelper.cs     Yes
WeakEvents WeakEventProxy.cs     Yes
Extension Methods DispatcherExtensions.cs Yes    
Extension Methods GenericListExtensions.cs   Yes  
Interactivity Actions CommandDrivenGoToStateAction.cs     Yes
Interactivity Behaviours FocusBehaviourBase.cs Yes    
Interactivity Behaviours NumericTextBoxBehaviour.cs Yes    
Interactivity Behaviours SelectorDoubleClickCommandBehavior.cs Yes    
Interactivity Behaviours TextBoxFocusBehavior.cs Yes    
Interactivity Triggers CompletedAwareCommandTrigger.cs     Yes
Interactivity Triggers CompletedAwareGotoStateCommandTrigger.cs     Yes
Interactivity Triggers EventToCommandTrigger.cs     Yes
Messager Mediator MediatorMessageSinkAttribute.cs     Yes
Messager Mediator MediatorSingleton.cs     Yes
Services Implementation ChildWindowService.cs   Yes  
Services Implementation SLMessageBoxService.cs   Yes  
Services Implementation ViewAwareStatus.cs     Yes
Services Implementation ViewAwareStatusWindow.cs Yes    
Services Implementation VSMService.cs     Yes
Services Implementation WPFMessageBoxService.cs Yes    
Services Implementation WPFOpenFileService.cs Yes    
Services Implementation WPFSaveFileService.cs Yes    
Services Implementation WPFUIVisualizerService.cs Yes     
Services Interfaces IChildWindowService.cs   Yes  
Services Interfaces IMessageBoxService.cs   Yes  
Services Interfaces IViewAwareStatus.cs     Yes
Services Interfaces IViewAwareStatusWindow.cs Yes    
Services Interfaces IVSM.cs     Yes
Services Interfaces IMessageBoxService.cs Yes    
Services Interfaces IOpenFileService.cs Yes    
Services Interfaces ISaveFileService.cs Yes    
Services Interfaces IUIVisualizerService.cs Yes    
Services Test Implementations TestChildWindowService.cs   Yes  
Services Test Implementations TestMessageBoxService.cs   Yes  
Services Test Implementations TestViewAwareStatus.cs     Yes
Services Test Implementations TestViewAwareStatusWindow.cs Yes    
Services Test Implementations TestVSMService.cs     Yes
Services Test Implementations TestMessageBoxService.cs Yes    
Services Test Implementations TestOpenFileService.cs Yes    
Services Test Implementations TestSaveFileService.cs Yes    
Services Test Implementations TestUIVisualizerService.cs Yes    
Threading AddRangeObservableCollection.cs (this is a specific SL implementation)   Yes  
Threading AddRangeObservableCollection.cs (this is a specific WPF implementation) Yes    
Threading BackgroundTaskManager.cs     Yes
Threading ISynchronizationContext.cs     Yes
Threading UISynchronizationContext.cs     Yes
Threading ApplicationHelper.cs Yes    
Threading DispatcherNotifiedObservableCollection.cs Yes    
Menus CinchMenuItem.cs     Yes
Utilities ArgumentValidator.cs     Yes
Utilities IWeakEventListener.cs (this is a System class missing from SL, so I created it)   Yes  
Utilities ObservableHelper.cs     Yes
Utilities PropertyChangedEventManager.cs (this is a System class missing from SL, so I created it)   Yes  
Utilities PropertyObserver.cs     Yes
Utilities BindingEvaluator.cs Yes    
Utilities ObservableDictionary.cs Yes    
Utilities TreeHelper.cs Yes    
Validation RegexRule.cs     Yes
Validation Rule.cs     Yes
Validation SimpleRule.cs     Yes
ViewModels EditableValidatingViewModelBase.cs     Yes
ViewModels IViewStatusAwareInjectionAware.cs     Yes
ViewModels ValidatingViewModelBase.cs     Yes
ViewModels ViewMode.cs     Yes
ViewModels ViewModelBase.cs     Yes
ViewModels ViewModelBaseSLSpecific.cs   Yes  
ViewModels ViewModelBaseWPFSpecific.cs Yes    
Workspaces ChildWindowResolver.cs   Yes  
Workspaces CinchBootStrapper.cs (SL Version)   Yes  
Workspaces CinchBootStrapper.cs (WPF version) Yes    
Workspaces PopupNameToViewLookupKeyMetadataAttribute.cs     Yes
Workspaces IWorkspaceAware.cs Yes    
Workspaces MockView.cs Yes    
Workspaces NavProps.cs Yes    
Workspaces PopupResolver.cs Yes    
Workspaces ViewnameToViewLookupKeyMetadataAttribute.cs Yes    
Workspaces ViewResolver.cs Yes    
Workspaces WorkspaceData.cs Yes    

Now that I have shown you what classes will work with WPF/SL, let's get on with the rest of this article, shall we? But first, here are the links to the old Cinch V1 articles.

In case you missed Cinch V1, and have an interest in MVVM, I would strongly recommend that you read all the Cinch V1 articles first, as it will give you a much deeper understanding of the content that will be presented in these Cinch V2 articles.

CinchV1 Article Links

Some of you may never have seen the old Cinch V1 articles, so I will also include a list of these here, and where the Cinch V2 still uses the same functionality as Cinch V1, I will be redirecting people to these articles.

CinchV2 Article Links

OK, so that is what the article roadmap looks like. I guess it is now time to dive into the guts of this article, so let's go:

What Does It Do

For Cinch V1, I created a LOB (line of business) application, but at work, I am working on a massive LOB app, and truth be known, I was just tired of creating yet another LOB app, and the stuff that is common between Cinch V1 and V2 can be seen quite clearly in the old Cinch V1 demos. The things that have really changed are the UI Services, and the attached properties have now become Blend behaviours.

So this time I decided I would do something a little bit more creative, much to the dismay of several readers. However, some readers may be pleased to know that I have also been contacted by two other CodeProject users who use Cinch, and one will be writing a Cinch V2 LOB article, and another will be writing a VB.NET Cinch V2 application, both of which I will be linking to from the Cinch CodePlex site when these CodeProject users let me know that they are done writing the articles.

Anyway, that is neither here nor there. As I say, what I decided to do was write something a bit different. So without further ado, what does the WPF demo app do?

Well, I think this can be summarized by the following bullet points:

  • Create a tabbed main interface that allows n-many closeable tabs to be shown, where each tab may be either an "About" tab or an "Image Viewer" tab.
  • Create an Image Viewer view that displays images from a particular folder (specified in App.Config) and allows users to rate each image and save and load the rating that each image received.
  • Create an About view that allows the user to open a popup window to see various web sites.

Now, that may not look like much, but believe me, that is enough to showcase most of Cinch's functionality.

What Does It Look Like

Now that I have talked about what it does, let's have a look at what it looks like, shall we?

When you start the app, it should look something like this (remember to change the App.Config to point to somewhere where you have some images).

What you can see from the figure above is that it is a single window app. The main window is called MainWindow, which has a TabControl which is hosting a number of Views. This TabControl is populated via a ObservableCollection<WorkSpaceData> from the MainWindowViewModel.

The first view you can see below is called ImageLoaderView and simply shows a number of images from your PC. The path used is configured in App.Config.

From the ImageLoaderView, it is possible to use the "Add Rating" button to launch the AddImageRatingPopup. Obviously, the showing of the popup is actually done in a ViewModel called ImageLoaderViewModel.

The next view that is shown in the MainWindow is called AboutView which makes use of an AboutViewModel.

From the AboutView, it is also possible to launch the AboutViewLinkRequestedPopup. This showing of the popup is done in the AboutViewModel.

Overall Structure

The following diagram illustrates the overall structure of the Views/ViewModels and popup windows for the WPF demo app. There are a number of helper classes and services, but I will discuss those as we get to them. For now, just take note of the overall structure of the WPF demo app as shown below:

How Does It Work

The next three sections shall attempt to outline all the functions that the Views/ViewModels and popup windows perform in the WPF demo app.

PopUps

In this section, we will talk about how you can show popups from your ViewModels.

Ensuring That a Popup is Available to Show

Some of you may be familiar with how this all works from earlier Cinch articles, or even because you have used Cinch V1, however some of you may not know, so for those of you new to this, the basic idea is as follows:

There is a service that deals with showing popups called IUIVisualizerService, that holds a Dictionary<string, Type> such that a consumer of this IUIVisualizerService service can simply request a popup by name (string) from the internal Dictionary<string, Type>, and then the IUIVisualizerService will locate that entry in the Dictionary<string, Type> and create a new instance of that Type and show it.

For clarity, here is the full IUIVisualizerService service implementation for WPF:

using System;
using System.Collections.Generic;
using System.Windows;
using System.ComponentModel.Composition;
using MEFedMVVM.ViewModelLocator;

namespace Cinch
{
    /// <summary>
    /// This class implements the IUIVisualizerService for WPF purposes.
    /// If you have attributed up your views
    /// using the ViewnameToViewLookupKeyMetadataAttribute
    /// Registration of Views with the IUIVisualizerService service is automatic.
    /// However you can still register views manually, to do this
    /// simply put some lines like this in you App.Xaml.cs
    /// ViewModelRepository.Instance.Resolver.Container.
    ///   GetExport<IUIVisualizerService>().Value.Register(
    ///   "MainWindow", typeof(MainWindow));
    /// </summary>
    [PartCreationPolicy(CreationPolicy.Shared)]
    [ExportService(ServiceType.Both, typeof(IUIVisualizerService))]
    public class WPFUIVisualizerService : IUIVisualizerService
    {
        #region Data
        private readonly Dictionary<string, Type> _registeredWindows;
        #endregion

        #region Ctor
        public WPFUIVisualizerService()
        {
            _registeredWindows = new Dictionary<string, Type>();
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Registers a collection of entries
        /// </summary>
        /// <param name="startupData"></param>
        public void Register(Dictionary<string, Type> startupData)
        {
            foreach (var entry in startupData)
                Register(entry.Key, entry.Value);
        }

        /// <summary>
        /// Registers a type through a key.
        /// </summary>
        /// <param name="key">Key for the UI dialog</param>
        /// <param name="winType">Type which implements dialog</param>
        public void Register(string key, Type winType)
        {
            if (string.IsNullOrEmpty(key))
                throw new ArgumentNullException("key");
            if (winType == null)
                throw new ArgumentNullException("winType");
            if (!typeof(Window).IsAssignableFrom(winType))
                throw new ArgumentException("winType must be of type Window");

            lock (_registeredWindows)
            {
                _registeredWindows.Add(key, winType);
            }
        }

        /// <summary>
        /// This unregisters a type and removes it from the mapping
        /// </summary>
        /// <param name="key">Key to remove</param>
        /// <returns>True/False success</returns>
        public bool Unregister(string key)
        {
            if (string.IsNullOrEmpty(key))
                throw new ArgumentNullException("key");

            lock (_registeredWindows)
            {
                return _registeredWindows.Remove(key);
            }
        }

        /// <summary>
        /// This method displays a modaless dialog associated with the given key.
        /// </summary>
        /// <param name="key">Key previously
        ///     registered with the UI controller.</param>
        /// <param name="state">Object state
        ///     to associate with the dialog</param>
        /// <param name="setOwner">Set the owner of the window</param>
        /// <param name="completedProc">Callback used
        ///     when UI closes (may be null)</param>
        /// <returns>True/False if UI is displayed</returns>
        public bool Show(string key, object state, bool setOwner,
            EventHandler<UICompletedEventArgs> completedProc)
        {
            Window win = CreateWindow(key, state, setOwner, completedProc, false);
            if (win != null)
            {
                win.Show();
                return true;
            }
            return false;
        }

        /// <summary>
        /// This method displays a modal dialog associated with the given key.
        /// </summary>
        /// <param name="key">Key previously
        ///    registered with the UI controller.</param>
        /// <param name="state">Object state
        ///    to associate with the dialog</param>
        /// <returns>True/False if UI is displayed.</returns>
        public bool? ShowDialog(string key, object state)
        {
            Window win = CreateWindow(key, state, true, null, true);
            if (win != null)
                return win.ShowDialog();

            return false;
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// This creates the WPF window from a key.
        /// </summary>
        /// <param name="key">Key</param>
        /// <param name="dataContext">DataContext (state) object</param>
        /// <param name="setOwner">True/False to set ownership to MainWindow</param>
        /// <param name="completedProc">Callback</param>
        /// <param name="isModal">True if this is a ShowDialog request</param>
        /// <returns>Success code</returns>
        private Window CreateWindow(string key, object dataContext, bool setOwner,
            EventHandler<UICompletedEventArgs> completedProc, bool isModal)
        {
            if (string.IsNullOrEmpty(key))
                throw new ArgumentNullException("key");

            Type winType;
            lock (_registeredWindows)
            {
                if (!_registeredWindows.TryGetValue(key, out winType))
                    return null;
            }

            var win = (Window)Activator.CreateInstance(winType);

            if (dataContext is IViewStatusAwareInjectionAware)
            {
                IViewAwareStatus viewAwareStatus = 
                  ViewModelRepository.Instance.Resolver.Container.
                  GetExport<IViewAwareStatus>().Value;
                viewAwareStatus.InjectContext((FrameworkElement)win);
                ((IViewStatusAwareInjectionAware)
                  dataContext).InitialiseViewAwareService(viewAwareStatus);
            }

            win.DataContext = dataContext;

            if (setOwner)
                win.Owner = Application.Current.MainWindow;

            if (dataContext != null)
            {
                var bvm = dataContext as ViewModelBase;
                if (bvm != null)
                {
                    if (isModal)
                    {
                        bvm.CloseRequest += 
                          ((EventHandler<CloseRequestEventArgs>)((s, e) =>
                        {
                            try
                            {
                                win.DialogResult = e.Result;
                            }
                            catch (InvalidOperationException)
                            {
                                win.Close();
                            }
                        })).MakeWeak(eh => bvm.CloseRequest -= eh);
                    }
                    else
                    {
                        bvm.CloseRequest += 
                          ((EventHandler<CloseRequestEventArgs>)((s, e) => 
                            win.Close())).MakeWeak(eh => bvm.CloseRequest -= eh); 
                    }
                    bvm.ActivateRequest += 
                      ((EventHandler<EventArgs>)((s, e) => win.Activate())).MakeWeak(
                        eh => bvm.ActivateRequest -= eh); 
                }
            }

            win.Closed += (s, e) =>
            {
                if (completedProc != null)
                {
                    completedProc(this, new UICompletedEventArgs()
                    {
                        State = dataContext,
                        Result = (isModal) ? win.DialogResult : null
                    });
                }
            };

            return win;
        }
        #endregion
    }
}

For further reading, here is a link: CinchV2_2.aspx#CoreServices and read the WPFUIVisualizerService section.

You might be wondering how the IUIVisualizerService Dictionary<string, Type> gets populated in time to make sure that when a popup is requested, it is present in the Dictionary<string, Type>. Well, this can happen in two different ways.

Manually Adding Items to the Dictionary

You can add popup items manually to the IUIVisualizerService Dictionary<string, Type> at a suitable time, such as on app construction or possibly even startup. So you might have something like:

public partial class App : Application
{
    public App()
    {
        ViewModelRepository.Instance.Resolver.Container.
            GetExport<IUIVisualizerService>().Value.Register(
                "AddImageRatingPopup", 
                typeof(AddImageRatingPopup));

        InitializeComponent();
    }
}

That line will ensure that IUIVisualizerService Dictionary<string, Type> is populated with the correct KeyValuePair.

Automatically Finding Types That are Popups

Manually adding things is all well and good, but Cinch V2 does provide a better way, by the use of attributes, and a bootstrapper which is run at startup. So if we have a popup window which we know we are going to use with the IUIVisualizerService, all we have to do is attribute it up as follows in its code-behind:

[PopupNameToViewLookupKeyMetadata("AddImageRatingPopup",typeof(AddImageRatingPopup))]
public partial class AddImageRatingPopup : Window
{
}

So we now have a popup window which is attributed up, but that is only half the story, we need to make sure something examines these PopupNameToViewLookupKeyMetadata attributes. That is what the CinchBootStrapper's job is. Basically, the CinchBootStrapper takes a IEnumerable<Assembly> to examine for Types in the passed in IEnumerable<Assembly> that have this PopupNameToViewLookupKeyMetadata attribute on them, and if they do, they are added into the IUIVisualizerService, ready for later use. All you must do is make sure to call the CinchBootStrapper, again either on App construction or App startup.

Here is an example from the Cinch V2 WPF demo app:

/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
    #region Initialisation
    /// <summary>
    /// Initiliase Cinch using the CinchBootStrapper. 
    /// </summary>
    public App()
    {
        CinchBootStrapper.Initialise(new List<Assembly> { typeof(App).Assembly });
        InitializeComponent();
    }
    #endregion
}

Showing a Specific Popup

So once you have a popup window with the KeyValuePair entry inside the IUIVisualizerService Dictionary<string, Type>, it really is child's play to show a popup from a ViewModel. You would simply do something like this:

namespace CinchV2DemoWPF
{
    [ExportViewModel("AboutViewModel")]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    public class AboutViewModel : ViewModelBase
    {
        public IUIVisualizerService uiVisualizer;

        [ImportingConstructor]
        public AboutViewModel(IUIVisualizerService uiVisualizer)
        {

            this.uiVisualizer = uiVisualizer;
            AboutViewEventToVMFiredCommand = 
              new SimpleCommand<Object, EventToCommandArgs>(
              ExecuteAboutViewEventToVMFiredCommand);
        }
        #endregion

        private void ExecuteAboutViewEventToVMFiredCommand(EventToCommandArgs args)
        {
            //Create popups ViewModel
            AboutViewLinkRequestedPopupViewModel aboutViewLinkRequestedPopupViewModel =
                new AboutViewLinkRequestedPopupViewModel();

            switch ((String)args.CommandParameter)
            {
                case "Home":
                    aboutViewLinkRequestedPopupViewModel.NavigateTo = 
                        @"http://cinch.codeplex.com/";
                    break;
                case "Source":
                    aboutViewLinkRequestedPopupViewModel.NavigateTo =
                        @"http://cinch.codeplex.com/SourceControl/list/changesets";
                    break;
            }
            //show popup
            uiVisualizer.ShowDialog("AboutViewLinkRequestedPopup", 
                                    aboutViewLinkRequestedPopupViewModel);
        }
    }
}

Cinch V2 also provides a test double of the IUIVisualizerService that you can use for testing, and it works pretty much as described here: CinchV.aspx#UIVisualizer. The only difference being that you no longer need to resolve anything for the IUIVisualizerService, you would simply inject your ViewModel under test with the TestUIVisualizerService.

This difference is due to the way Cinch V1 handles services, using DI/IOC and a common ServiceLocator pattern. Whereas Cinch V2 just relies on everything being injected through constructor parameters or via property setters. So if you want to use a test version of one of the services, you simply need to inject the test version (TestUIVisualizerService for the example above) from your unit test code rather than the real one.

Application Management

There are really only two things that are required to make the demo app work, and these are as follows:

App.Config

You must specify a valid image location in the App.Config in order for the app to work correctly. This is what my App.Config file looks like when I run the demo app at home:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <appSettings>
    <add key="YourImagePath" 
      value="C:\Users\Public\Pictures\Sample Pictures"/>
  </appSettings>
</configuration>

App Construction

As I mentioned above in the Popups section, Cinch V2 supports popup lookups and various other lookups by the use of attributes, where Types that have these attributes are found on startup. But in order for this to work correctly, Cinch needs to be told what Assemblies to look at. For the demo app, all Views/Popups are defined in the same Assembly as the demo, so I only need to tell Cinch to look for Cinch attributed Types in that Assembly. This is done by the following code in the app constructor:

/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
    #region Initialisation
    /// <summary>
    /// Tell Cinch what Assemblies to look in for Cinch attributed types that
    /// can be cached, to prevent the user from manually having to add things
    /// to lookup Dictionaries later
    /// </summary>
    public App()
    {
        CinchBootStrapper.Initialise(
          new List<Assembly> { typeof(App).Assembly });
        InitializeComponent();
    }
    #endregion
}

The Cinch BootStrapper accepts a IEnumerable<Assembly> so you can pass in other DLLs if you split your popups into different assemblies.

Views/ViewsModels

There a number of ViewModels in the Cinch V2 WPF demo app. As such, we will examine each of these in turn and see how both the View/ViewModel work together.

MainWindow / MainWindowViewModel

The MainWindow simply acts as a container to host a number of other Views within a specialized TabControl which I mentioned in another Cinch V2 article: CinchV2_3.aspx#Workspaces.

Read that section first, and then you will understand this section a little bit better. As I said, the MainWindow simply hosts other Views inside a specialized TabControl, so let's see the XAML for the MainWindow.

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:CinchV2="clr-namespace:Cinch;assembly=Cinch.WPF"
    xmlns:meffed="http:\\www.codeplex.com\MEFedMVVM"
    xmlns:local="clr-namespace:CinchV2DemoWPF;assembly="
    xmlns:Microsoft_Windows_Themes=
      "clr-namespace:Microsoft.Windows.Themes;
       assembly=PresentationFramework.Aero"
    x:Class="CinchV2DemoWPF.MainWindow"
    Icon="/CinchV2DemoWPF;component/Images/CinchIcon.png"
    Title="CinchV2 : WPF Demo app"
    MinHeight="600" 
    MinWidth="800"
    WindowState="Maximized"
    WindowStartupLocation="CenterScreen"
    meffed:ViewModelLocator.ViewModel="MainWindowViewModel">

    <Window.Resources>
        <DataTemplate DataType="{x:Type CinchV2:WorkspaceData}">
            <AdornerDecorator>
                <Border HorizontalAlignment="Stretch" 
                        VerticalAlignment="Stretch" 
                        CinchV2:NavProps.ViewCreator="{Binding}"/>
            </AdornerDecorator>
        </DataTemplate>
    </Window.Resources>

    <Grid>
         ......
         ......
         ......
         ......
        <local:TabControlEx Grid.Row="1" x:Name="tab1"
            ItemsSource="{Binding Views}"  TabStripPlacement="Left"
                CinchV2:NavProps.ShouldHideHostWhenNoItems="true"
                ItemContainerStyle="{StaticResource TabItemStyleVerticalTabs}"
                Style="{DynamicResource TabControlStyleVerticalTabs}" 
                IsSynchronizedWithCurrentItem="True" 
                DisplayMemberPath="DisplayText">

            <local:TabControlEx.ContextMenu>
                <ContextMenu IsOpen="{Binding ShowContextMenu, Mode=OneWay}">
                    <Menu x:Name="menu" Margin="0,0,0,0" 
                        Height="Auto" Foreground="Black"
                        ItemContainerStyle="{StaticResource ContextMenuItemStyle}"
                        ItemsSource="{Binding MainWindowOptions}" 
                        BorderBrush="Transparent"
                        VerticalAlignment="Top" 
                        Background="Transparent" />
                </ContextMenu>
            </local:TabControlEx.ContextMenu>
        </local:TabControlEx>
    </Grid>
</Window>

It can be seen that there is a TabControlEx and also a ContextMenu, as well as the MeffedMVVM ViewModelLocator.ViewModel attached DP to resolve the ViewModel. So let us now turn our attention to the MainWindowViewModel and see what that looks like, we are expecting it to provide the following functions:

  • ContextMenu support.
  • Some initial WorkSpaces for the TabControlEx items.

So here is the pertinent code from the MainWindowViewModel, we can indeed see that the two functions we just mentioned are catered for. This MainWindowViewModel supplies a List<CinchMenuItem> MainWindowOptions property, which is being used as the ContextMenu in the MainWindow, and it also adds WorkspaceData items into a Views property which is being used as the ItemsSource for the TabControl in the MainWindow.

/// <summary>
/// This ViewModel demonstrates how to use WorkSpaces and Menus. You will
/// need to look in the MainWindow.xaml and also the AppStyles.xaml ResourceDictionary
/// to see how the Styles are used to tie up with this ViewModel
/// </summary>
[ExportViewModel("MainWindowViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class MainWindowViewModel : ViewModelBase
{
    #region Data
    private bool showContextMenu = false;
    private IViewAwareStatus viewAwareStatusService;
    #endregion

    #region Ctor
    [ImportingConstructor]
    public MainWindowViewModel(IViewAwareStatus viewAwareStatusService)
    {
        this.viewAwareStatusService = viewAwareStatusService;
        this.viewAwareStatusService.ViewLoaded += ViewAwareStatusService_ViewLoaded;
    }
    #endregion

    #region Private Methods
    /// <summary>
    /// Creates and returns the menu items
    /// </summary>
    private List<CinchMenuItem> CreateMenus()
    {

        List<CinchMenuItem> menu = new List<CinchMenuItem>();

        CinchMenuItem menuActions = new CinchMenuItem("Actions");
        menu.Add(menuActions);

        CinchMenuItem menuAbout = new CinchMenuItem("About CinchV2");
        menuAbout.Command = new SimpleCommand<object, object>((x) =>
        {
            WorkspaceData workspace2 = 
                new WorkspaceData(@"/CinchV2DemoWPF;component/Images/About.png",
                "AboutView", null, "About Cinch V2", true);
            Views.Add(workspace2);
            ShowContextMenu = false;
        });
        menuActions.Children.Add(menuAbout);


        CinchMenuItem menuImages = new CinchMenuItem("ImageLoaderView");
        menuImages.Command = new SimpleCommand<object, object>((x) =>
        {
            String imagePath = 
              ConfigurationManager.AppSettings["YourImagePath"].ToString();

            WorkspaceData workspaceImages = 
                new WorkspaceData(@"/CinchV2DemoWPF;component/Images/imageIcon.png",
                "ImageLoaderView", imagePath, "Image View", true);
            Views.Add(workspaceImages);
            ShowContextMenu = false;
        });
        menuActions.Children.Add(menuImages);

        return menu;
    }

    private void ViewAwareStatusService_ViewLoaded()
    {

        if (Designer.IsInDesignMode)
            return;

        String imagePath = 
          ConfigurationManager.AppSettings["YourImagePath"].ToString();

        WorkspaceData workspace1 = 
            new WorkspaceData(@"/CinchV2DemoWPF;component/Images/imageIcon.png",
            "ImageLoaderView", imagePath, "Image View", true);

        WorkspaceData workspace2 = 
           new WorkspaceData(@"/CinchV2DemoWPF;component/Images/About.png",
           "AboutView", null, "About Cinch V2", true);

        Views.Add(workspace1);
        Views.Add(workspace2);
        SetActiveWorkspace(workspace1);
    }
    #endregion

    #region Public Properties
    /// <summary>
    /// Returns the bindbable Main Window options
    /// </summary>
    public List<CinchMenuItem> MainWindowOptions
    {
        get
        {
            return CreateMenus();
        }
    }

    /// <summary>
    /// ShowContextMenu
    /// </summary>
    static PropertyChangedEventArgs showContextMenuArgs =
        ObservableHelper.CreateArgs<MainWindowViewModel>(x => x.ShowContextMenu);

    public bool ShowContextMenu
    {
        get { return showContextMenu; }
        private set
        {
            showContextMenu = value;
            NotifyPropertyChanged(showContextMenuArgs);
        }
    }
    #endregion

}

ImageLoaderView / ImageLoaderViewModel

The ImageLoaderViewModel is the most complex one in the Cinch V2 WPF demo app, and it carries out the following functions:

  • Loads a set of images (folder specified in App.Config) which are loaded using a non-core service, for which there is a design time version also supplied.
  • Allows the showing/hiding of an actions area using SimpleCommand in reverse along with a CompletedAwareCommandTrigger.
  • Allows the opening of a rating popup window (AddImageRatingPopup described below).
  • Uses various other standard services such as MessageBoxService/SaveFileService/OpenFileService.

I will now explain how each of these parts is implemented in the Cinch V2 WPF demo app.

Loads a Set of Images Using Non-Core Service

As I stated in one of the previous Cinch V2 articles, Cinch V2 has a concept of core services, such as IMessageBoxService, ISaveFileService, IOpenFileService, etc., but it also makes use of non-core (application specific) services. These application specific services are extra interfaces and implementations that are also marked up with the MeffedMVVM attributes to allow them to be imported into a ViewModel.

The basic rationale behind this thinking is that it is quite testable. Imagine that your ViewModel is getting data from an external source such as a Web Service or WCF service, and that the web/WCF service is being developed in parallel. To enable testing of your ViewModel, it is a good idea to communicate with the external code via a contract interface. Not only does this keep the contract between the client app and the Web Service/WCF service well known, it also facilitates testing. If the ViewModel accepts a ISomeInterface service and expects to get data from that from somewhere, you can either use the real one, which would call the Web Service/WCF service, or you could inject a test double and simply test your ViewModel without there being a dependency on any Web Service/WCF service (that may not even be in a state to test against yet). It is all about the testability.

Anyway, the Cinch WPF demo code makes use of two such services, which are described below:

IImageDiskOperations

The ImageLoaderViewModel makes use of the IImageDiskOperations service to save and retrieve image ratings to/from an XML file of the user's choice. Where the IImageDiskOperations service contract looks like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CinchV2DemoWPF
{
    /// <summary>
    /// Data service used by the
    /// <c>ImageLoaderViewModel</c> to carry out Save/open
    /// operations
    /// </summary>
    public interface IImageDiskOperations
    {
        /// <summary>
        /// Saves viewModelsToSave to a XML file, this demonstrates the use of
        /// the <c>SaveFileService</c> from
        /// the <c>ImageLoaderViewModel</c>
        /// </summary>
        bool Save(string fileName, 
             IEnumerable<ImageViewModel> viewModelsToSave);

        /// <summary>
        /// retusn a  List<ImageViewModel>
        /// from an XML file, this demonstrates the use of
        /// the <c>OpenFileService</c> from
        /// the <c>ImageLoaderViewModel</c>
        /// </summary>
        List<ImageViewModel> Open(string fileName);
    }
}

And the real IImageDiskOperations (used at design time too) implementation looks like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition;
using System.Xml.Linq;
using System.Xml;

using MEFedMVVM.ViewModelLocator;

namespace CinchV2DemoWPF
{
    public static class CustomXElementExtensions
    {
        public static string SafeValue(this XElement input)
        {
            return (input == null) ? string.Empty : (string)input.Value;
        }
    }

    /// <summary>
    /// Runtime/Deigntime implementation of the 
    /// ImageDiskOperations service used by
    /// the <c>ImageLoaderViewModel</c> to save/open data
    /// </summary>
    [PartCreationPolicy(CreationPolicy.Shared)]
    [ExportService(ServiceType.Both, typeof(IImageDiskOperations))]
    public class ImageDiskOperations : IImageDiskOperations
    {
        #region IImageDiskOperations Members

        /// <summary>
        /// Saves viewModelsToSave to a XML file, this demonstrates the use of
        /// the <c>SaveFileService</c>
        /// from the <c>ImageLoaderViewModel</c>
        /// </summary>
        public bool Save(string fileName, 
               IEnumerable<ImageViewModel> viewModelsToSave)
        {

            CreateInitialFile(fileName, viewModelsToSave.First());
            IQueryable<ImageViewModel> allButFirst = 
                viewModelsToSave.Skip(1).AsQueryable<ImageViewModel>();

            foreach (ImageViewModel imageVM in allButFirst)
            {
                AppendToFile(fileName, imageVM);
            }
            return true;
        }

        /// <summary>
        /// retusn a  List<ImageViewModel> from an XML file,
        /// this demonstrates the use of
        /// the <c>OpenFileService</c>
        /// from the <c>ImageLoaderViewModel</c>
        /// </summary>
        public List<ImageViewModel> Open(string fileName)
        {
            var xmlImageViewModelResults =
                from imageVM in StreamElements(fileName, "ImageVM")
                select new ImageViewModel
                {
                    ImagePath = imageVM.Element("ImagePath").SafeValue(),
                    FileName = imageVM.Element("FileName").SafeValue(),
                    FileDate = DateTime.Parse(imageVM.Element("FileDate").SafeValue()),
                    FileExtension = imageVM.Element("FileExtension").SafeValue(),
                    FileSize = int.Parse(imageVM.Element("FileSize").SafeValue()),
                    Rating = int.Parse(imageVM.Element("Rating").SafeValue())
                };

            return xmlImageViewModelResults.ToList();
        }

        #endregion
        #region Private Methods

        public static IEnumerable<XElement> StreamElements(string uri, string name)
        {
            using (XmlReader reader = XmlReader.Create(uri))
            {
                reader.MoveToContent();
                while (reader.Read())
                {
                    if ((reader.NodeType == XmlNodeType.Element) &&
                      (reader.Name == name))
                    {
                        XElement element = (XElement)XElement.ReadFrom(reader);
                        yield return element;
                    }
                }
                reader.Close();
            }
        }

        private static void AppendToFile(string fullXmlPath, ImageViewModel imageVM)
        {
            XElement imagesVM_XMLDocument = XElement.Load(fullXmlPath);
            imagesVM_XMLDocument.Add(new XElement("ImageVM",
                        new XElement("ImagePath", imageVM.ImagePath),
                        new XElement("FileName", imageVM.FileName),
                        new XElement("FileDate", imageVM.FileDate),
                        new XElement("FileExtension", imageVM.FileExtension),
                        new XElement("FileSize", imageVM.FileSize),
                        new XElement("Rating", imageVM.Rating)));

            imagesVM_XMLDocument.Save(fullXmlPath);
        }

        private static void CreateInitialFile(string fullXmlPath, ImageViewModel imageVM)
        {
            XElement imagesVM_XMLDocument =
                new XElement("AllImageViewModels",
                    new XElement("ImageVM",
                        new XElement("ImagePath", imageVM.ImagePath),
                        new XElement("FileName", imageVM.FileName),
                        new XElement("FileDate", imageVM.FileDate),
                        new XElement("FileExtension", imageVM.FileExtension),
                        new XElement("FileSize", imageVM.FileSize),
                        new XElement("Rating", imageVM.Rating))
                );
            imagesVM_XMLDocument.Save(fullXmlPath);
        }
        #endregion
    }
}

And this IImageDiskOperations service is imported into the ImageLoaderViewModel like this:

[ExportViewModel("ImageLoaderViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class ImageLoaderViewModel : ViewModelBase
{

private IImageProvider imageProvider;
private IImageDiskOperations imageDiskOperations;

[ImportingConstructor]
public ImageLoaderViewModel(
    IImageProvider imageProvider,
    IImageDiskOperations imageDiskOperations)
{
    //setup services
    this.imageProvider = imageProvider;
    this.imageDiskOperations = imageDiskOperations;
}

As the ImageLoaderViewModel simply expects a IImageDiskOperations, you could easily inject a test double or Mock IImageDiskOperations into it when doing your unit testing. See the beauty of this approach, this is the real good bit about MeffedMVVM I think.

IImageProvider

The IImageProvider service simply provides images to the ImageLoaderViewModel. Where the IImageProvider service contract looks like this.

Notice how this one is expected to be an asynchronous service, where a callback delegate is called back on completion.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CinchV2DemoWPF
{
    /// <summary>
    /// Data service used by the <c>ImageLoaderViewModel</c> to obtain data
    /// </summary>
    public interface IImageProvider
    {
        void FetchImages(string imagePath, 
             Action<List<ImageData>> callback);
    }

    /// <summary>
    /// Data class used by <c>IImageProvider</c>
    /// </summary>
    public class ImageData
    {
        public string ImagePath { get; set; }
        public string FileName { get; set; }
        public DateTime FileDate { get; set; }
        public string FileExtension { get; set; }
        public int FileSize { get; set; }
    }
}

And the real IImageProvider implementation looks like this, notice how it is using a Cinch multithreading helper, BackgroundTaskManager<T>, to do the work.

/// <summary>
/// Runtime implementation of the 
/// Data service used by the <c>ImageLoaderViewModel</c> to obtain data
/// </summary>
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.Runtime, typeof(IImageProvider))]
public class RunTimeImageProvider : IImageProvider
{
    #region Data
    private BackgroundTaskManager<string, List<ImageData>> bgWorker = 
        new BackgroundTaskManager<string, List<ImageData>>();
    #endregion

    #region Public Methods/Properties

    public void FetchImages(string imagePath, 
                Action<List<ImageData>> callback)
    {
        bgWorker.TaskFunc = (argument) =>
            {
                return FetchImagesInternal(argument);
            };

        bgWorker.CompletionAction = (result) =>
            {
                callback(result);
            };

        bgWorker.WorkerArgument = imagePath;
        bgWorker.RunBackgroundTask();
    }

    /// <summary>
    /// To allow this class to be unit tested stand alone
    /// See CinchV1 articles about Unit Testing for this
    /// Or comments in Cinch BackgroundTaskManager<T> class
    /// </summary>
    public BackgroundTaskManager<string,List<ImageData>> BgWorker
    {
        get { return bgWorker; }
    }

    #endregion

    #region Private Methods
    private List<ImageData> FetchImagesInternal(string imagePath)
    {
        List<string> imageFiles = new List<string>();

        string strFilter = "*.jpg;*.png;*.gif";
        string[] filters = strFilter.Split(';');
        foreach (string filter in filters)
        {
            imageFiles.AddRange(Directory.GetFiles(imagePath, filter));
        }

        List<ImageData> images = new List<ImageData>();

        if (imageFiles.Count > 0)
        {
            int maxImages = imageFiles.Count > 20 ? 20 : imageFiles.Count;

            for (int i = 0; i < maxImages; i++)
            {
                FileInfo fi = new FileInfo(imageFiles[i]);
                ImageData id = new ImageData();
                id.ImagePath = imageFiles[i];
                id.FileDate = fi.LastWriteTime;
                id.FileExtension = fi.Extension;
                id.FileName = fi.Name;
                id.FileSize = (int)fi.Length / 1024;
                images.Add(id);
            }
        }

        return images;
    }
    #endregion
}

Whilst the design time IImageProvider service looks like this. Note that we simply callback immediately, we do not use any multithreading at all. You could also do something like this in your unit test. Testing threading operations in Unit tests is not that easy and typically involves WaitHandles etc. I leave it up to you, but just so you know, you can do something like this, no problem.

/// <summary>
/// Designtime implementation of the 
/// Data service used by the <c>ImageLoaderViewModel</c> to obtain data
/// </summary>
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.DesignTime, typeof(IImageProvider))]
public class DesigntimeImageProvider : IImageProvider
{
    #region Public Methods

    public void FetchImages(string imagePath, 
           Action<List<ImageData>> callback)
    {
        List<ImageData> fakeImages = new List<ImageData>();
        for (int i = 0; i < 10; i++)
        {
            ImageData id = new ImageData();
            id.ImagePath = 
              @"C:\Users\Public\Pictures\Sample Pictures\Desert.jpg";
            id.FileDate = DateTime.Now;
            id.FileExtension = "*.jpg";
            id.FileName = "Desert.jpg";
            id.FileSize = 223;
            fakeImages.Add(id);
        }
        callback(fakeImages);
    }

    #endregion
}

And this IImageProvider service is imported into the ImageLoaderViewModel like this:

[ExportViewModel("ImageLoaderViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class ImageLoaderViewModel : ViewModelBase
{

private IImageProvider imageProvider;
private IImageDiskOperations imageDiskOperations;

[ImportingConstructor]
public ImageLoaderViewModel(
    IImageProvider imageProvider,
    IImageDiskOperations imageDiskOperations)
{
    //setup services
    this.imageProvider = imageProvider;
    this.imageDiskOperations = imageDiskOperations;
}

As the ImageLoaderViewModel simply expects a IImageProvider, you could easily inject a test double or Mock IImageProvider into it.

Show/Hide Actions Area Using SimpleCommand / CompletedAwareCommandTrigger

The demo app has a small area on the ImageLoaderView which is not always visible. It is only visible when the requested VisualState has been asked for (default is "HideActionsState").

The area I am talking about looks like this, where we use the two Label controls at the top to Show/Hide the Actions area:

Where there are two Label controls that use EventToCommandTrigger to fire a SimpleCommand in the ImageLoaderViewModel.

<Label FontFamily="Wingdings" Foreground="Black" 
    VerticalAlignment="Center" Margin="10,5,5,5"
    VerticalContentAlignment="Center"
    FontSize="20" FontWeight="Normal" 
    Content="þ">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseLeftButtonUp">
            <CinchV2:EventToCommandTrigger 
                    Command="{Binding ShowActionsCommand}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Label>

<Label FontFamily="Wingdings" Foreground="Black" 
    VerticalAlignment="Center" Margin="5"
    VerticalContentAlignment="Center"
    FontSize="20" FontWeight="Normal" 
    Content="ý">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseLeftButtonUp">
            <CinchV2:EventToCommandTrigger 
                    Command="{Binding HideActionsCommand}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Label>

Where the SimpleCommands in the ImageLoaderViewModel simply fire an empty delegate.

//EventToCommand triggered, see the View
ShowActionsCommand = new SimpleCommand<Object, Object>(ExecuteShowActionsCommand);
HideActionsCommand = new SimpleCommand<Object, Object>(ExecuteHideActionsCommand);

//some reverse commands, that the VM fires, and the View uses as CompletedAwareCommandTriggers
//to carry out some actions. In this case GoToStateActions are used in the View
ShowActionsCommandReversed = new SimpleCommand<Object, Object>((input) => { });
HideActionsCommandReversed = new SimpleCommand<Object, Object>((input) => { });

....
....

/// <summary>
/// Goto "ShowActionsState", Using CompletedAwareCommandTrigger
/// </summary>
private void ExecuteShowActionsCommand(Object args)
{
    ShowActionsCommandReversed.Execute(null);
}

/// <summary>
/// Goto "HideActionsState", Using CompletedAwareCommandTrigger
/// </summary>
private void ExecuteHideActionsCommand(Object args)
{
    HideActionsCommandReversed.Execute(null);
}

Then in the XAML for ImageLoaderView, there are some Blend Interactions for the whole UserControl, which listen for these reverse SimpleCommands by using CompletedAwareCommandTrigger, and will put the UserControl into a new VisualState, dependant on what reverse SimpleCommand was fired from the ViewModel causing the CompletedAwareCommandTrigger to react and change to a new VisualState.

<i:Interaction.Triggers>
    <CinchV2:CompletedAwareCommandTrigger 
                    Command="{Binding ShowActionsCommandReversed}">
        <ei:GoToStateAction StateName="ShowActionsState"/>
    </CinchV2:CompletedAwareCommandTrigger>

    <CinchV2:CompletedAwareCommandTrigger 
                    Command="{Binding HideActionsCommandReversed}">
        <ei:GoToStateAction StateName="HideActionsState"/>
    </CinchV2:CompletedAwareCommandTrigger>
</i:Interaction.Triggers>

<VisualStateManager.VisualStateGroups>
    <VisualStateGroup x:Name="RectangleStates">
        <VisualStateGroup.Transitions>
            <VisualTransition GeneratedDuration="0:0:0.5">
                <VisualTransition.GeneratedEasingFunction>
                    <ElasticEase EasingMode="EaseInOut" 
                          Oscillations="5" Springiness="6"/>
                </VisualTransition.GeneratedEasingFunction>
            </VisualTransition>
        </VisualStateGroup.Transitions>
        <VisualState x:Name="ShowActionsState">
            <Storyboard>
                <DoubleAnimation Duration="0" To="1" 
                    Storyboard.TargetProperty=
                       "(UIElement.RenderTransform).(ScaleTransform.ScaleX)" 
                    Storyboard.TargetName="bordActions" 
                    d:IsOptimized="True"/>
                <DoubleAnimation Duration="0" To="1" 
                    Storyboard.TargetProperty=
                      "(UIElement.RenderTransform).(ScaleTransform.ScaleY)" 
                    Storyboard.TargetName="bordActions" 
                    d:IsOptimized="True"/>
            </Storyboard>
        </VisualState>
        <VisualState x:Name="HideActionsState"/>
        <VisualState x:Name="NullState"/>
    </VisualStateGroup>
</VisualStateManager.VisualStateGroups>
Allow the Opening of a Popup Window

As some of you may know, Cinch also provides a way to show a popup (we also covered this earlier), so by hooking up a SimpleCommand to a button from the UI to show a popup is almost trivial. Here is what you do:

In your XAML, have a Button hooked to a SimpleCommand:

<Button Grid.Row="0"
        Template="{StaticResource GlassButton}" 
        Margin="10" 
        HorizontalAlignment="Stretch" 
        Command="{Binding AddImageRatingCommand}">
    <StackPanel Orientation="Horizontal">
        <Label Style="{StaticResource selectedImageLabelStyle}" 
                    Content="Add Rating"/>
        <Label Style="{StaticResource selectedImageLabelStyle}" 
                    FontFamily="Wingdings 2" Content="êêêêê"/>
    </StackPanel>
</Button>

Where the ImageLoaderViewModel declares the SimpleCommand and SimpleCommand.Execute handlers like this:

AddImageRatingCommand = new SimpleCommand<Object, Object>(ExecuteAddImageRatingCommand);

....

/// <summary>
/// Show the AddImageRatingPopup using the IUIVisualizerService, passing
/// it a ValidatingViewModel that should validate that a valid rating between
/// 1-5 is entered by the user. If we get a valid rating then apply it to the
/// currently selected ImageViewModel
/// </summary>
private void ExecuteAddImageRatingCommand(Object args)
{
    ImageRatingViewModel imageRatingViewModel = 
         new ImageRatingViewModel(messageBoxService);
    imageRatingViewModel.ImageRating.DataValue = 
       ((ImageViewModel)loadedImagesCV.CurrentItem).Rating;


    bool? result = uiVisualizerService.ShowDialog(
          "AddImageRatingPopup", imageRatingViewModel);
    if (result.HasValue && result.Value)
    {
        ((ImageViewModel)loadedImagesCV.CurrentItem).Rating = 
            imageRatingViewModel.ImageRating.DataValue;
    }
}

This obviously relies on the IUIVisualizerService which the ImageLoaderViewModel imports using MeffedMVVM, as follows:

[ExportViewModel("ImageLoaderViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class ImageLoaderViewModel : ViewModelBase
{

private IMessageBoxService messageBoxService;
[ImportingConstructor]
public ImageLoaderViewModel(
    IMessageBoxService messageBoxService,
    IUIVisualizerService uiVisualizerService)
{
    //setup services
    this.messageBoxService = messageBoxService;
    this.uiVisualizerService = uiVisualizerService;
}

Use Various Other Services

The WPF demo app also showcases how to use several other core Cinch services such as IOpenFileService and ISaveFileService. Let's have a quick look at these in action:

SaveFileService

As Cinch supplies a ISaveFileService, it is almost trivial to use, we can just do something like this in our ViewModel. It can also be seen that this code makes use of the IImageDiskOperations that we discussed earlier.

private void ExecuteSaveToFileCommand(Object args)
{
    saveFileService.InitialDirectory = @"C:\";
    saveFileService.OverwritePrompt = true;
    saveFileService.Filter = ".xml | XML Files";
    
    var result = saveFileService.ShowDialog(null);
    if (result.HasValue && result.Value == true)
    {
        try
        {
            if (imageDiskOperations.Save(saveFileService.FileName, 
                loadedImages.AsEnumerable()))
            {
                messageBoxService.ShowInformation(string.Format(
                    "Successfully saved images to file\r\n{0}",
                    saveFileService.FileName));
            }
        }
        catch (Exception ex)
        {
            messageBoxService.ShowError(string.Format(
              "An error occurred saving images to file\r\n{0}",
              ex.Message));
        }
    }
}

Note: Cinch also provides a TestSaveFileService, which you can read more about in the Cinch V1 articles.

OpenFileService

As Cinch supplies a IOpenFileService, it is almost trivial to use, we can just do something like this in our ViewModel. It can also be seen that this code makes use of the IImageDiskOperations that we discussed earlier.

/// <summary>
/// Create a new List<ImageViewModel> by reading a XML file using XLINQ
/// </summary>
private void ExecuteOpenExistingFileCommand(Object args)
{
    openFileService.InitialDirectory = @"C:\";
    openFileService.Filter = ".xml | XML Files";

    var result = openFileService.ShowDialog(null);
    if (result.HasValue && result.Value == true)
    {
        try
        {
            List<ImageViewModel> xmlReadViewModels = 
                  imageDiskOperations.Open(openFileService.FileName);
            if (xmlReadViewModels != null)
            {
                loadedImages = xmlReadViewModels;
                LoadedImagesCV = CollectionViewSource.GetDefaultView(loadedImages);
                if (loadedImages != null)
                    LoadedImagesCV.MoveCurrentTo(loadedImages.First());

                messageBoxService.ShowInformation(string.Format(
                    "Successfully retreived images from file\r\n{0}",
                    saveFileService.FileName));
            }
            else
            {
                messageBoxService.ShowError(string.Format(
                    "Couldn't load any images from file\r\n{0}",
                    saveFileService.FileName));
            }
        }
        catch (Exception ex)
        {
            messageBoxService.ShowError(
                string.Format("An error occurred opening file\r\n{0}", 
                              ex.Message));
        }
    }
}

Note: Cinch also provides a TestOpenFileService, which you can read more about in the Cinch V1 articles.

AddImageRatingPopup / ImageRatingViewModel

AddImageRatingPopup is simply used to add a rating between 1-5 for a selected ImageViewModel in the ImageLoaderViewModel. As such, the ImageLoaderViewModel opens the AddImageRatingPopup and pushes a newed up ImageRatingViewModel at it. Here is the SimpleCommand Execute code that is run when the "Add Rating" button is clicked.

/// <summary>
/// Show the AddImageRatingPopup using the IUIVisualizerService, passing
/// it a ValidatingViewModel that should validate that a valid rating between
/// 1-5 is entered by the user. If we get a valid rating then apply it to the
/// currently selected ImageViewModel
/// </summary>
private void ExecuteAddImageRatingCommand(Object args)
{
    ImageRatingViewModel imageRatingViewModel = 
         new ImageRatingViewModel(messageBoxService);
    imageRatingViewModel.ImageRating.DataValue = 
       ((ImageViewModel)loadedImagesCV.CurrentItem).Rating;

    bool? result = uiVisualizerService.ShowDialog(
          "AddImageRatingPopup", imageRatingViewModel);
    if (result.HasValue && result.Value)
    {
        ((ImageViewModel)loadedImagesCV.CurrentItem).Rating = 
            imageRatingViewModel.ImageRating.DataValue;
    }
}

It can be seen that a new instance of a ImageRatingViewModel is created, and that the ImageRatingViewModel ImageRating DataWrapper<T> within it representing the current rating is set to the current rating associated with the selected ImageViewModel within the ImageLoaderViewModel.

After that, the AddImageRatingPopup popup window is shown using the IUIVisualizerService, where the IUIVisualizerService will create the popup and set its DataContext to be the newly instantiated ImageRatingViewModel.

So we now have a AddImageRatingPopup popup window created that is using a ImageRatingViewModel as its DataContext, but what does a ImageRatingViewModel do? Well, let's have a look at the code for it. Here it is in its entirety:

/// <summary>
/// A simple ViewModel that is the ViewModel for the 
/// <c>AddImageRatingPopup.xaml</c> popup window.
/// 
/// This example shows you you to show popups from
/// we can use a Validating ViewModel, and also how
/// to use the control focus from the ViewModel using the
/// <c>TextBoxFocusBehavior</c>. 
/// It also shows how to the <c>NumericTextBoxBehaviour</c> for
/// the Rating TextBox.
/// </summary>
public class ImageRatingViewModel : ValidatingViewModelBase
{
    #region Data
    private DataWrapper<Int32> imageRating;
    private IEnumerable<DataWrapperBase> cachedListOfDataWrappers;
    private static SimpleRule imageRatingRule;
    private IMessageBoxService messageBoxService;
    #endregion

    #region Ctor

    public ImageRatingViewModel(IMessageBoxService messageBoxService)
    {
        //setup services
        this.messageBoxService = messageBoxService;
            
        //Commands
        SaveImageRatingCommand = 
          new SimpleCommand<Object, Object>(ExecuteSaveImageRatingCommand);

        #region Create DataWrappers

        ImageRating = new DataWrapper<Int32>(this, imageRatingChangeArgs);
        ImageRating.IsEditable = true;

        //fetch list of all DataWrappers, so they can be used again later without the
        //need for reflection
        cachedListOfDataWrappers =
          DataWrapperHelper.GetWrapperProperties<ImageRatingViewModel>(this);
        #endregion

        #region Create Validation Rules

        imageRating.AddRule(imageRatingRule);
        #endregion
    }

    static ImageRatingViewModel()
    {
        imageRatingRule = new SimpleRule("DataValue", 
                     "ImageRating must be between 1-5",
                    (Object domainObject)=>
                    {
                        DataWrapper<Int32> obj = 
                           (DataWrapper<Int32>)domainObject;
                        return obj.DataValue < 0 || obj.DataValue > 5;
                    });
    }
    #endregion

    #region Public Properties

    //commands
    public SimpleCommand<Object, Object> SaveImageRatingCommand { get; private set; }

    /// <summary>
    /// CustomerId
    /// </summary>
    static PropertyChangedEventArgs imageRatingChangeArgs =
        ObservableHelper.CreateArgs<ImageRatingViewModel>(x => x.ImageRating);

    public DataWrapper<Int32> ImageRating
    {
        get { return imageRating; }
        private set
        {
            imageRating = value;
            NotifyPropertyChanged(imageRatingChangeArgs);
        }
    }
    #endregion

    #region Private Methods

    private void ExecuteSaveImageRatingCommand(Object args)
    {
            
        if (IsValid)
        {
            CloseActivePopUpCommand.Execute(true);
        }
        else
        {
            NotifyPropertyChanged(isValidChangeArgs);
            RaiseFocusEvent("ImageRating");
            messageBoxService.ShowError(
              "The Rating entered is invalid it must be between 1-5");
        }
    }

    #endregion

    #region Overrides
    /// <summary>
    /// Is the ViewModel Valid
    /// </summary>

    static PropertyChangedEventArgs isValidChangeArgs =
        ObservableHelper.CreateArgs<ImageRatingViewModel>(x => x.IsValid);

    public override bool IsValid
    {
        get
        {
            //return base.IsValid and use DataWrapperHelper, if you are
            //using DataWrappers
            return base.IsValid &&
                DataWrapperHelper.AllValid(cachedListOfDataWrappers);
        }
    }
    #endregion
}

There are a number of things to note with this code:

  1. It inherits from ValidatingViewModelBase, as such is expected to provide validation rules.
  2. It uses a DataWrapper<T> for its image rating data.
  3. It can set focus to a particular TextBox using the SetFocus event, which we discussed in this previous article.
  4. That when the popup is considered valid, it will close itself using the ViewModelBase.CloseActivePopupCommand, which will return control to the ImageLoaderViewModel, which showed the popup modally, and can now make use of the possibly modified values within the ImageRatingViewModel that was passed in from the ImageLoaderViewModel to the AddImageRatingPopup.

Most of this can be seen directly in the ImageRatingViewModel code above, the only things that can not are the TextBox validations and how it uses the Focus behaviour.

Here is the relevant XAML:

<TextBox Text="{Binding ImageRating.DataValue, UpdateSourceTrigger=LostFocus, 
            ValidatesOnDataErrors=True, ValidatesOnExceptions=True}"
            Style="{StaticResource ValidatingTextBox}"
                IsEnabled="{Binding ImageRating.IsEditable}">
    <i:Interaction.Behaviors>
        <CinchV2:TextBoxFocusBehavior IsUsingDataWrappers="true" />
        <CinchV2:NumericTextBoxBehaviour/>
    </i:Interaction.Behaviors>
</TextBox>

Where the Style called ValidatingTextBox looks like this:

<Style x:Key="ValidatingTextBox" TargetType="{x:Type TextBoxBase}">
    <Setter Property="SnapsToDevicePixels" Value="True"/>
    <Setter Property="OverridesDefaultStyle" Value="True"/>
    <Setter Property="KeyboardNavigation.TabNavigation" Value="None"/>
    <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
    <Setter Property="MinWidth" Value="120"/>
    <Setter Property="MinHeight" Value="20"/>
    <Setter Property="AllowDrop" Value="true"/>
    <Setter Property="Validation.ErrorTemplate" Value="{x:Null}"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TextBoxBase}">
                <Border 
                        Name="Border"
                        CornerRadius="5" 
                        Padding="2"
                        Background="White"
                        BorderBrush="Black"
                        BorderThickness="2" >
                    <ScrollViewer Margin="0" x:Name="PART_ContentHost"/>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsEnabled" Value="False">
                        <Setter TargetName="Border" 
                          Property="Background" Value="LightGray"/>
                        <Setter TargetName="Border" 
                          Property="BorderBrush" Value="Black"/>
                        <Setter Property="Foreground" Value="Gray"/>
                    </Trigger>
                    <Trigger Property="Validation.HasError" Value="true">
                        <Setter TargetName="Border" Property="BorderBrush" 
                                Value="Red"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Style.Triggers>
        <Trigger Property="Validation.HasError" Value="true">
            <Setter Property="ToolTip"
                        Value="{Binding RelativeSource={x:Static RelativeSource.Self},
                        Path=(Validation.Errors).CurrentItem.ErrorContent}"/>
        </Trigger>
    </Style.Triggers>
</Style>

Which obviously results in the following being shown whenever the validation rules defined in the ImageRatingViewModel are broken:

AboutView / AboutViewModel

The AboutView is pretty simple, it just holds a FlowDocument, and also link buttons that make use of the EventToCommandTrigger Blend Trigger that I discussed here: CinchV2_3.aspx#Interactivity.

Here is the pertinent XAML for the AboutView; as before, notice the MeffedMVVM ViewModelLocator.ViewModel attached DP to resolve the ViewModel:

<UserControl x:Class="CinchV2DemoWPF.AboutView"
             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="http:\\www.codeplex.com\MEFedMVVM"
             xmlns:local="clr-namespace:CinchV2DemoWPF;assembly="
             xmlns:i="clr-namespace:System.Windows.Interactivity;
                      assembly=System.Windows.Interactivity"
             mc:Ignorable="d" 
             d:DesignHeight="371" d:DesignWidth="533"
             meffed:ViewModelLocator.ViewModel="AboutViewModel">

    <Grid>
         ......
         ......
         ......
         ......
        <Grid Grid.Column="1" Background="{StaticResource mainGridBrush}">
            <StackPanel Orientation="Vertical" 
                  VerticalAlignment="Top" Margin="30">
                <Label Style="{StaticResource aboutLabelStyle}"  
                         Content="Check Out Cinch:"/>
                <StackPanel Orientation="Vertical">
                    <TextBlock Style="{StaticResource aboutTextBlockStyleLinks}" 
                                   Text="Home Page [At Codeplex]">
                        <i:Interaction.Triggers>
                            <i:EventTrigger EventName="MouseLeftButtonDown">
                                <CinchV2:EventToCommandTrigger 
                                     Command="{Binding AboutViewEventToVMFiredCommand}" 
                                     CommandParameter="Home"/>
                            </i:EventTrigger>
                        </i:Interaction.Triggers>
                    </TextBlock>
                    <TextBlock Style="{StaticResource aboutTextBlockStyleLinks}" 
                                   Text="Source Code [At Codeplex]">
                        <i:Interaction.Triggers>
                            <i:EventTrigger EventName="MouseLeftButtonDown">
                                <CinchV2:EventToCommandTrigger 
                                     Command="{Binding AboutViewEventToVMFiredCommand}" 
                                     CommandParameter="Source"/>
                            </i:EventTrigger>
                        </i:Interaction.Triggers>
                    </TextBlock>
                </StackPanel>
            </StackPanel>
        </Grid>
    </Grid>
</UserControl>

Not much going on there really, apart from the ViewModel resolution using MeffedMVVM, and the 2 x EventToCommandTriggers. So let's have a look at the AboutViewModel for completeness.

[ExportViewModel("AboutViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class AboutViewModel : ViewModelBase
{
    public IUIVisualizerService uiVisualizer;

    [ImportingConstructor]
    public AboutViewModel(IUIVisualizerService uiVisualizer)
    {
        this.uiVisualizer = uiVisualizer;
        AboutViewEventToVMFiredCommand = 
          new SimpleCommand<Object, EventToCommandArgs>(
          ExecuteAboutViewEventToVMFiredCommand);
    }

    /// <summary>
    /// An event to command fired command, have a look at the AboutView, and look for
    /// where this command is used to see how the View can fire Commands in the ViewModel
    /// passing in Parameters 
    /// </summary>
    public SimpleCommand<Object, EventToCommandArgs> 
           AboutViewEventToVMFiredCommand { get; private set; }

    private void ExecuteAboutViewEventToVMFiredCommand(EventToCommandArgs args)
    {
        AboutViewLinkRequestedPopupViewModel aboutViewLinkRequestedPopupViewModel =
            new AboutViewLinkRequestedPopupViewModel();
        switch ((String)args.CommandParameter)
        {
            case "Home":
                aboutViewLinkRequestedPopupViewModel.NavigateTo = 
                    @"http://cinch.codeplex.com/";
                break;
            case "Source":
                aboutViewLinkRequestedPopupViewModel.NavigateTo =
                    @"http://cinch.codeplex.com/SourceControl/list/changesets";
                break;
        }
        uiVisualizer.ShowDialog("AboutViewLinkRequestedPopup", 
                                aboutViewLinkRequestedPopupViewModel);
    }
    #endregion
}

So what do these two XAML declared EventToCommandTrigger Blend Triggers actually do? Well, they both call the AboutViewModel AboutViewEventToVMFiredCommand SimpleCommand passing in different CommandParameter values. Then the AboutViewModel uses its IUIVisualizerService which was injected via MeffedMVVM, to show a popup window called AboutViewLinkRequestedPopup. You can learn more about how popups work, by reading the Popups section later in the article.

AboutViewLinkRequestedPopup / AboutViewLinkRequestedPopupViewModel

The AboutViewLinkRequestedPopup simply navigates to a requested web page in an embedded WebBrowser.

As we just saw, the AboutViewModel is responsible for showing a popup window called AboutViewLinkRequestedPopup, which is done using the IUIVisualizerService. Now if were to examine the XAML for the popup window, we would not see any MeffedMVVM attached DP in there to resolve the ViewModel, which is different from before.

<Window x:Class="CinchV2DemoWPF.AboutViewLinkRequestedPopup"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="CinchV2 : WPF Demo app"  
        Icon="/CinchV2DemoWPF;component/Images/CinchIcon.png"
        Height="700" 
        Width="700"
        WindowStartupLocation="CenterOwner">
    <Grid>
        <WebBrowser x:Name="browser"  Margin="0"/>
    </Grid>
</Window>

The main reason being that popups in Cinch are expected to have some state (Viewodel) pushed at them by the caller. The popup manipulates the ViewModel pushed at it, and then more than likely closes, but since the caller was the one that made the initial ViewModel to push at the popup, the caller (parent ViewModel) has all the changes that were done in the popup in the ViewModel it pushed at the popup window. So that is why you do not see any MeffedMVVM Attached DP; basically popup ViewModels are expected to be created by other ViewModels.

What I typically do is push in the expected services into the popup's ViewModel from the parent ViewModel, then push the newed up ViewModel into the popup using the IUIVisualizerService. This approach does mean that the parent ViewModel needs to have a reference to the service it is intending to push at the child ViewModel, but hey, I am fine with that.

Actually, there is a way you can still use the MeffedMVVM attached DP/attributes to simply get MeffedMVVM to inject property setters for your expected services, but it's kind of an advanced topic, and you probably will not have much need to do that. However, if you do have a need to let MeffedMVVM inject property setters (such as for services), this Cinch forum post makes interesting reading:

http://www.codeproject.com/Messages/3533572/Question-about-ViewModel-constructors-with-MEF.aspx

But anyway, we digress for now; let's concentrate on normal usage, which is for a parent ViewModel to create a new popup ViewModel; we saw that in the AboutViewModels code, let's just revisit that quickly:

private void ExecuteAboutViewEventToVMFiredCommand(EventToCommandArgs args)
{
    AboutViewLinkRequestedPopupViewModel aboutViewLinkRequestedPopupViewModel =
        new AboutViewLinkRequestedPopupViewModel();
    switch ((String)args.CommandParameter)
    {
        case "Home":
            aboutViewLinkRequestedPopupViewModel.NavigateTo = 
                @"http://cinch.codeplex.com/";
            break;
        case "Source":
            aboutViewLinkRequestedPopupViewModel.NavigateTo =
                @"http://cinch.codeplex.com/SourceControl/list/changesets";
                
            break;
    }
    uiVisualizer.ShowDialog("AboutViewLinkRequestedPopup", 
                            aboutViewLinkRequestedPopupViewModel);
}

See how it creates a AboutViewLinkRequestedPopupViewModel and passes that as the state to the IUIVisualizerService for use with the new popup instance? Let's shift our attention to this AboutViewLinkRequestedPopupViewModel, which looks like this in its entirety:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Cinch;
using System.ComponentModel;
using System.ComponentModel.Composition;

namespace CinchV2DemoWPF
{
    ///NOTE : As this is a popup we should not be manually setting the Views 
    ///ViewModel to a pre populated ViewModel, by using the 
    ///<c>WPFUIVisualizerService</c> service, as we would typically pass the
    ///popup and object to alter the state with
    public class AboutViewLinkRequestedPopupViewModel : 
           ViewModelBase, IViewStatusAwareInjectionAware
    {
        #region Data
        private string navigateTo;
        #endregion

        #region Public Properties

        private IViewAwareStatus ViewAwareStatusService { get; set; }

        /// <summary>
        /// NavigateTo
        /// </summary>
        static PropertyChangedEventArgs navigateToArgs =
            ObservableHelper.CreateArgs<AboutViewLinkRequestedPopupViewModel>(
            x => x.NavigateTo);

        public string NavigateTo
        {
            get { return navigateTo; }
            set
            {
                navigateTo = value;
                NotifyPropertyChanged(navigateToArgs);
            }
        }
        #endregion

        #region IViewStatusAwareInjectionAware Members

        public void InitialiseViewAwareService(IViewAwareStatus viewAwareStatusService)
        {
            this.ViewAwareStatusService = viewAwareStatusService;
            this.ViewAwareStatusService.ViewLoaded += ViewAwareStatusService_ViewLoaded;
        }
        #endregion

        #region Private Methods

        private void ViewAwareStatusService_ViewLoaded()
        {
            //Get the View from the ViewAwareStatusService as a specific interface
            //and ask it to navigate its internal WebBrowser to the requested Url
            //Sometimes a tiny bit of code behind in the view is the correct thing
            //to do, we could abstract ourselves to insanity, but the thing is, if
            //it truly is a UI type operation and is not really something that requires
            //a lot of testing, I see nothing wrong with a tiny bit of code behind in the
            //view and that is what this is showing you
            IWebBrowserNavigatable webBrowserNavigatable = 
                this.ViewAwareStatusService.View as IWebBrowserNavigatable;
            if (webBrowserNavigatable != null)
            {
                ((IWebBrowserNavigatable)webBrowserNavigatable).NavigateTo(NavigateTo);
            }
        }
        #endregion
    }
}

There are actually a few subtleties going on there, the first of which is the use of the IViewStatusAwareInjectionAware interface. What that is for is, so that when the IUIVisualizerService creates a popup, it examines the ViewModel is was passed in, and sees it wants to know about the IViewStatusAware service (which is what the IViewStatusAwareInjectionAware interface tells it), and if it does, a new IViewStatusAware for the View is injected into the Viewmodel.

Here is the relevant code from the IUIVisualizerService that deals with this:

private Window CreateWindow(string key, object dataContext, bool setOwner,
        EventHandler<UICompletedEventArgs> completedProc, bool isModal)
{
    if (string.IsNullOrEmpty(key))
        throw new ArgumentNullException("key");

    Type winType;
    lock (_registeredWindows)
    {
       if (!_registeredWindows.TryGetValue(key, out winType))
          return null;
    }

    var win = (Window)Activator.CreateInstance(winType);

    if (dataContext is IViewStatusAwareInjectionAware)
    {
        IViewAwareStatus viewAwareStatus = 
           ViewModelRepository.Instance.Resolver.Container.
           GetExport<IViewAwareStatus>().Value;
        viewAwareStatus.InjectContext((FrameworkElement)win);
           ((IViewStatusAwareInjectionAware)
             dataContext).InitialiseViewAwareService(viewAwareStatus);
    }

    win.DataContext = dataContext;

    ......
    ......
    ......
}

The other subtlety in the AboutViewLinkRequestedPopupViewModel code is that it does something strange in the IViewAwareStatus.Loaded handler:

private void ViewAwareStatusService_ViewLoaded()
{
    //Get the View from the ViewAwareStatusService as a specific interface
    //and ask it to navigate its internal WebBrowser to the requested Url
    //Sometimes a tiny bit of code behind in the view is the correct thing
    //to do, we could abstract ourselves to insanity, but the thing is, if
    //it truly is a UI type operation and is not really something that requires
    //a lot of testing, I see nothing wrong with a tiny bit of code behind in the
    //view and that is what this is showing you
    IWebBrowserNavigatable webBrowserNavigatable = 
        this.ViewAwareStatusService.View as IWebBrowserNavigatable;
    if (webBrowserNavigatable != null)
    {
        ((IWebBrowserNavigatable)webBrowserNavigatable).NavigateTo(NavigateTo);
    }
}

See how it gets the View from the IViewAwareStatus instance and is expecting it to be a IWebBrowserNavigatable? How is that possible?

Well, the IViewAwareStatus service exposes the View (using a WeakReference) so you can cast the View to whatever interface a View may implement and use it in your ViewModel. In this case, the AboutViewLinkRequestedPopup View implements the IWebBrowserNavigatable interface, as shown below.

[PopupNameToViewLookupKeyMetadata("AboutViewLinkRequestedPopup", 
        typeof(AboutViewLinkRequestedPopup))]
public partial class AboutViewLinkRequestedPopup : 
    Window, 
    IWebBrowserNavigatable
    //Show that sometimes code behind is the right thing to do
{

    public void NavigateTo(string url)
    {
        browser.Navigate(url);
    }
}

The ViewModel can now talk to the View using this IWebBrowserNavigatable interface.

I don't normally use any interfaces on my Views, but sometimes it is the correct thing to do, so just have your ViewModel talk to the View using a well known contract, a.k.a. an interface.

That's It ....For Now

That is all I wanted to say for now. I have one more article in this new series and then I am done. The next one is on the Silverlight demo app, and just by the way will be my 100th article here at CodeProject, which will be quite something, so would be nice to get some votes/comments on that one.

Could I just ask if you have enjoyed this article, and feel it is going to help you out, could you please show your support by leaving a vote/comment?

As before, if you have any deep MEF related questions, you should direct this to Marlon Grech either by using his blog C# Disciples, or by using the MefedMVVM CodePlex site; any other Cinch V2 questions will be answered by the next Cinch V2 articles.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here