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

CinchV2: Version2 of my Cinch MVVM framework: Part 6 of n

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

Table of Contents

Introduction

Last time we talked about the Cinch V2 WPF demo app. In this article, we will be going through the Cinch V2 Silvelight 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 specific SL implementation)   Yes  
Threading AddRangeObservableCollection.cs (this is 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 Silverlight demo app do?

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

  • There is a main TabControl that allows you to enter a username to play a game of O's and X's as against the computer
  • There is also a O's and X's control
  • There is a list of previously played games that have been played where the user can call up a ChildWindow representing the played game

Now, that may not look like much, but believe me, that is enough to showcase most of Cinch's Silverlight 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:

MainWindow is sitting there waiting for you to type in a username to play a game as:

When you enter a name and press OK, you can use the Flip button (top right) to show a new GameView and GameStatView.

GameView allows you to play a game by clicking on the grid buttons:

GameStatView shows you a list of all the previously played games, which you can choose to view using the buttons provided.

If you click one of the buttons of the GameStatView, you will be presented with a PlayedGameChildWindow ChildWindow which will show you the previous game state.

Overall Structure

The following diagram illustrates the overall structure of the Views/ViewModels and popup ChildWindows for the Silverlight 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 Silverlight 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 ChildWindows perform in the Silverlight demo app.

ChildWindows

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

Ensuring That ChildWindow is Available to Show

There is a service that deals with showing ChildWindows called IChildWindowService, that holds a Dictionary<string, Type> such that a consumer of this IChildWindowService service can simply request a ChildWindow by name (string) from the internal Dictionary<string, Type>, and then the IChildWindowService 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 IChildWindowService service implementation for SL:

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

namespace Cinch
{
    /// <summary>
    /// This class implements the IChildWindowService for SL purposes.
    /// If you have attributed up your views using the PopupNameToViewLookupKeyMetadata
    /// Registration of Views with the IChildWindowService service is automatic.
    /// However you can still register views manually, to do this simply put some lines like this in you App.Xaml.cs
    /// MefLocator.Container.GetExport<IChildWindowService>().Value.Register("ChildWindow1", typeof(ChildWindow1));
    /// </summary>
    /// </summary>
    /// <example>
    /// <![CDATA[
    /// 
    ///    //ui is an instance of the IChildWindowService which was MEF injected (say) into your ViewModel
    ///    bool? dialogResult = null;
    ///    ui.Show(popName, this, (s, e) =>
    ///    {
    ///        dialogResult = e.Result;
    ///        //you can do what you like with dialogResult
    ///    });
    ///    //NOTE : You should not do anymore here, as the ChildWindow, although it appears
    ///    //modal, it does not block parent code.
    /// ]]>
    /// </example>
    [PartCreationPolicy(CreationPolicy.Shared)]
    [ExportService(ServiceType.Both, typeof(IChildWindowService))]
    public class ChildWindowService : IChildWindowService
    {
        #region Data
        private readonly Dictionary<string, Type> _registeredWindows;
        #endregion

        #region Ctor
        public ChildWindowService()
        {
            _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(ChildWindow).IsAssignableFrom(winType))
                throw new ArgumentException("winType must be of type ChildWindow");

            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 ChildWindow associated with the given key
        /// calling code is not blocked, and will not wait on the ChildWindow being
        /// closed. So this should only be used when there is no code dependant on
        /// the ChildWindows DialogResult. If you want to use the result of the ChildWindow
        /// being shown you can should create a callback delegate for the completedProc
        /// </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="completedProc">Callback used when UI closes (may be null)</param>
        public void Show(string key, object state, EventHandler<UICompletedEventArgs> completedProc)
        {
            ChildWindow win = CreateChildWindow(key, state, completedProc);
            if (win != null)
            {
                win.Show();
            }
        }
 
        #endregion

        #region Private Methods

        /// <summary>
        /// This creates the SL ChildWindow from a key.
        /// </summary>
        /// <param name="key">Key</param>
        /// <param name="dataContext">DataContext (state) object</param>
        /// <param name="completedProc">Callback used when UI closes (may be null)</param>
        /// <returns>ChildWindow</returns>
        private ChildWindow CreateChildWindow(string key, object dataContext, 
            EventHandler<UICompletedEventArgs> completedProc)
        {
            if (string.IsNullOrEmpty(key))
                throw new ArgumentNullException("key");

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

            var win = (ChildWindow)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;
            ViewModelBase bvm=null;

            EventHandler<CloseRequestEventArgs> handler = ((s, e) =>
                    {
                        try
                        {
                            win.DialogResult = e.Result;
                        }
                        catch (InvalidOperationException)
                        {
                            win.Close();
                        }
                    });

            if (dataContext != null)
            {
                bvm = dataContext as ViewModelBase;
                if (bvm != null)
                {
                    bvm.CloseRequest += handler;

                }
            }

            win.Closed += (s, e) =>
            {
                bvm.CloseRequest -= handler;

                if (completedProc != null)
                {
                    completedProc(this, new UICompletedEventArgs()
                    {
                        State = dataContext,
                        Result = win.DialogResult
                    });

                    GC.Collect();
                }
            };

            return win;
        }
        #endregion
    }
}

For further reading, have a look at this link CinchV2_2.aspx#CoreServices and read the IChildWindowService section.

So you might be wondering how does this IChildWindowServiceDictionary<string, Type> get populated in time to make sure that when a ChildWindow is requested, that 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 IChildWindowService 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<IChildWindowService >().Value.Register("ChildWindow1", 
                typeof(ChildWindow1));
        InitializeComponent();
    }
}

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

Automatically Finding Types That Are ChildWindow

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 IChildWindowService, all we have to do is attribute it up as follows in its code-behind:

[PopupNameToViewLookupKeyMetadata("PlayedGameChildWindow",typeof(PlayedGameChildWindow))]
public partial class PlayedGameChildWindow : ChildWindow
{
}

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 an IEnumerable<Assembly> to examine for any Types in the passed-in IEnumerable<Assembly> that have the PopupNameToViewLookupKeyMetadata attribute on them, and if they do, they are added into the IChildWindowService 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 ChildWindow

Once you have a ChildWindowKeyValuePair entry inside the IChildWindowService Dictionary<string, Type>, it really is child's play (pardon the pun) to show a ChildWindow from a ViewModel. You would simply do something like this:

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel;

using Cinch;
using MEFedMVVM.Common;
using MEFedMVVM.ViewModelLocator;

namespace CinchV2DemoSL
{
    public class GameStatViewModel : ViewModelBase
    {
        private ICommand viewGameCommand;
        private IChildWindowService ChildWindowService { get; set; }

        public GameStatViewModel(string winnerName, string gameText)
        {
            ViewGameCommand = new SimpleCommand<Object, Object>(ExecuteViewGameCommand);
        }

        public SimpleCommand<Object, Object> ViewGameCommand { get; private set; }

        private void ExecuteViewGameCommand(object o)
        {
            bool? dialogResult = null;
            ChildWindowService.Show("PlayedGameChildWindow", 
                new PlayedGameViewModel(GameText), (s, e) =>
            {
                dialogResult = e.Result;
                string result = dialogResult.HasValue && dialogResult.Value ? "ok" : "Cancel";
                MessageBoxService.ShowInformation("You clicked " + result);
                //you can do what you like with dialogResult
            });
        }
    }
}

Important: ChildWindows in Silverlight are not the same as Modal dialogs, in WPF say, they look Modal and act Modal, but any code after showing a ChildWindow will continue to run. So when we show a ChildWindow, we need to ensure no more code is run. How do we deal with the modified state that the ChildWindow may have manipulated I hear you ask. Well, quite simple, we use a callback, as shown in the code snippet above. We can simply hook up a lambda there, and run some code when the ChildWindow is closed.

Cinch V2 also provides a test double of the IChildWindowService that you can use for testing.

So if you want to use a test version of one of the services, you simply need to inject the test version (TestChildWindowService for the example above) from your unit test code rather than the real one.

Application Management

There is really only one thing that is required to make the demo app work, and that is as follows:

App Construction

As I mentioned above in the ChildWindows 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 Silverlight demo app. As such, we will examine each of these in turn and see how both the View/ViewModel work together.

UserEntryView / UserEntryViewModel

This view is the first view that is presented when the Silverlight demo app starts. Its purpose is simple, get a name from the user which can be used to store against games of O's and X's that the user plays using the rest of the Silverlight application.

There is really not too much to this view, let's have a look at the View first, shall we? Here is the relevant code from the UserEntryView XAML:

<UserControl x:Class="CinchV2DemoSL.UserEntryView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400"
    Width="650" Height="370"
    xmlns:CinchV2="clr-namespace:Cinch;assembly=Cinch.SL"
    xmlns:meffed="http:\\www.codeplex.com\MEFedMVVM"
    meffed:ViewModelLocator.ViewModel="UserEntryViewModel">

    <Border BorderBrush="#ff656565" BorderThickness="2" 
            HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="VisualStateGroup">
                <VisualStateGroup.Transitions>
                    <VisualTransition GeneratedDuration="0" To="InValidState"/>
                </VisualStateGroup.Transitions>
                <VisualState x:Name="InValidState">
                    <Storyboard>
                        <ObjectAnimationUsingKeyFrames 
                              Storyboard.TargetProperty="(UIElement.Visibility)" 
                              Storyboard.TargetName="image">
                            <DiscreteObjectKeyFrame KeyTime="0">
                                <DiscreteObjectKeyFrame.Value>
                                    <Visibility>Visible</Visibility>
                                </DiscreteObjectKeyFrame.Value>
                            </DiscreteObjectKeyFrame>
                        </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualState>
                <VisualState x:Name="ValidState"/>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>

        <Grid x:Name="grid">
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>

            <Grid Grid.Row="0" Background="{StaticResource ViewBackGrounds}">
                .......
                .......
                
                <TextBox Height="26" Margin="10,79,0,0" TextWrapping="Wrap" 
                    VerticalAlignment="Top" Width="300"
                    Text="{Binding UserName.DataValue, Mode=TwoWay, 
                        ValidatesOnDataErrors=True, 
                        ValidatesOnExceptions=True,
                        ValidatesOnNotifyDataErrors=True}" HorizontalAlignment="Left"/>

                <Image x:Name="image" Margin="0,-29,23,152" 
                   HorizontalAlignment="Right" Width="134" 
                   Source="/CinchV2DemoSL;component/Images/error.png" 
                   Visibility="Collapsed">

                    <Image.Effect>
                        <DropShadowEffect Color="#7F979797" 
                           ShadowDepth="12" Opacity="0.5"/>
                    </Image.Effect>
                </Image>
            </Grid>
               ......
               ......
               ......
                <Button Content="Ok" Height="25" Margin="5" 
                   Width="70" Foreground="Black"
                   Command="{Binding SaveUserNameCommand}" 
                   HorizontalAlignment="Right"/>
               ......
               ......
               ......

        </Grid>
    </Border>
</UserControl>

There is not too much in there apart from a Button which fires a SimpleCommand and a TextBox which binds to the UserEntryViewModel.UserName property, but there is also some VisualStates that are used to show an error icon if the user doesn't enter a valid user name (basically, can't be empty).

The other thing to note is the standard ViewModel resolution which is done using MeffedMVVM.

Now, how about the UserEntryViewModel? Let's just recap on what it does:

  • Allows a user to enter a name.
  • The user name is validated, using a validation rule.
  • When the user clicks the OK button, if the user has entered a valid name, the control ViewModel is deemed valid and the username is saved to a file in IsolatedStorage using a non-core UI service, and the View is transitioned to the "ValidState" VisualState.
  • If however the user does not enter a username and clicks the OK button, the ViewModel is deemed invalid and the View is transitioned to the "InvalidState" VisualState.

Let's examine each of these elements, shall we?

Entering a Valid UserName / Validation

This is easily achieved by inheriting from a ValidatingViewModelBase and adding a Cinch SimpleRule validation rule to a DataWrapper<T>.

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel.Composition;
using System.Threading;
using System.Collections.Generic;
using System.ComponentModel;

using Cinch;
using MEFedMVVM.ViewModelLocator;

namespace CinchV2DemoSL
{
    /// <summary>
    /// Game Stat List ViewModel
    /// Demonstrates CinchV2 IDataErrorInfo support/DataWrappers/IVSM service
    /// MeffedMVVM service injection
    /// </summary>
    [ExportViewModel("UserEntryViewModel")]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    public class UserEntryViewModel : ValidatingViewModelBase
    {
        private DataWrapper<String> userName;
        private IEnumerable<DataWrapperBase> cachedListOfDataWrappers;
        private static SimpleRule userNameRule;

        [ImportingConstructor]
        public UserEntryViewModel(
        .....
        .....
        .....)
        {
            UserName = new DataWrapper<String>(this, userNameChangeArgs);
            UserName.IsEditable = true;

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

            userName.AddRule(userNameRule);
        }

        static UserEntryViewModel()
        {
            userNameRule = new SimpleRule("DataValue", "UserName can not be empty",
                    (Object domainObject) =>
                    {
                        DataWrapper<String> obj = (DataWrapper<String>)domainObject;
                        return String.IsNullOrEmpty(obj.DataValue);
                    });
        }
   
        /// <summary>
        /// UserName
        /// </summary>
        static PropertyChangedEventArgs userNameChangeArgs =
            ObservableHelper.CreateArgs<UserEntryViewModel>(x => x.UserName);

        public DataWrapper<String> UserName
        {
            get { return userName; }
            private set
            {
                userName = value;
                NotifyPropertyChanged(userNameChangeArgs);
            }
        }

        /// <summary>
        /// Is the ViewModel Valid
        /// </summary>
        static PropertyChangedEventArgs isValidChangeArgs =
            ObservableHelper.CreateArgs<UserEntryViewModel>(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);
            }
        }
    }
}

Where we have the following declared in the XAML:

<TextBox Height="26" Margin="10,79,0,0" TextWrapping="Wrap" 
        VerticalAlignment="Top" Width="300"
        Text="{Binding UserName.DataValue, Mode=TwoWay, 
            ValidatesOnDataErrors=True, 
            ValidatesOnExceptions=True,
            ValidatesOnNotifyDataErrors=True}" HorizontalAlignment="Left"/>

Checking Whether the User Entry Was Valid

This is very simple. All there is to it is to use a SimpleCommand<T1,T2> for the "OK" button and check the IsValid property as shown above. And if the user entered a valid name, transition the View to the "ValidState" VisualState using the IVSM service, and save a IsolatedStorage file with the user name in it. If the user entered some invalid data (as defined by the Cinch SimpleRule validation rule), simply transition the View to the "InvalidState" VisualState using the IVSM service.

The important parts of the UserEntryViewModel are shown below:

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel.Composition;
using System.Threading;
using System.Collections.Generic;
using System.ComponentModel;

using Cinch;
using MEFedMVVM.ViewModelLocator;

namespace CinchV2DemoSL
{
    /// <summary>
    /// Game Stat List ViewModel
    /// Demonstrates CinchV2 IDataErrorInfo support/DataWrappers/IVSM service
    /// MeffedMVVM service injection
    /// </summary>
    [ExportViewModel("UserEntryViewModel")]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    public class UserEntryViewModel : ValidatingViewModelBase
    {
        private IMessageBoxService messageBoxService;
        private IViewAwareStatus viewAwareStatusService;
        private IChildWindowService childWidowService;
        private IGameStorerProvider gameStorerProvider;
        private IVSM visualStateManagerService;
    
        [ImportingConstructor]
        public UserEntryViewModel(
            IMessageBoxService messageBoxService,
            IViewAwareStatus viewAwareStatusService,
            IChildWindowService childWidowService,
            IGameStorerProvider gameStorerProvider,
            IVSM visualStateManagerService)
        {
            this.messageBoxService = messageBoxService;
            this.viewAwareStatusService = viewAwareStatusService;
            this.childWidowService = childWidowService;
            this.gameStorerProvider = gameStorerProvider;
            this.visualStateManagerService = visualStateManagerService;

            //Commands
            SaveUserNameCommand = new SimpleCommand<Object, Object>(ExecuteSaveUserNameCommand);
        }

        public SimpleCommand<Object, Object> SaveUserNameCommand { get; private set; }

        private void ExecuteSaveUserNameCommand(Object args)
        {
            if (IsValid)
            {
                visualStateManagerService.GoToState("ValidState");
                gameStorerProvider.WriteUserNameToFile(UserName.DataValue);
                messageBoxService.ShowError("Successfully saved username, flip to play a game!");
            }
            else
            {
                visualStateManagerService.GoToState("InValidState");
                messageBoxService.ShowError("The UserName entered is invalid it must contain a value");
            }
        }
    ......
    ......
    }
}

The more eagle eyed amongst you may be thinking, but hey hang on a minute, what if I am Unit testing my ViewModel, I will not be in a web context and may not even want my ViewModel to be using an IsolatedStorage file. No problem. This saving of the user name to file is done by the use of a non-core service, called IGameStorerProvider, which looks like this:

/// <summary>
/// Data service used to store/retrieve game data
/// </summary>
public interface IGameStorerProvider
{
    void StoreGameResults(string winnerName, String completeGameText);
    void WriteUserNameToFile(string username);
    string ReadUserNameFromFile();
    void FetchGameResults(Action<List<Tuple<string, string>>> callback);
}

I have provided a runtime version of this, which looks like this:

/// <summary>
/// Runtime Data service used to store/retrieve game data
/// </summary>
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.Runtime, typeof(IGameStorerProvider))]
public class RunTimeGameStorerProvider : IGameStorerProvider
{

#region Data
    private BackgroundTaskManager<object,List<Tuple<string, string>>> bgWorker = 
        new BackgroundTaskManager<object, List<Tuple<string, string>>>();
#endregion

#region IGameStorerProvider Members

    public void StoreGameResults(string winnerName, string completeGameText)
    {
        IsolatedStorageHelper.StoreGameResults(winnerName, completeGameText);
    }

    public void WriteUserNameToFile(string username)
    {
        IsolatedStorageHelper.WriteUserNameToFile(username);
    }

    public string ReadUserNameFromFile()
    {
        return IsolatedStorageHelper.ReadUserNameFromFile();
    }

    public void FetchGameResults(Action<List<Tuple<string, string>>> callback)
    {
        bgWorker.TaskFunc = (argument) =>
        {
            return IsolatedStorageHelper.FetchGameResults();
        };

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

        bgWorker.RunBackgroundTask();
}
#endregion

#region Public Properties

    /// <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<object, List<Tuple<string, string>>> BgWorker
    {
        get { return bgWorker; }
    }

#endregion
}

And a design time version to supply design time data to Blend, which looks like this:

/// <summary>
/// Runtime Data service used to store/retrieve game data
/// </summary>
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.DesignTime, typeof(IGameStorerProvider))]
public class DesignTimeGameStorerProvider : IGameStorerProvider
{
    #region IGameStorerProvider Members

    public void StoreGameResults(string winnerName, string completeGameText)
    {
        //do nothing
    }

    public void WriteUserNameToFile(string username)
    {
        //do nothing
    }

    public string ReadUserNameFromFile()
    {
        return "Sacha";
    }

    public void FetchGameResults(Action<List<Tuple<string, string>>> callback)
    {
        List<Tuple<string, string>> results = 
                new List<Tuple<string, string>>();
        results.Add(new Tuple<string,string>("Sacha won","O:X:X:O:O:X:X:O:O:"));
        results.Add(new Tuple<string,string>("Computer won","O:O:O:X:X:X:X:O:O:"));
        results.Add(new Tuple<string,string>("Sacha won","O:X:X:O:O:X:X:O:O:"));
        callback(results);
    }

    #endregion
}

Now in your unit test, you can simply pass in a Mock or a test double of IGameStorerProvider to do what you want to do in your ViewModel under test.

GameView / GameViewModel

At its most basic level, the GameView simple allows users to play the computer in a game of O's and X's. When the game ends, the winner (or draw) is stored to IsolatedStorage. The user may choose to play again using the "Play Again" Image. When a winner is found, the GameView shows a new VisualState which does a little animation for the Winner text, using the winner's name (either computer or the UserName typed in, in the first UserNameEntryView).

A lot of the code in the GameViewModel is about the logic to determine who the winner was, and managing the game state, which strictly has nothing to do with Cinch, so I will not be writing about that, I will just concentrate on the bits that cover the goodies from the Cinch framework.

Let's start with the "Play Again" Image. If we examine the XAML, we can see that this uses the Cinch EventToCommandTrigger to fire a Cinch SimpleCommand when a MouseDown event occurs on the Image.

<Image Height="30" Width="30" Margin="5,2,5,2"
            Source="/CinchV2DemoSL;component/Images/repeat.png" 
            VerticalAlignment="Center">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseLeftButtonDown">
            <CinchV2:EventToCommandTrigger 
                Command="{Binding RestartCommand}" 
                CommandParameter="5"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Image>

And this is what the relevant parts of the GameViewModel look like to support this SimpleCommand:

using System;
using System.Collections.Generic;
using System.Windows.Input;
using System.ComponentModel.Composition;
using System.ComponentModel;
using System.Linq;

using Cinch;
using MEFedMVVM.ViewModelLocator;

namespace CinchV2DemoSL
{
    [ExportViewModel("GameViewModel")]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    public class GameViewModel : ViewModelBase
    {
        private IVSM visualStateManagerService;
        private IViewAwareStatus viewAwareStatusService;
        private IGameStorerProvider gameStorerProvider;

        [ImportingConstructor]
        public GameViewModel(            
            IVSM visualStateManagerService,
            IGameStorerProvider gameStorerProvider)
        {
            this.visualStateManagerService = visualStateManagerService;
            this.gameStorerProvider = gameStorerProvider;

            RestartCommand = new SimpleCommand<Object,Object>(ExecuteRestartCommand);
        }

        public SimpleCommand<Object, Object> RestartCommand { get; private set; }

        private void ExecuteRestartCommand(object o)
        {
            ....
             ....
            visualStateManagerService.GoToState("RestartedState");
        }
    }
}

It can be seen that the Execute delegate for the SimpleCommand also makes use of the IVSM service, to go to a "RestartedState" VisualState when the RestartCommand is executed. So what else does this ViewModel do?

Well, when a game is completed, it needs to be saved, which we can see in this code snippet from the GameViewModel:

private void SetWinState(string winnerText)
{
    HaveWinner = true;
    WinnerName = winnerText + " Wins";
    DisableRemainingCells();
    StoreGameState();
    visualStateManagerService.GoToState("WinOrCompletedState");
}

private void StoreGameState()
{
    string actualWinnerName = WinnerName.StartsWith(GameCellViewModel.PlayersText) ?
           IsolatedStorageHelper.ReadUserNameFromFile() + " won" : "Computer won";
    string winner = HaveWinner ? actualWinnerName : "no winner";

    string gameText = "";
    foreach (GameCellViewModel cell in gameCells)
    {
        gameText += cell.CellText + ":";
    }
    gameText.TrimEnd(":".ToCharArray());

    gameStorerProvider.StoreGameResults(actualWinnerName, gameText);

    Mediator.Instance.NotifyColleagues<bool>("RefreshGameStats", true);
}

There are a couple of things of note in there. Firstly, in the StoreGameState() method, we are making use of the IGameStorerProvider non-core service that we discussed earlier, to store the results to a IsolatedStorageFile. We are also making use of the IVSM service to push the GameView into a "WinOrCompletedState" VisualState when the game is deemed to be complete.

There is one more thing of note, in that at runtime, both the GameView and the ListGameStatView are within a single TabControl, so every time a new game is completed within the GameViewModel, a Mediator message is sent to the ListGameStatViewModel to update its winner's data using the runtime IGameStorerProvider, which is shown through bindings on the ListGameStatView.

ListGameStatView / ListGameStatViewModel

The ListGameStatView really just shows a List<GameStateViewModel> of who won a particular game of O's and X's. There is not really much to say about this ViewModel apart from the fact that it makes use of the IGameStorerProvider to get data for creating the List<GameStateViewModel> from. Here is the ListGameStateViewModel in its entirety. Please forgive the method mentioning images that was copied from the WPF demo, I forgot to rename it, the intention should still be clear.

using System;
using System.Collections.Generic;
using System.Windows.Input;
using System.ComponentModel.Composition;
using System.ComponentModel;
using System.Linq;
using System.Collections.ObjectModel;

using Cinch;
using MEFedMVVM.ViewModelLocator;

namespace CinchV2DemoSL
{
    /// <summary>
    /// Game Stat List ViewModel
    /// Demonstrates CinchV2 CIViewAwareStatus service/Mediator
    /// MeffedMVVM service injection, along with a utility
    /// service that is used at both design/runtime
    /// </summary>
    [ExportViewModel("ListGameStatViewModel")]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    public class ListGameStatViewModel : ViewModelBase
    {
        #region Data
        private ObservableCollection<GameStatViewModel> gameStats=
            new ObservableCollection<GameStatViewModel>();
        private IGameStorerProvider gameStorerProvider;
        private IViewAwareStatus viewAwareStatusService;
        #endregion

        #region Ctor
        [ImportingConstructor]
        public ListGameStatViewModel(
            IGameStorerProvider gameStorerProvider,
            IViewAwareStatus viewAwareStatusService)
        {
            this.gameStorerProvider = gameStorerProvider;
            this.viewAwareStatusService = viewAwareStatusService;
            this.viewAwareStatusService.ViewLoaded += viewAwareStatusService_ViewLoaded;
            Mediator.Instance.Register(this);
        }
        #endregion

        #region Mediator Message Sinks
        [MediatorMessageSinkAttribute("RefreshGameStats")]
        public void RefreshGameStatsMessageSink(bool dummy)
        {
            FetchData();
        }
        #endregion

        #region Public Methods/Properties
        /// <summary>
        /// GameStats
        /// </summary>
        static PropertyChangedEventArgs gameStatsChangeArgs =
            ObservableHelper.CreateArgs<ListGameStatViewModel>(x => x.GameStats);

        public ObservableCollection<GameStatViewModel> GameStats
        {
            get { return gameStats; }
            set
            {
                gameStats = value;
                NotifyPropertyChanged(gameStatsChangeArgs);
            }
        }
        #endregion

        #region Private Methods
        private void viewAwareStatusService_ViewLoaded()
        {
            FetchData();
        }

        private void FetchData()
        {
            gameStorerProvider.FetchGameResults(LoadImagesFromRetrievedData);
        }


        private void LoadImagesFromRetrievedData(List<Tuple<string, string>> data)
        {
            ObservableCollection<GameStatViewModel> newStats = 
                 new ObservableCollection<GameStatViewModel>();
            foreach (Tuple<String, String> result in data)
            {
                newStats.Add(new GameStatViewModel(result.Item1, result.Item2));
            }
            GameStats = newStats;
        }

        #endregion

        #region Overrides
        protected override void OnDispose()
        {
            base.OnDispose();
            Mediator.Instance.Unregister(this);
        }
        #endregion

    }
}

One thing to note is that because this ViewModel makes use of the IGameStorerProvider, design time data can be supplied. Let's have a quick look at that. Recall, I had a design time IGameStorerProvider service implementation like this:

/// <summary>
/// Runtime Data service used to store/retrieve game data
/// </summary>
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.DesignTime, typeof(IGameStorerProvider))]
public class DesignTimeGameStorerProvider : IGameStorerProvider
{
    #region IGameStorerProvider Members

    public void StoreGameResults(string winnerName, string completeGameText)
    {
        //do nothing
    }

    public void WriteUserNameToFile(string username)
    {
        //do nothing
    }

    public string ReadUserNameFromFile()
    {
        return "Sacha";
    }

    public void FetchGameResults(Action<List<Tuple<string, string>>> callback)
    {
        List<Tuple<string, string>> results = 
                  new List<Tuple<string, string>>();
        results.Add(new Tuple<string,string>("Sacha won","O:X:X:O:O:X:X:O:O:"));
        results.Add(new Tuple<string,string>("Computer won","O:O:O:X:X:X:X:O:O:"));
        results.Add(new Tuple<string,string>("Sacha won","O:X:X:O:O:X:X:O:O:"));
        callback(results);
    }

    #endregion
}

Which results in this in Blend:

Where it uses the UserName that was entered on the UserNameEntryView / UserNameEntryViewModel.

There is one more thing of note, in that at runtime, both the GameView and the ListGameStatView are within a single TabControl, so every time a new game is completed within the GameViewModel, a Mediator message is sent to the ListGameStatViewModel to update its winner's data using the runtime IGameStorerProvider, which is shown through bindings on the ListGameStatView.

The ListGameStatViewModel holds a list of individual GameStatViewModels, each of which can be used to show the PlayedGameChildWindow ChildWindow. There is not much to say about the GameStatView/GameStatViewModel, the view really only contains a DataTemplate that has a single button that fires a Cinch SimpleCommand<T1,T2> to show the previously saved game state that was stored in IsolatedStorage (using the IGameStorerProvider service). So let's just have a look at the SimpleCommand<T1,T2> that is used to show the PlayedGameChildWindow ChildWindow, as that is the only really important bit of functionality within the GameStatView/GameStatViewModel; here it is:

private void ExecuteViewGameCommand(object o)
{
    bool? dialogResult = null;
    ChildWindowService.Show("PlayedGameChildWindow", 
        new PlayedGameViewModel(GameText), (s, e) =>
    {
        dialogResult = e.Result;
        string result = dialogResult.HasValue && dialogResult.Value ? "ok" : "Cancel";
        MessageBoxService.ShowInformation("You clicked " + result);
        //you can do what you like with dialogResult
    });
}

It is important to remember that ChildWindows in Silverlight are not the same as Modal dialogs, in WPF say, they look Modal and kinda act Modal, but any code after showing a ChildWindow will continue to run. So when we show a ChildWindow, we need to ensure no more code is run. How do we deal with the modified state that the ChildWindow may have manipulated, I hear you ask. Well, quite simple, we use a callback as shown in the code snippet above. We can simply hook up a lambda there, and run some code when the ChildWindow is closed.

And the PlayedGameChildWindow ChildWindow code-behind looks like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

using Cinch;

namespace CinchV2DemoSL.ChildWindows
{
    [PopupNameToViewLookupKeyMetadata("PlayedGameChildWindow",
                                      typeof(PlayedGameChildWindow))]
    public partial class PlayedGameChildWindow : ChildWindow
    {
        public PlayedGameChildWindow()
        {
            InitializeComponent();
        }

        private void OKButton_Click(object sender, RoutedEventArgs e)
        {
            this.DialogResult = true;
        }

        private void CancelButton_Click(object sender, RoutedEventArgs e)
        {
            this.DialogResult = false;
        }
    }
}

Note: if we were using WPF, we could use the IsDefault/IsCancel properties in XAML, but as Silverlight does not support these, we must have a few code-behind handlers to close the ChildWindow.

As we just saw, the GameStatViewModel is responsible for showing the PlayedGameChildWindow ChildWindow, which it does using the IChildWindowService. Now if were to examine the XAML for the PlayedGameChildWindow ChildWindow, we will not see any MeffedMVVM attached DP in there to resolve the ViewModel, which is different from before.

<controls:ChildWindow x:Class="CinchV2DemoSL.ChildWindows.PlayedGameChildWindow"
           xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
           xmlns:controls="clr-namespace:System.Windows.Controls;
                           assembly=System.Windows.Controls"
           xmlns:local="clr-namespace:CinchV2DemoSL"                      
           Width="400" Height="400" 
           Title="Played Game" 
           Background="{StaticResource verticalTabHeaderBackground}">
    
    <Grid x:Name="LayoutRoot" Margin="2">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <ItemsControl  Grid.Row="0"
                    HorizontalAlignment="Center" 
                    VerticalAlignment="Stretch" 
                    ItemsSource="{Binding GameCells}"
                    Background="Transparent"
                    ItemTemplate="{StaticResource cellDataTemplate}">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <local:UniformGrid Background="Transparent"
                            Columns="3" Rows="3" 
                            Width="300" Height="300"/>
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
        </ItemsControl>

        <Button x:Name="CancelButton" Content="Cancel" 
                Click="CancelButton_Click" 
                Width="75" Height="23" 
                HorizontalAlignment="Right" Margin="0,12,0,0" 
                Grid.Row="1" />
        <Button x:Name="OKButton" Content="OK" 
           Click="OKButton_Click" Width="75" 
           Height="23" HorizontalAlignment="Right" 
           Margin="0,12,79,0" Grid.Row="1" />
    </Grid>
</controls:ChildWindow>

The main reason being that ChildWindows 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 ChildWindow, the caller (parent ViewModel) has all the changes that were done in the ChildWindow 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 ChildWindow ViewModel from the parent ViewModel, then push the newed up ViewModel into the ChildWindow using the IChildWindowService. 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

Special Note

This is my 100th article here at CodeProject, so I just thought I would ask that if you appreciate the articles I have brought you up until now, some nice votes would be most welcome. Anyway it's up to you, but they would be most welcome, a nice way to mark my 100th article.

That's It ....For Now

That is all I wanted to say for now, I can now get back to working on loads of other stuff that I have ear marked for myself. I may do a few enhancements (there are three people that have asked for), but they are enhancements, not bugs, so not vital, I may or may not do them.

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 them 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