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

Cinch V2: Version 2 of my Cinch MVVM framework: Part 3 of n

Rate me:
Please Sign up or sign in to vote.
4.96/5 (42 votes)
1 Jan 2011CPOL30 min read 172.2K   37   97
If Jack Daniels made MVVM frameworks.

Table of Contents

Introduction

The last time we talked about Cinch V2 services. In this article, we will examine what is brand new to Cinch V2, and where appropriate, I will show you if it is replacing some Cinch V1 functionality.

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 AreaClass NameWPFSilverlight (4 or above)Both
Business objectsEditableValidatingObject.cs  Yes
Business objectsValidatingObject.cs  Yes
Business objectsDataWrapper.cs  Yes
CommandsEventToCommandArgs.cs  Yes
CommandsSimpleCommand.cs  Yes
CommandsWeakEventHandlerManager.cs  Yes
EventsCloseRequestEventArgs.cs  Yes
EventsUICompletedEventArgs.cs  Yes
WeakEventsWeakEvent.cs  Yes
WeakEventsWeakEventHelper.cs  Yes
WeakEventsWeakEventProxy.cs  Yes
Extension MethodsDispatcherExtensions.csYes  
Extension MethodsGenericListExtensions.cs Yes 
Interactivity ActionsCommandDrivenGoToStateAction.cs  Yes
Interactivity BehavioursFocusBehaviourBase.csYes  
Interactivity BehavioursNumericTextBoxBehaviour.csYes  
Interactivity BehavioursSelectorDoubleClickCommandBehavior.csYes  
Interactivity BehavioursTextBoxFocusBehavior.csYes  
Interactivity TriggersCompletedAwareCommandTrigger.cs  Yes
Interactivity TriggersCompletedAwareGotoStateCommandTrigger.cs  Yes
Interactivity TriggersEventToCommandTrigger.cs  Yes
Messager MediatorMediatorMessageSinkAttribute.cs  Yes
Messager MediatorMediatorSingleton.cs  Yes
Services ImplementationChildWindowService.cs Yes 
Services ImplementationSLMessageBoxService.cs Yes 
Services ImplementationViewAwareStatus.cs  Yes
Services ImplementationViewAwareStatusWindow.csYes  
Services ImplementationVSMService.cs  Yes
Services ImplementationWPFMessageBoxService.csYes  
Services ImplementationWPFOpenFileService.csYes  
Services ImplementationWPFSaveFileService.csYes  
Services ImplementationWPFUIVisualizerService.csYes   
Services InterfacesIChildWindowService.cs Yes 
Services InterfacesIMessageBoxService.cs Yes 
Services InterfacesIViewAwareStatus.cs  Yes
Services InterfacesIViewAwareStatusWindow.csYes  
Services InterfacesIVSM.cs  Yes
Services InterfacesIMessageBoxService.csYes  
Services InterfacesIOpenFileService.csYes  
Services InterfacesISaveFileService.csYes  
Services InterfacesIUIVisualizerService.csYes  
Services Test ImplementationsTestChildWindowService.cs Yes 
Services Test ImplementationsTestMessageBoxService.cs Yes 
Services Test ImplementationsTestViewAwareStatus.cs  Yes
Services Test ImplementationsTestViewAwareStatusWindow.csYes  
Services Test ImplementationsTestVSMService.cs  Yes
Services Test ImplementationsTestMessageBoxService.csYes  
Services Test ImplementationsTestOpenFileService.csYes  
Services Test ImplementationsTestSaveFileService.csYes  
Services Test ImplementationsTestUIVisualizerService.csYes  
ThreadingAddRangeObservableCollection.cs (this is a specific Silverlight implementation) Yes 
ThreadingAddRangeObservableCollection.cs (this is a specific WPF implementation)Yes  
ThreadingBackgroundTaskManager.cs  Yes
ThreadingISynchronizationContext.cs  Yes
ThreadingUISynchronizationContext.cs  Yes
ThreadingApplicationHelper.csYes  
ThreadingDispatcherNotifiedObservableCollection.csYes  
MenusCinchMenuItem.cs  Yes
UtilitiesArgumentValidator.cs  Yes
UtilitiesIWeakEventListener.cs (this is a System class missing from Silverlight, so I created it) Yes 
UtilitiesObservableHelper.cs  Yes
UtilitiesPropertyChangedEventManager.cs (this is a System class missing from Silverlight, so I created it) Yes 
UtilitiesPropertyObserver.cs  Yes
UtilitiesBindingEvaluator.csYes  
UtilitiesObservableDictionary.csYes  
UtilitiesTreeHelper.csYes  
ValidationRegexRule.cs  Yes
ValidationRule.cs  Yes
ValidationSimpleRule.cs  Yes
ViewModelsEditableValidatingViewModelBase.cs  Yes
ViewModelsIViewStatusAwareInjectionAware.cs  Yes
ViewModelsValidatingViewModelBase.cs  Yes
ViewModelsViewMode.cs  Yes
ViewModelsViewModelBase.cs  Yes
ViewModelsViewModelBaseSLSpecific.cs Yes 
ViewModelsViewModelBaseWPFSpecific.csYes  
WorkspacesChildWindowResolver.cs Yes 
WorkspacesCinchBootStrapper.cs (Silverlight version) Yes 
WorkspacesCinchBootStrapper.cs (WPF version)Yes  
WorkspacesPopupNameToViewLookupKeyMetadataAttribute.cs  Yes
WorkspacesIWorkspaceAware.csYes  
WorkspacesMockView.csYes  
WorkspacesNavProps.csYes  
WorkspacesPopupResolver.csYes  
WorkspacesViewnameToViewLookupKeyMetadataAttribute.csYes  
WorkspacesViewResolver.csYes  
WorkspacesWorkspaceData.csYes  

Now that I have shown you what classes will work with WPF/Silverlight, 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.

Cinch V1 Article Links

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

Cinch V2 Article Links

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

New Stuff for Cinch V2

Now we can get into the guts of this article which is really the new stuff that has been added to Cinch V2; some of it is a rewrite of Cinch V1 stuff that has been rewritten to current best practices, such as using Blend interactivity.

SimpleCommand

This is a rewrite of the SimpleCommand that was available in Cinch V1.

So what's changed? Well, quite a bit actually:

  • I have added two constructors to it to make it trivial to declare a new SimpleCommand.
  • I have added a Weak CommandCompleted event (this is incredibly useful, you will see more on this in a bit).
  • I have added two generic parameters to it, such that the ICommand.CanExecute parameter and the ICommand.Execute parameters can be declared as having different parameter types.

So what does the new SimpleCommand<T1,T2> look like:

C#
using System.Windows.Input;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

namespace Cinch
{
    /// <summary>
    /// Interface that is used for ICommands that notify when they are
    /// completed
    /// </summary>
    public interface ICompletionAwareCommand
    {
        /// <summary>
        /// Notifies that the command has completed
        /// </summary>
        WeakActionEvent<object> CommandCompleted { get; set; }
    }
 
    /// <summary>
    /// Simple delegating command, based largely on DelegateCommand from PRISM/CAL
    /// </summary>
    /// <typeparam name="T1">The type for the ICommand.CanExecute() parameter</typeparam>
    /// <typeparam name="T2">The type for the ICommand.Execute() parameter</typeparam>
    public class SimpleCommand<T1,T2> : ICommand, ICompletionAwareCommand
    {
        private Func<T1, bool> canExecuteMethod;
        private Action<T2> executeMethod;
        private WeakActionEvent<object> commandCompleted;
 
        public SimpleCommand(Func<T1, bool> canExecuteMethod, Action<T2> executeMethod)
        {
            this.executeMethod = executeMethod;
            this.canExecuteMethod = canExecuteMethod;
            this.CommandCompleted = new WeakActionEvent<object>();
        }
 
        public SimpleCommand(Action<T2> executeMethod)
        {
            this.executeMethod = executeMethod;
            this.canExecuteMethod = (x) => { return true; };
            this.CommandCompleted = new WeakActionEvent<object>();
        }
       
        public WeakActionEvent<object> CommandCompleted { get; set;}
 
        public bool CanExecute(T1 parameter)
        {
            if (canExecuteMethod == null) return true;
            return canExecuteMethod(parameter);
        }
 
        public void Execute(T2 parameter)
        {
            if (executeMethod != null)
            {
                executeMethod(parameter);
            }
 
            //now raise CommandCompleted for this ICommand
            WeakActionEvent<object> completedHandler = CommandCompleted;
            if (completedHandler != null)
            {
                completedHandler.Invoke(parameter);
            }
        }
 
        public bool CanExecute(object parameter)
        {
            return CanExecute((T1)parameter);
        }
 
        public void Execute(object parameter)
        {
            Execute((T2)parameter);
        }
 
#if SILVERLIGHT
        /// <summary>
        /// Occurs when changes occur that affect whether the command should execute.
        /// </summary>
        public event EventHandler CanExecuteChanged;
#else
        /// <summary>
        /// Occurs when changes occur that affect whether the command should execute.
        /// </summary>
        public event EventHandler CanExecuteChanged
        {
            add
            {
                if (canExecuteMethod != null)
                {
                    CommandManager.RequerySuggested += value;
                }
            }
 
            remove
            {
                if (canExecuteMethod != null)
                {
                    CommandManager.RequerySuggested -= value;
                }
            }
        }
#endif
        /// <summary>
        /// Raises the <see cref="CanExecuteChanged" /> event.
        /// </summary>
        [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic",
            Justification = "The this keyword is used in the Silverlight version")]
        [SuppressMessage("Microsoft.Design", "CA1030:UseEventsWhereAppropriate",
            Justification = "This cannot be an event")]
        public void RaiseCanExecuteChanged()
        {
#if SILVERLIGHT
            var handler = CanExecuteChanged;
            if (handler != null)
            {
                handler(this, EventArgs.Empty);
            }
#else
            CommandManager.InvalidateRequerySuggested();
#endif
        }
    }
}

Some of the more eagle eyed amongst you may have noticed a CommandCompleted WeakActionEvent, which we will discuss in the next section.

So how do we now use these new improved SimpleCommand<T1,T2>? Well, it is actually quite simple now, all you need to do is something like this:

Step 1: Add a Property for the Command

This is how you could expose a SimpleCommand<T1,T2> property from your ViewModel:

C#
public SimpleCommand<Object, Object> OpenExistingFileCommand { get; private set; }

Step 2 : Construct a New Command

And here is how you would construct the actual command (this example assumes the command can always execute, as such no CanExecute delegate is supplied):

C#
OpenExistingFileCommand = new SimpleCommand<Object, Object>(ExecuteOpenExistingFileCommand);

Step 3 : Add the Command Methods

And here is an example of what an Execute method may look like:

C#
private void ExecuteOpenExistingFileCommand(Object args)
{
    .....
    .....
}

Actions/Triggers

Now I am a massive fan of attached DPs, but I am also willing to roll over and play the game when something better comes along. And a while back, something better did come along by way of the Blend Interactity.dll, which contains base classes for Actions/Behaviours/Triggers, all of which have kind of come from what a lot of people were already doing with Attached DPs. Basically, the Blend Interactity.dll contains base classes that mimic what the WPF/Silverlight community were already doing with Attached DPs, just formalised into a pattern, that allows Blend users to simply drag on these classes to the design surface and change a few properties, and bingo...magic occurs.

Anyhow, all that said, I used to have a bunch of Attached DPs in Cinch V1, and by and large, these have just morphed their way into Blend Interactivity Actions/Behaviours/Triggers, but there are some new ones in here too.

You can access these new Cinch V2 Actions/Behaviours/Triggers using the standard Blend Assets tab:

Image 1

So let's continue and see what Cinch V2 provides for us.

Behaviours

Cinch V2 provides the following Behaviours:

NumericTextBoxBehaviour (WPF only, Was also available in Cinch V1 as Attached DP)

This is a pretty standard Behaviour that simply makes a TextBox only accept numeric data.

Here is how you would use it in your XAML:

XML
<TextBox Text="{Binding ImageRating.DataValue, UpdateSourceTrigger=LostFocus, 
        ValidatesOnDataErrors=True, ValidatesOnExceptions=True}"
         Style="{StaticResource ValidatingTextBox}"
         IsEnabled="{Binding ImageRating.IsEditable}">
    <i:Interaction.Behaviors>
        <CinchV2:NumericTextBoxBehaviour/>
    </i:Interaction.Behaviors>
</TextBox>
SelectorDoubleClickCommandBehavior (WPF only, was also available in Cinch V1 as Attached DP)

This is also a pretty standard behaviour that can be used to fire a ViewModel Command whenever a Selector item is double clicked; although the WPF demo app does not have an example of this, this is how you would use it within XAML:

XML
<ListView ItemsSource="{Binding People}" IsSynchronizedWithCurrentItem="True">
    <i:Interaction.Behaviors>
    <CinchV2:SelectorDoubleClickCommandBehavior Command="{Binding SomeViewModelCommand}" />
    </i:Interaction.Behaviors>
</TextBox>

If you don't care about the EventArgs making their way into the ViewModel, you can simply declare the ViewModel code like this:

C#
//declare command  
public SimpleCommand<Object, Object> SelectorDoubleClickCommand { get; private set; }
 
//initalise command
SelectorDoubleClickCommand = 
  new SimpleCommand<Object, Object>(ExecuteSelectorDoubleClickCommand);
 
//command handler
private void ExecuteSelectorDoubleClickCommand(Object args)
{
   //do something
}

If however you want to know about the EventArgs that caused the Command to fire, you would do something like this in your ViewModel:

C#
//Command property in ViewModel
public SimpleCommand<Object, EventToCommandArgs> 
          SelectorDoubleClickCommand { get; private set; }
 
//Where you setup your Command in your ViewModel
SelectorDoubleClickCommand = 
  new SimpleCommand<Object, EventToCommandArgs>(ExecuteSelectorDoubleClickCommand);
 
 
//Command handlers
private void ExecuteSelectorDoubleClickCommand(EventToCommandArgs args)
{
    ICommand commandRan = args.CommandRan;
    Object o = args.CommandParameter; //get command parameter
    EventArgs ea = args.EventArgs; //get event args
    var sender = args.Sender; //get orginal sender (ListView in this case)
}

It can be seen that you can get all the relevant information, such as:

  • The Command parameter
  • The EventArgs
  • The sender (source of the event, ListView in this case)

Note: I am not an advocate of having UI type objects in my ViewModel, as it is harder to test, but some folk love it, so I do provide that within the args.EventArgs / args.Sender objects, but I have to say I have never had to do this sort of thing in any ViewModel code I have ever written. So use it at your peril, don't blame me when you can't test your ViewModel properly, I warned you.

TextBoxFocusBehavior (WPF only)

Cinch V2 also provides a way for ViewModels to set focus within a View. The basic idea is this:

  1. The ViewModelBase class has a public method called RaiseFocusEvent(String focusProperty) which raises a ViewModelBase event called FocusRequested
  2. Then there is a Blend Behaviour called TextBoxFocusBehavior, which listens for the ViewModelBase FocusRequested event
  3. When the TextBoxFocusBehavior sees the ViewModelBase FocusRequested event fire, it sees if the Behaviour target FrameworkElements (TextBox) binding matches the requested property name, and if it does, focus is moved to the Behaviour target object (TextBox)

So what does the code look like? Well, starting with the ViewModelBase class, there is this code:

C#
public event Action<String> FocusRequested;
 
/// <summary>
/// Raises the Focus Requested event
/// </summary>
/// <param name="focusProperty"></param>
public void RaiseFocusEvent(String focusProperty)
{
    FocusRequested(focusProperty);
}

And then we have something like this in the TextBoxFocusBehavior:

C#
public class TextBoxFocusBehavior : FocusBehaviorBase
{
#region Protected Methods
protected DependencyProperty GetSourceProperty()
{
    //As this is a TextBox we use TextBox.TextProperty
    return TextBox.TextProperty;
}
#endregion
 
#region Overrides
protected override void OnAttached()
{
 
    if (!(AssociatedObject is TextBoxBase))
        return;
 
    base.OnAttached();
 
    AssociatedObject.Loaded += AssociatedObject_Loaded;
}
 
void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
    if (AssociatedObject.DataContext is ViewModelBase)
        ((ViewModelBase)AssociatedObject.DataContext).FocusRequested += 
                 TextBoxFocusBehavior_FocusRequested;
}
 
protected override void OnDetaching()
{
    base.OnDetaching();
    AssociatedObject.Loaded -= AssociatedObject_Loaded;
    if (AssociatedObject.DataContext is ViewModelBase)
        ((ViewModelBase)AssociatedObject.DataContext).FocusRequested -= 
               TextBoxFocusBehavior_FocusRequested;
}
#endregion
 
#region Private Methods
private void TextBoxFocusBehavior_FocusRequested(String propertyPath)
{
  Binding binding = BindingOperations.GetBinding(
                      AssociatedObject, GetSourceProperty());
  base.ConductFocusOnElement(binding,propertyPath, IsUsingDataWrappers);
}
#endregion
 
#region DPs
 
#region IsUsingDataWrappers
 
/// <summary>
/// IsUsingDataWrappers Dependency Property
/// </summary>
public static readonly DependencyProperty IsUsingDataWrappersProperty =
    DependencyProperty.Register("IsUsingDataWrappers", 
    typeof(bool), typeof(TextBoxFocusBehavior),
    new FrameworkPropertyMetadata((bool)false));
 
/// <summary>
/// Gets or sets the IsUsingDataWrappers property.
/// </summary>
public bool IsUsingDataWrappers
{
    get { return (bool)GetValue(IsUsingDataWrappersProperty); }
    set { SetValue(IsUsingDataWrappersProperty, value); }
}
 
#endregion
 
#endregion
 
}

You can see that this Blend Behaviour supports DataWrappers too, you just need to specify if the TextBox you are applying this Behaviour to is bound to a DataWrapper. You may also notice that this class delegates some work to a base class, that is where the actual focus work takes place, so let's have a look at that too.

C#
/// <summary>
/// Provides a focus behaviour base class that attempts
/// to focus elements by matching their bound property paths
/// with a input propertyPath string 
/// </summary>
public abstract class FocusBehaviorBase : Behavior<FrameworkElement>
{
    #region Protected Methods
    /// <summary>
    /// Attempts to force focus to the bound property with the same propertyPath
    /// as the propertyPath input
    /// </summary>
    /// <param name="elementBinding">Binding to evaluate</param>
    /// <param name="propertyPath">propertyPath to try and find finding for</param>
    /// <param name="isUsingDataWrappers">shoul be true if the property is bound to a 
    /// <c>Cinch.DataWrapper</c></param>
    protected virtual void ConductFocusOnElement(Binding elementBinding, 
        String propertyPath, bool isUsingDataWrappers)
    {
        if (elementBinding == null)
            return;
 
        if (isUsingDataWrappers)
        {
            if (!elementBinding.Path.Path.Contains(propertyPath))
                return;
        }
        else
        {
            if (elementBinding.Path.Path != propertyPath)
                return;
        }
 
        // Delay the call to allow the current batch
        // of processing to finish before we shift focus.
        AssociatedObject.Dispatcher.BeginInvoke((Action)delegate
        {
 
            if (!AssociatedObject.Focus())
            {
                DependencyObject fs = FocusManager.GetFocusScope(AssociatedObject);
                FocusManager.SetFocusedElement(fs, AssociatedObject);
            }
        },
        DispatcherPriority.Background);
    }
    #endregion
}

See how this also deals with DataWrappers. Anyway, that is the internals; how might we use it? Quite simply really.

In your ViewModel, whenever you want to set focus for a TextBox, do something like:

C#
RaiseFocusEvent("ImageRating");

And in your XAML for your TextBox, have something like this:

XML
<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" />
        </i:Interaction.Behaviors>
</TextBox>

If the property your TextBox is bound to is not a DataWrapper property, simply set IsUsingDataWrapper="false".

Triggers

Cinch V2 provides the following Triggers.

CompletedAwareCommandTrigger

Most WPF/Silverlight users will be well used to the idea of calling Commands in ViewModels from their Views. But occasionally, what you need is the opposite, you need the ViewModel to tell the View to do something. In Cinch V2, I have provided for this mechanism, by providing a SimpleCommand that fires a CommandCompleted event when it has run its Execute delegate. This command can then be used as a Trigger to run some Blend Actions inside a View.

Cinch V2 goes a step further and provides a Blend Trigger that is expecting to be bound to a SimpleCommand that has a CommandCompleted event, and will run the Triggers Actions when the CommandCompleted event fires.

Here is the full code for the Trigger:

C#
public class CompletedAwareCommandTrigger : TriggerBase<FrameworkElement>
{
    #region DPs
 
    #region Command
    public static readonly DependencyProperty CommandProperty =
        DependencyProperty.Register("Command", typeof(ICompletionAwareCommand),
            typeof(CompletedAwareCommandTrigger), null);
 
    /// <summary>
    /// Gets or sets the Command property. 
    /// </summary>
    public ICompletionAwareCommand Command
    {
        get { return (ICompletionAwareCommand)GetValue(CommandProperty); }
        set { SetValue(CommandProperty, value); }
    }
    #endregion
 
    #endregion
 
    #region Overrides
    /// <summary>
    /// Called after the behavior is attached to an AssociatedObject.
    /// </summary>
    /// <remarks>
    /// Override this to hook up functionality to the AssociatedObject.
    /// </remarks>
    protected override void OnAttached()
    {
        base.OnAttached();
        this.Command.CommandCompleted += Command_Completed;
 
    }
 
    protected override void OnDetaching()
    {
        base.OnDetaching();
        this.Command.CommandCompleted -= Command_Completed;
    }
    #endregion
 
    #region Private Method
    private void Command_Completed(object parameter)
    {
 
        // Invoke the actions
        InvokeActions(parameter);
    }
    #endregion
}

And this is how we might use this in some XAML:

XML
<CinchV2:CompletedAwareCommandTrigger 
    Command="{Binding ShowActionsCommandReversed}">
    <ei:GoToStateAction StateName="ShowActionsState"/>
</CinchV2:CompletedAwareCommandTrigger>

This example uses a standard Blend GoToStateAction but this could be any Action that you like that you want to trigger based on a SimpleCommand in the ViewModel Completing:

And this is what some ViewModel code might look like:

C#
//public property for the View to bind to
public SimpleCommand<Object, Object> ShowActionsCommandReversed { get; private set; }
  
// initialise the Command, do nothing with its Execute delegate
// 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) => { });
 
//Fire the command from the ViewModel, so the Views
//trigger can carry out any Actions associated 
//with this command completing
ShowActionsCommandReversed.Execute(null);
CompletedAwareGotoStateCommandTrigger

I don't know how many of you know this, but the standard VisualStateManager that can be used to programmatically go to a particular VisualState will only work when the VisualStateGroup that contains the VisualState you are trying to go to is contained directly under the root element in your VisualTree.

In fact, this is a fair assumption, as that is the way the standard Blend GoToStateAction is expecting to work; if you look at the VisulStateManager.GoToState, you will see it only accepts a control as shown below:

Image 2

But sometimes you may require your VisualStateGroups to not be directly under the root node in your VisualTree (OK it is rare, but it does happen), and the FrameworkElement you might want them in may or may not be a control.

Now I don't think many people know this, but there is also a ExtendedVisualStateManager that can work with any FrameworkElement; as such, Cinch V2 provides a Blend trigger that can work with a Cinch SimpleCommand<T1,T2> CommandCompleted event to make use of the ExtendedVisualStateManager or the standard VisualStateManager

Note: I do not expect this to be used that much, but here is how one might use it anyway.

This is what the complete code for the CompletedAwareGotoStateCommandTrigger looks like:

C#
public class CompletedAwareGoToStateCommandTrigger : TriggerBase<FrameworkElement>
{
    #region DPs
 
    #region Command
    public static readonly DependencyProperty CommandProperty =
        DependencyProperty.Register("Command", typeof(ICompletionAwareCommand),
            typeof(CompletedAwareGoToStateCommandTrigger), null);
 
    /// <summary>
    /// Gets or sets the Command property. 
    /// </summary>
    public ICompletionAwareCommand Command
    {
        get { return (ICompletionAwareCommand)GetValue(CommandProperty); }
        set { SetValue(CommandProperty, value); }
    }
    #endregion
 
    #region IsBeingUsedAtRootLevel
#if !SILVERLIGHT
    public static readonly DependencyProperty IsBeingUsedAtRootLevelProperty =
        DependencyProperty.Register("IsBeingUsedAtRootLevel", typeof(bool),
            typeof(CompletedAwareGoToStateCommandTrigger), new UIPropertyMetadata(false));
#else
    public static readonly DependencyProperty IsBeingUsedAtRootLevelProperty =
        DependencyProperty.Register("IsBeingUsedAtRootLevel", typeof(bool),
            typeof(CompletedAwareGoToStateCommandTrigger), new PropertyMetadata(false));
#endif
 
    /// <summary>
    /// Gets or sets the IsBeingUsedAtRootLevel property. 
    /// </summary>
    public bool IsBeingUsedAtRootLevel
    {
        get { return (bool)GetValue(IsBeingUsedAtRootLevelProperty); }
        set { SetValue(IsBeingUsedAtRootLevelProperty, value); }
    }
    #endregion
    #endregion
 
    #region Overrides
    /// <summary>
    /// Called after the behavior is attached to an AssociatedObject.
    /// </summary>
    /// <remarks>
    /// Override this to hook up functionality to the AssociatedObject.
    /// </remarks>
    protected override void OnAttached()
    {
        base.OnAttached();
        this.Command.CommandCompleted += Command_Completed;
    }
 
 
    protected override void OnDetaching()
    {
        base.OnDetaching();
        this.Command.CommandCompleted -= Command_Completed;
    }
    #endregion
 
    #region Private Methods
    private void Command_Completed(object parameter)
    {
 
        if (IsBeingUsedAtRootLevel)
        {
            // Invoke the actions
            InvokeActions(parameter);
        }
        else
        {
            if (VisualStateManager.GetVisualStateGroups(
                     this.AssociatedObject).Count > 0)
            {
                ExtendedVisualStateManager.GoToElementState(
                      this.AssociatedObject, (string)parameter, true);
            }
        }
    }
    #endregion
}

And this is what your XAML might look like (although the demo app doesn't include this, I have tried it outside of the demo apps and it does work):

XML
<TabControl x:Name="tabControl">
    <i:Interaction.Triggers>
        <CinchV2:CompletedAwareGoToStateCommandTrigger 
            IsBeingUsedAtRootLevel="True"
            Command="{Binding GoToStateCommand}">
            <CinchV2:CommandDrivenGoToStateAction 
            TargetObject="{Binding ElementName=tabControl}"/>
        </CinchV2:CompletedAwareGoToStateCommandTrigger>
    </i:Interaction.Triggers>
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="VisualStateGroup">
            <VisualState x:Name="GreenState">
                
            </VisualState>
            <VisualState x:Name="BlueState">
                
            </VisualState>
        </VisualStateGroup>
</VisualStateManager.VisualStateGroups>
                 
.....rest of code
.....rest of code
.....rest of code
</TabControl>

It can be seen that you can tell it whether it is being used at root level within the VisualTree, in which case it will use a standard GoToStateAction, or else it will use the ExtendedVisualStateManager.

And this is what some demo ViewModel code might look like:

C#
public SimpleCommand<String, String> GoToStateCommand { get; private set; }
             
GoToStateCommand = new SimpleCommand<String, String>(
    (parameter) => { return !string.IsNullOrEmpty(parameter); },
        (input)=> {});
     
//WHICH ALLOWS YOUR TO GO TO A NEW VISUAL STATE AS EASILY AS THIS IN YOUR VIEWMODEL
     
GoToStateCommand.Execute("BlueState");
EventToCommandTrigger

As the name suggests, this Trigger provides an event to command functionality; that is, it will fire a ViewModel based Command when a certain event occurs. Now, some might argue that there is support for this already by simply using the standard Blend Actions/Triggers. Whilst that is partially true, the standard Blend ones lack the ability to disable the consuming FrameworkElement when the Command can not execute. This is something that Cinch V2 does offer over the standard Blend Actions/Trigger combination.

So here is what you might have in your ViewModel:

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

And then in your View, you might have something like this:

XML
<Label Content="Show Actions">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="MouseLeftButtonUp">
            <CinchV2:EventToCommandTrigger 
                    Command="{Binding ShowActionsCommand}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Label>

And here is what this might look like in Expression Blend:

Image 3

All you have to to do is enter a custom binding and enter the Command Binding as shown above, obviously replacing the Command for your own command in your ViewModel.

If you don't care about the EventArgs making their way into the ViewModel, you can simply declare the ViewModel code like this:

C#
//declare command  
public SimpleCommand<Object, Object> ShowActionsCommand { get; private set; }
 
//initalise command
ShowActionsCommand = new SimpleCommand<Object, Object>(ExecuteShowActionsCommand);
 
//command handler
private void ExecuteShowActionsCommand(Object args)
{
   ShowActionsCommandReversed.Execute(null);
}

If however you want to know about the EventArgs that caused the Command to fire, you would do something like this in your ViewModel:

C#
//declare command  
public SimpleCommand<Object,EventToCommandArgs> 
           ViewEventToVMFiredCommand { get; private set; } }
 
//initalise command
ViewEventToVMFiredCommand = 
  new SimpleCommand<Object,EventToCommandArgs>(ExecuteViewEventToVMFiredCommand);
 
//command handler
private void ExecuteViewEventToVMFiredCommand(EventToCommandArgs args)
{
    ICommand commandRan = args.CommandRan;
    Object o = args.CommandParameter;
    EventArgs ea = args.EventArgs;
    var sender = args.Sender;
}

It can be seen that you can get all the relevant information, such as:

  • The Command parameter
  • The EventArgs
  • The sender (source of the event)

Note: I am not an advocate of having UI type objects in my ViewModel, as it is harder to test, but some folk love it, so I do provide that within the args.EventArgs / args.Sender objects, but I have to say I have never had to do this sort of thing in any ViewModel code I have ever written. So use it at your peril, don't blame me when you can't test your ViewModel properly, I warned you.

Actions

Cinch V2 provides the following Actions:

CommandDrivenGoToStateAction

This is a simple class that inherits from GoToStateAction and is expecting to be used in conjunction with a Cinch V2 CompletedAwareGoToStateCommandTrigger, which has a CommandCompleted event (which we talked about above), which you can use to supply a StateName with, using the CommandParameter in your ViewModel.

Here is an example of how you might use this:

So you would have something like this in your XAML:

XML
<TabControl x:Name="tabControl">
    <i:Interaction.Triggers>
        <CinchV2:CompletedAwareGoToStateCommandTrigger 
            IsBeingUsedAtRootLevel="True"
            Command="{Binding GoToStateCommand}">
            <CinchV2:CommandDrivenGoToStateAction 
            TargetObject="{Binding ElementName=tabControl}"/>
        </CinchV2:CompletedAwareCommandTrigger>
    </i:Interaction.Triggers>
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup x:Name="VisualStateGroup">
            <VisualState x:Name="GreenState">
                
            </VisualState>
            <VisualState x:Name="BlueState">
                
            </VisualState>
        </VisualStateGroup>
</VisualStateManager.VisualStateGroups>
                 
.....rest of code
.....rest of code
.....rest of code
</TabControl>

And you would have something like this in your ViewModel; obviously, you could also use the VisualStateManagerService as provided by Cinch V2, but as I mentioned, that only works if the VisualStateGroups are directly under the root node in the VisualTree, as the standard VisualStateManager is used.

C#
public SimpleCommand<String, String> GoToStateCommand { get; private set; }
             
GoToStateCommand = new SimpleCommand<String, String>(
    (parameter) => { return !string.IsNullOrEmpty(parameter); },
        (input)=> {});
     
//WHICH ALLOWS YOUR TO GO TO A NEW VISUAL STATE AS EASILY AS THIS IN YOUR VIEWMODEL
     
GoToStateCommand.Execute("BlueState");

Key Binding To Command

Note: In Cinch V1, there was also support for input gestures firing commands; with WPF 4, this is less than trivial, and can be achieved using the following sort of code:

XML
<Window.InputBindings>
    <KeyBinding Command="{Binding SomeCommand}" Key="F1" Modifiers="ALT"/>
</Window.InputBindings>

Workspaces

This section discusses Workspace support in Cinch V1 (OK, there was no MeffedMVVM support in V1, I mean a V1'ish approach in V2 really) and Cinch V2 proper. The workspace techniques employed by a V1'ish offering and V2 proper offer quite different support for Workspaces and design time data, so please read carefully.

ViewModel First, Ala Cinch V1 stylee

Now in Cinch V1, there was some kind of attempt at workspaces using an ObservableCollection<ViewModelBase> and marrying that up with Views using some specific DataTemplates in a resource dictionary somewhere. You can read more about this approach using this Cinch V1 article link: CinchIII.aspx#CloseVM.

Using this approach, we are assuming a ViewModel first arrangement, and the problem with this approach is that you are not really lending the ViewModel the best support it could get in order for MeffedMVVM to supply design time data. Actually, there is a way, it's just not the preferred way in Cinch V2.

So let us just have a look at what sort of design time support Cinch V2/MeffedMVVM offers the DataTemplates workspace approach that a Cinch V1'ish type app uses.

ViewModel design

The ViewModel first and DataTemplates method is still supported by Cinch V2, and MeffedMVVM can still be used to supply design time data, though the way you have to create the ExportViewModelAttribute on the ViewModel would need to be told that the ViewModel is expecting to be set directly as the DataContext for a View (say via a DataTemplate), which would be the case in a ViewModel first approach using DataTemplates as done in a Cinch V1'ish style app.

You would set the ExportViewModelAttribute on the ViewModel as shown below, and also implement a special MeffedMVVM interface called IDesignTimeAware. So taking all that into account, we might have a ViewModel something like this:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Cinch;
using MEFedMVVM;
using MEFedMVVM.ViewModelLocator;
using System.Collections.ObjectModel;
using System.ComponentModel;
using MEFedMVVM.Common;
using MEFedMVVM;
using System.ComponentModel.Composition; 
 
namespace WpfApplication1
{
    [ExportViewModel("DummyViewModel", true)]
    public class DummyViewModel : ViewModelBase, IDesignTimeAware
    {
        public DummyViewModel()
        {
        }
 
        private ObservableCollection<string> 
        data=new ObservableCollection<string>();
 
        static PropertyChangedEventArgs dataChangeArgs =
                    ObservableHelper.CreateArgs<DummyViewModel>(x => x.Data);
 
        public DummyViewModel()
        {
            this.DisplayName = "DummyViewModel";
 
            if (!Designer.IsInDesignMode)
            {
                data.Clear();
                for (int i = 0; i < 10; i++)
                {
                    data.Add(string.Format("Runtime {0}", i.ToString()));
                }
            }
        }
 
        public ObservableCollection<string> Data
        {
            get { return data; }
            set
            {
                if (data == null)
                {
                    data = value;
                    NotifyPropertyChanged(dataChangeArgs);
                }
            }
        }
 
        #region IDesignTimeAware Members
 
        public void DesignTimeInitialization()
        {
            data.Clear();
            for (int i = 0; i < 10; i++)
            {
                data.Add(string.Format("DESIGN TIME {0}", i.ToString()));
            }
        }
 
        #endregion
    }
}

Important: The [ExportViewModel("DummyViewModel", true)] line tells MeffedMVVM that this ViewModel is data aware, as it is used in some sort of DataTemplate approach.

Another thing to note is that the ViewModel implements a MeffedMVVM interface called IDesignTimeAware which allows MeffedMVVM to call the DesignTimeInitialization() method in order to supply design time data for this ViewModel. The only bad thing with this is that your ViewModel now contains code that is only used at design time. But a small price to pay, I think.

View design

The other piece of the puzzle is to use the standard MeffedMVVM Attached DP, as shown in this View, which allows MeffedMVVM to locate the ViewModel to supply design time data for:

XML
<UserControl x:Class="WpfApplication1.DummyView"
             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:MEFed="http:\\www.codeplex.com\MEFedMVVM"
             MEFed:ViewModelLocator.ViewModel="DummyViewModel"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <ListBox ItemsSource="{Binding Data}">
 
    </ListBox>
</UserControl>
ViewModel-View Matching

As I stated at the start of this section, Cinch V1 makes use of a ViewModel first paradigm, and as such, the View is created by using DataTemplates, where there is expected to be a ObservableCollection<ViewModelBase> somewhere, and some DataTemplates to match against the specific ViewModelBase instances. Something like this example that shows a View that has a ObservableCollection<ViewModelBase> bound to a TabControl:

XML
<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication1"
        Title="MainWindow" Height="350" Width="525">
    
    <Window.Resources>
 
        <DataTemplate DataType="{x:Type local:DummyViewModel}">
            <AdornerDecorator>
                    <local:DummyView />
            </AdornerDecorator>
        </DataTemplate>
 
    </Window.Resources>
 
    <TabControl ItemsSource="{Binding Path=Workspaces}" 
                DisplayMemberPath="DisplayName"/>

</Window>

Marlon talks more about this feature on his blog: http://marlongrech.wordpress.com/2010/05/23/mefedmvvm-v1-0-explained/, but basically, what Marlon does within MeffedMVVM to support this scenario is, he hooks into the View's DataContextChanged event, and this is how MeffedMVVM is able to call the DesignTimeInitialization() method in order to supply design time data.

Here is the relevant code from the MeffedMVVM codebase:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition;
using System.Windows;
 
using MEFedMVVM.Common;
using System.ComponentModel.Composition.Primitives;
 
namespace MEFedMVVM.ViewModelLocator
{
    /// <summary>
    /// This is the ViewModel initializer that ViewModel after it is set as datacontext
    /// </summary>
    public class DataContextAwareViewModelInitializer : BasicViewModelInializer
    {
        public DataContextAwareViewModelInitializer(MEFedMVVMResolver resolver)
            : base (resolver )
        { }
 
        public override void CreateViewModel(Export viewModelContext, 
            FrameworkElement containerElement)
        {
            if (!Designer.IsInDesignMode) // if at runtime
            {
#if SILVERLIGHT
                RoutedEventHandler handler = null;
                handler = delegate
                {
                    // it means we have the VM instance now we should inject the services
                    if (containerElement.DataContext != null) 
                    {
                        resolver.SatisfyImports(
                                 containerElement.DataContext, containerElement);
                    }
                    containerElement.Loaded -= handler;
                };
                if (containerElement.DataContext == null)
                    containerElement.Loaded += handler;
                else
                {
                    handler(null, default(RoutedEventArgs));
                }
#else
                DependencyPropertyChangedEventHandler handler = null;
                handler = delegate
                {
                    // it means we have the VM instance now we should inject the services
                    if (containerElement.DataContext != null) 
                    {
                        resolver.SatisfyImports(
                                 containerElement.DataContext, containerElement);
                    }
                    containerElement.DataContextChanged -= handler;
                };
 
                if (containerElement.DataContext == null)
                    // we need to wait until the context is set
                    containerElement.DataContextChanged += handler;
                else // DataContext is already set 
                {
                    handler(null, default(DependencyPropertyChangedEventArgs));
                }
#endif
            }
 
            if(Designer.IsInDesignMode)
            {
                // this will create the VM and set it as DataContext
                base.CreateViewModel(viewModelContext, containerElement ); 
 
                //if the ViewModel is an IDataContextAware ViewModel
                //then we should call the DesignTimeInitialization
                var dataContextAwareVM = containerElement.DataContext as IDesignTimeAware;
                if (dataContextAwareVM != null)
                    dataContextAwareVM.DesignTimeInitialization();
            }
        }
    }
}

As I say, this is how a Cinch V1'ish app / ViewModel first can happily work with MeffedMVVM, and you can read more about the DataTemplates approach using the Cinch V1 article link: CinchIII.aspx#CloseVM.

But I also said this is not the proffered approach in Cinch V2, so let us have a look at what we might do in Cinch V2.

View First: A Better Approach (WPF Support Only, Sorry SL Users)

Within Cinch V2, what I wanted was a couple of things:

  1. The ability to use full View first design time data support offered by MeffedMVVM
  2. Allow some sort of contextual data to be passed to a view

So those are the requirements I set out with; sound simple, don't they? So how do they work? Well, the first one is dead simple, you have seen that before, and I talked about it in the first Cinch V2 article, read this link View-ViewModel Resolution for more details on that; that is standard Cinch V2/MeffedMVVM View-ViewModel resolution, so I will not go into that again.

The second point above is however totally new territory and something I do quite like actually.

So let's just go through a quick scenario:

"Suppose you have a TabControl that is showing a list of customers, and that list of customers is its own View (say CustomersListView/CustomerListViewModel), and when you click on one of the customers in the list, you wish to open a new View which shows the selected customer's details in a new View (say CustomerEditView / CustomerEditViewModel)."

That sounds easy enough, and you are probably thinking, oh, I could use a Mediator for that, but remember the Mediator is a broadcaster that broadcasts a message NotifyColleagues("New Customer Edit", SomeCustomer), so any subscriber of this message would have to work out whether they should add a new View for the CustomerEditView. It is not that easy, believe me.

So what I have come up with is a variation on what I offered in Cinch V1 using an ObservableCollection<T> and DataTemplates; it's just this time, I am using a View first approach to take full advantage of MeffedMVVM.

So how does it all work?

Well, in step by step instructions, it works like this:

  1. The WPF ViewModelBase class holds a ObservableCollection<WorkspaceData>, where each WorkspaceData has a CloseWorkSpaceCommand to ensure that the workspace can be closed (say if it's in a TabControl).
  2. Within the View that has the need to show the sub views, there is a single DataTemplate that matches against the type WorkspaceData.
  3. Within the DataTemplate for the WorkspaceData, there is an Attached DP which uses the bound WorkspaceData to deduce what View should be loaded.
  4. The Attached DP that now knows about the WorkspaceData can also obtain some additional contextual information from the bound WorkspaceData and passes this data to the View.

I have to say I think this approach now offers me the full support that I wanted; I can have View first, I have a way of adding Views to regions of another View using standard code all in my ViewModels. I can pass the newed up View some contextual data, and best of all, I get full design time data support for my ViewModels thanks to my Views being View first.

That is the brief, so what does the code look like? Well, let's start at the beginning.

WorkSpaces : The Actual WorkSpaceData Class

Probably the first thing to look at is the actual WorkspaceData code, which looks like this:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;

namespace Cinch
{
    /// <summary>
    /// Workspace data class which can be used within a DataTemplate along
    /// with the NavProps DP to manage workspaces
    /// </summary>
    public partial class WorkspaceData : INotifyPropertyChanged, IDisposable
    {
        #region Data
        private string imagePath;
        private string viewLookupKey;
        private object dataValue;
        private string displayText;
        private SimpleCommand<Object, Object> closeWorkSpaceCommand;
        private Boolean isCloseable = true;
        #endregion

        #region Ctor
        public WorkspaceData(string imagePath,string viewLookupKey, 
               object dataValue, string displayText, bool isCloseable)
        {
            Mediator.Instance.Register(this);
            this.ImagePath = imagePath;
            this.ViewLookupKey = viewLookupKey;
            this.DataValue = dataValue;
            this.DisplayText = displayText;
            this.IsCloseable = isCloseable;

            CloseWorkSpaceCommand = new SimpleCommand<object, object>(
                   x => true, x => ExecuteCloseWorkSpaceCommand(x));
        }
        #endregion

        #region Custom Closing Event

        public event CancelEventHandler WorkspaceTabClosing;

        protected void NotifyWorkspaceTabClosing(CancelEventArgs args)
        {
            CancelEventHandler handler = WorkspaceTabClosing;

            if (handler != null)
            {
                handler(this, args);
            }
        }

        #endregion

        #region Command Implememtation
        /// <summary>
        /// Executes the CloseWorkSpace Command
        /// </summary>
        private void ExecuteCloseWorkSpaceCommand(object o)
        {
            CancelEventHandler handler = WorkspaceTabClosing;
            if (handler != null && 
                WorkspaceTabClosing.GetInvocationList().Count() > 0)
            {
                CancelEventArgs args = new CancelEventArgs(false);
                NotifyWorkspaceTabClosing(args);
                if (args.Cancel == false)
                {
                    Mediator.Instance.NotifyColleagues<WorkspaceData>(
                                      "RemoveWorkspaceItem", this);
                }
            }
            else
            {
                Mediator.Instance.NotifyColleagues<WorkspaceData>(
                                  "RemoveWorkspaceItem", this);
            }
        }
        #endregion

        #region Public Properties

        /// <summary>
        /// The ViewModel that was created for this WorkSpaceData object,
        /// if it was used to create a View
        /// </summary>
        public Object ViewModelInstance { get; set; }

        /// <summary>
        /// CloseActivePopUpCommand : Close popup command
        /// </summary>
        public SimpleCommand<Object, Object> CloseWorkSpaceCommand { get; private set; }

        /// <summary>
        /// Is this workspace a closeable workspace
        /// </summary>
        static PropertyChangedEventArgs isCloseableArgs =
            ObservableHelper.CreateArgs<WorkspaceData>(x => x.IsCloseable);

        public Boolean IsCloseable
        {
            get { return isCloseable; }
            set
            {
                isCloseable = value;
                NotifyPropertyChanged(isCloseableArgs);
            }
        }

        /// <summary>
        /// True if this workspace has an image
        /// </summary>
        public bool HasImage
        {
            get
            {
                return !string.IsNullOrEmpty(ImagePath);
            }
        }

        /// <summary>
        /// ImagePath
        /// </summary>
        static PropertyChangedEventArgs imagePathArgs =
            ObservableHelper.CreateArgs<WorkspaceData>(x => x.ImagePath);

        public string ImagePath
        {
            get { return imagePath; }
            set
            {
                imagePath = value;
                NotifyPropertyChanged(imagePathArgs);
            }
        }

        /// <summary>
        /// View key lookup name
        /// </summary>
        static PropertyChangedEventArgs viewLookupKeyArgs =
            ObservableHelper.CreateArgs<WorkspaceData>(x => x.ViewLookupKey);

        public string ViewLookupKey
        {
            get { return viewLookupKey; }
            set
            {
                viewLookupKey = value;
                NotifyPropertyChanged(viewLookupKeyArgs);
            }
        }

        /// <summary>
        /// Workspace context data
        /// </summary>
        static PropertyChangedEventArgs dataValueArgs =
            ObservableHelper.CreateArgs<WorkspaceData>(x => x.DataValue);

        public object DataValue
        {
            get { return dataValue; }
            set
            {
                dataValue = value;
                NotifyPropertyChanged(dataValueArgs);
            }
        }

        /// <summary>
        /// Workspace display text, is used for Headers controls such as TabControl
        /// </summary>
        static PropertyChangedEventArgs displayTextArgs =
            ObservableHelper.CreateArgs<WorkspaceData>(x => x.DisplayText);

        public string DisplayText
        {
            get { return displayText; }
            set
            {
                displayText = value;
                NotifyPropertyChanged(displayTextArgs);
            }
        }
        #endregion

        #region Overrides
        public override string ToString()
        {
            return String.Format(
                "ViewLookupKey {0}, DisplayText {1}, IsCloseable {2}",
                ViewLookupKey, DisplayText, IsCloseable);
        }
        #endregion

        #region INotifyPropertyChanged

        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Notify using pre-made PropertyChangedEventArgs
        /// </summary>
        /// <param name="args"></param>
        protected void NotifyPropertyChanged(PropertyChangedEventArgs args)
        {
            PropertyChangedEventHandler handler = PropertyChanged;

            if (handler != null)
            {
                handler(this, args);
            }
        }

        /// <summary>
        /// Notify using String property name
        /// </summary>
        protected void NotifyPropertyChanged(String propertyName)
        {
            PropertyChangedEventHandler handler = PropertyChanged;

            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #endregion

        #region IDisposable Members

        /// <summary>
        /// Invoked when this object is being removed from the application
        /// and will be subject to garbage collection.
        /// </summary>
        public void Dispose()
        {
            Mediator.Instance.Unregister(this);
            this.OnDispose();
        }

        /// <summary>
        /// Child classes can override this method to perform 
        /// clean-up logic, such as removing event handlers.
        /// </summary>
        protected virtual void OnDispose()
        {
        }

#if DEBUG
        /// <summary>
        /// Useful for ensuring that ViewModel objects
        /// are properly garbage collected.
        /// </summary>
        ~WorkspaceData()
        {

        }
#endif

        #endregion // IDisposable Members
    }
}

As you can see, this is a pretty simple class that has a few properties and also notifies (using the Mediator) any ViewModel that holds an instance of one of these classes to remove it when the CloseWorkSpaceCommand (maybe from a closeable TabItem) is executed.

WorkSpaces: ViewModelBase Class Support

And the Cinch WPF ViewModelBase class has a ObservableCollection<WorkspaceData> such that any ViewModel that inherits from a Cinch ViewModelBase class is capable of managing workspaces.

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.ComponentModel.Composition;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Data;
 
namespace Cinch
{
    /// <summary>
    /// This is a WPF specific partial section of a ViewModelBase
    /// </summary>
    public abstract partial class ViewModelBase
    {
        #region Data
 
        /// <summary>
        /// Collection of workspaces that this ViewModel manages
        /// </summary>
        private ObservableCollection<WorkspaceData> views = 
        new ObservableCollection<WorkspaceData>();
        private ICollectionView collectionView;
 
        #endregion
 
        #region Ctor
        public ViewModelBase()
        {
 
            CloseWorkSpaceCommand = new SimpleCommand<object, object>(
                   x => true, x => ExecuteCloseWorkSpaceCommand());
  
            Mediator.Instance.RegisterHandler<WorkspaceData>(
                    "RemoveWorkspaceItem", OnNotifyDataRecieved);
            collectionView = CollectionViewSource.GetDefaultView(this.Views);
  
        }
        #endregion
 
        #region Mediator Message Sinks
 
        [MediatorMessageSink("RemoveWorkspaceItem")]
        void OnNotifyDataRecieved(WorkspaceData workspaceToRemove)
        {
            if (this.Views.Contains(workspaceToRemove))
            {
                this.Views.Remove(workspaceToRemove);
            }
        }
 
        #endregion
 
    
        #region Public Properties
 
        static PropertyChangedEventArgs viewsArgs =
            ObservableHelper.CreateArgs<ViewModelBase>(x => x.Views);
 
        public ObservableCollection<WorkspaceData> Views
        {
            get { return views; }
            set
            {
                views = value;
                NotifyPropertyChanged(viewsArgs);
            }
        }
 
        #endregion
 
        #region Public Methods
 
        public void SetActiveWorkspace(WorkspaceData viewnav)
        {
            if (collectionView != null)
                collectionView.MoveCurrentTo(viewnav);
        }
    
        #endregion
    }
}

You can also see that there is code that listens for the Mediator message from the WorkspaceData to remove it from the list of open WorkSpaces.

WorkSpaces: The DataTemplate to Make it all Sing

The next step is to add some WorkspaceData items to the Views collection. This is some code from the Cinch V2 WPF demo app:

C#
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);
    workspace1.WorkspaceTabClosing += ImageWorkSpace_WorkspaceTabClosing;
 
    WorkspaceData workspace2 = 
        new WorkspaceData(@"/CinchV2DemoWPF;component/Images/About.png",
        "AboutView", null, "About Cinch V2", true);
 
    Views.Add(workspace1);
    Views.Add(workspace2);
    SetActiveWorkspace(workspace1);
}

//User can choose to cancel closing of workspace here by setting CancelEventArgs
//on the sending WorkspaceData. When we can close the workspace we must also
//unhook the WorkspaceTabClosing to avoid any possible memory leak
private void ImageWorkSpace_WorkspaceTabClosing(object sender, CancelEventArgs e)
{
    e.Cancel = false;
    CustomDialogResults result = 
        messageBoxService.ShowYesNo("Are you sure you want to close this tab?", 
            CustomDialogIcons.Question);

    //if user did not want to cancel, keep workspace open
    if (result == CustomDialogResults.No)
    {
        e.Cancel = true;
    }
    //otherwise close workspace, and make sure to unhook WorkspaceTabClosing event
    //to prevent memory leak
    else
    {
        ((WorkspaceData)sender).WorkspaceTabClosing -= 
                                ImageWorkSpace_WorkspaceTabClosing;
    }
}

See how I am creating two workspaces there using the WorkspaceData, and you may also notice in the workspace 1 code above, I am even passing in some contextual data to it, which we will get to in a minute.

You can also see that there is an optional WorkspaceTabClosing that I can use to hook up, from where the ViewModel could possibly decide whether to really allow the WorkSpaceData to be closed (this could be some code, or asking the user, as I am in the example above).

So now we have some WorkspaceData items in the Views collection, what now? Well, we need to supply a single DataTemplate to match against the Type of WorkspaceData. This should only be done once per app, as the Template to use will always be the same (or it should be).

Here is how I defined the DataTemplate in the Cinch V2 WPF demo app:

XML
<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="
        x:Class="CinchV2DemoWPF.MainWindow"
        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>
 
    <local:TabControlEx 
            ItemsSource="{Binding Views}" 
            CinchV2:NavProps.ShouldHideHostWhenNoItems="true"
            DisplayMemberPath="DisplayText">
    </local:TabControlEx>
 
</Window>

In this XAML, see how I am binding a TabControl (well, it's a special TabControl which I will also talk about in this article) to the Views collection, and I am providing a DataTemplate which uses an Attached DP called NavProps (more on this in just a second).

Image 4

*** Very Important Notes ****

Sorry about the image, but it got your attention right!!!!!

Impotant Note 1

I would very, very strongly recommend that you use a DataTemplate like that shown above. In fact, you must provide a Border in your DataTemplate; otherwise the workspaces in Cinch will not work. As the NavProps DP is expecting the parent to be a Border, you must supply a Border as the root container within the DataTemplate, so this is quite important.

Impotant Note 2

The other thing to note is that the workspaces are really only intended to be bound against a ItemsControl, and as such you must stick to using ItemsControl or any of its super types, such as TabControl/ListBox etc. Basically, with a ListBox, you should be able to craft anything, from a single item, to multiple items; remember, with a ListBox, you can swap out the ItemsPanelTemplate to use any of the standard layout containers such as Grid/Canvas etc., so I am confident you can do anything with ItemsControl or any of its super types. Failure to use a ItemsControl will result in non-working workspaces.

You must abide by these two notes....If you don't, this will result in non-working workspaces, so just follow these notes and you should be fine.

WorkSpaces: The NavProps Attached DP to Resolve the View

Recall from earlier, we briefly touched on an Attached DP called NavProps. Well, what does that do for us? A couple of things actually. There are actually two Attached DPs:

  1. The ShouldHideHostWhenNoItems Attached DP: Can be used to hide the ItemsControl host when there are no more displayable items left.
  2. The ViewCreator Attached DP: Is used to bind with a WorkspaceData object, and when it changes, it examines the bound WorkspaceData.ViewLookupKey and will create a new View based on that key.

ShouldHideHostWhenNoItems Attached DP

This is pretty simple really, it is basically just a boolean that can be set on your ItemsControl to have it hide itself when there are no more displayable items in it. This is probably most useful when you allow your users to close workspace items (such as closeable tabs).

XML
<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="
        x:Class="CinchV2DemoWPF.MainWindow"
        meffed:ViewModelLocator.ViewModel="MainWindowViewModel">
 
    <local:TabControlEx 
            ItemsSource="{Binding Views}" 
            CinchV2:NavProps.ShouldHideHostWhenNoItems="true"
            DisplayMemberPath="DisplayText">
    </local:TabControlEx>
 
</Window>

ViewCreator Attached DP

This is the second Attached DP within the NavProps class. And as I say, this one is responsible for actually creating a new View based on some magical string that is set on the databound WorkspaceData.ViewLookupKey. This code looks like this:

C#
/// <summary>
/// This DP is used to create the actual workspace View based on the value of the
/// bound WorkspaceData.ViewType
/// </summary>
#region ViewCreator
 
/// <summary>
/// ViewCreator Attached Dependency Property
/// </summary>
public static readonly DependencyProperty ViewCreatorProperty =
    DependencyProperty.RegisterAttached("ViewCreator", 
    typeof(WorkspaceData), typeof(NavProps),
        new FrameworkPropertyMetadata((WorkspaceData)null,
            new PropertyChangedCallback(OnViewCreatorChanged)));
 
/// <summary>
/// Gets the ViewCreator property.
/// </summary>
public static WorkspaceData GetViewCreator(DependencyObject d)
{
    return (WorkspaceData)d.GetValue(ViewCreatorProperty);
}
 
/// <summary>
/// Sets the ViewCreator property.
/// </summary>
public static void SetViewCreator(DependencyObject d, WorkspaceData value)
{
    d.SetValue(ViewCreatorProperty, value);
}
 
/// <summary>
/// Handles changes to the ViewCreator property.
/// </summary>
private static void OnViewCreatorChanged(DependencyObject d, 
    DependencyPropertyChangedEventArgs e)
{
    ItemsControl itemsControl = null;
 
    if (e.NewValue == null)
    {
        itemsControl = TreeHelper.TryFindParent<ItemsControl>(d);
        bool shouldHideHostWhenNoItems = 
          (bool)itemsControl.GetValue(NavProps.ShouldHideHostWhenNoItemsProperty);
        if (shouldHideHostWhenNoItems)
        {
            if (itemsControl != null)
                itemsControl.Visibility = Visibility.Collapsed;
        }
        return;
    }
 
    Border contPresenter = (Border)d;
    WorkspaceData viewNavData = (WorkspaceData)e.NewValue;
 
    var theView = ViewResolver.CreateView(viewNavData.ViewLookupKey);
    viewNavData.ViewModelInstance = ((FrameworkElement)theView).DataContext;
    
    IWorkSpaceAware dataAwareView = theView as IWorkSpaceAware;
    if (dataAwareView == null)
    {
        throw new InvalidOperationException(
            "NavProps attached property is only designed to work " + 
        " with Views that implement the IWorkSpaceAware interface");
    }
    else
    {
        dataAwareView.WorkSpaceContextualData = viewNavData;
        contPresenter.Child = (UIElement)dataAwareView;
    }
    itemsControl = TreeHelper.TryFindParent<ItemsControl>(d);
    if (itemsControl != null)
        itemsControl.Visibility = Visibility.Visible;
}
 
#endregion

Recall the from DataTemplate earlier:

XML
<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="
        x:Class="CinchV2DemoWPF.MainWindow"
        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>

    <local:TabControlEx 
            ItemsSource="{Binding Views}" 
            CinchV2:NavProps.ShouldHideHostWhenNoItems="true"
            DisplayMemberPath="DisplayText">
    </local:TabControlEx>
 
</Window>

Now you may notice that there is a class called ViewResolver (var theView = ViewResolver.CreateView(viewNavData.ViewLookupKey);), so how does this ViewResolver know what to do with a string, and how does it create a new Window from it? Well, simply put, the ViewResolver is just a Dictionary<string,type> where it uses Activator.CreateInstance to create a new instance of a type that matches a string lookup key (yes, the one from the bound WorkspaceData).

It then grabs the DataContext (MEF supplied) from the View, and stores it back in the WorkspaceData object, such that the code that created the WorkspaceData object will have a link to the newly created ViewModel.

It then casts the obtained View to IWorkSpaceAware and sets the dataAwareView.WorkSpaceContextualData property, passing in the contextual data as supplied by the WorkspaceData.

In the demo app, I use the WorkspaceData to pass a directory path to the ImageLoaderView using the IWorkSpaceAware interface that the View implements. What happens then is MeffedMVVM creates the ViewModel, and the WPF demo app's ViewModel just happens to use the IViewAwareStatus service, so we hook into the loaded event of that, and then do the following in the ViewModel.

C#
private void ViewAwareStatusService_ViewLoaded()
{
    if (!Designer.IsInDesignMode)
    {
        var view = viewAwareStatusService.View;
        IWorkSpaceAware workspaceData = (IWorkSpaceAware)view;
        DirectoryName = (String)workspaceData.WorkSpaceContextualData.DataValue;
    }
    LoadImages(DirectoryName);
}

And that is how we manage to get contextual data from the workspace into the View and also into the ViewModel via the View (using the IWorkspaceAware interface on the View).

But how does the ViewResolvers Dictionary<string,type> get populated in the first place? Well, in the Cinch V2 WPF demo app, this happens in the App.xaml.cs when you call the CinchBootstrapper to reflectively look through an IEnumerable<Assembly> to try and find any Views (UserControls) that are attributed up with the special Cinch ViewnameToViewLookupKeyMetadata.

C#
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Windows;
using System.ComponentModel.Composition.Hosting;

using Cinch;
using System.Reflection;
using MEFedMVVM.ViewModelLocator;
 
namespace CinchV2DemoWPF
{
    public partial class App : Application
    {
        public App()
        {
            CinchBootStrapper.Initialise(
               new List<Assembly> { typeof(App).Assembly });
            InitializeComponent();
        }
    }
}

And here is an example view with the ViewnameToViewLookupKeyMetadata:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Cinch;
using System.Diagnostics;
 
namespace CinchV2DemoWPF
{
 
    [ViewnameToViewLookupKeyMetadata("ImageLoaderView", typeof(ImageLoaderView))]
    public partial class ImageLoaderView : UserControl, IWorkSpaceAware
    {
 
    }
}

And that is how the ViewResolvers Dictionary<string,type> is populated ready for the Attached DP to call upon it to create the View that matches the requested View type from the WorkspaceData that is being bound to in the DataTemplate.

WorkSpaces: Special Notes

Now all of this is grand, but unfortunately, WPF throws some weirdness in our path, in the form of the TabControl. Which is a bastard of a control. How many of you know that in WPF the TabControl's VisualTree only keeps the selected item in the VisualTree.

Does that sound bad to you? No, think again (though this is only a problem when using DataTemplates, direct TabItem / View combination is OK). So we have several Views which use MeffedMVVM to create a ViewModel within a TabControl. We then change tabs, and guess what? The View gets trashed, and when we go back to a previous TabItem, as we are using Vew first and MeffedMVVM, a new ViewModel is created for the View.

Now that is a bit messed up, don't you think? Well, I for one do.

Luckily, help is at hand. I have long known this, and have crafted a special TabControl for WPF that does not trash the VisualTree on selection changed, but rather keeps all items in memory and changes the Visibility of them.

This requires two things that you need to include in your own WPF projects:

TabControlEx C# code:

This looks like this:

C#
using System;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
 
namespace CinchV2DemoWPF
{
    [TemplatePart(Name = "PART_ItemsHolder", Type = typeof(Panel))]
    public class TabControlEx : TabControl
    {
        private Panel _itemsHolder = null;
 
        public TabControlEx()
            : base()
        {
            // this is necessary so that we get
            // the initial databound selected item
            this.ItemContainerGenerator.StatusChanged += 
                 ItemContainerGenerator_StatusChanged;
        }
 
        /// <summary>
        /// if containers are done, generate the selected item
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
        {
            if (this.ItemContainerGenerator.Status == 
                     GeneratorStatus.ContainersGenerated)
            {
                this.ItemContainerGenerator.StatusChanged -= 
                     ItemContainerGenerator_StatusChanged;
                UpdateSelectedItem();
            }
        }
 
        /// <summary>
        /// get the ItemsHolder and generate any children
        /// </summary>
        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            _itemsHolder = GetTemplateChild("PART_ItemsHolder") as Panel;
            UpdateSelectedItem();
        }
 
        /// <summary>
        /// when the items change we remove any generated panel
        /// children and add any new ones as necessary
        /// </summary>
        /// <param name="e"></param>
        protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
        {
            base.OnItemsChanged(e);
 
            if (_itemsHolder == null)
            {
                return;
            }
 
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Reset:
                    _itemsHolder.Children.Clear();
                    break;
 
                case NotifyCollectionChangedAction.Add:
                case NotifyCollectionChangedAction.Remove:
                    if (e.OldItems != null)
                    {
                        foreach (var item in e.OldItems)
                        {
                            ContentPresenter cp = FindChildContentPresenter(item);
                            if (cp != null)
                            {
                                _itemsHolder.Children.Remove(cp);
                            }
                        }
                    }
 
                    // don't do anything with new items because we don't want to
                    // create visuals that aren't being shown
 
                    UpdateSelectedItem();
                    break;
 
                case NotifyCollectionChangedAction.Replace:
                    throw new NotImplementedException("Replace not implemented yet");
            }
        }
 
        /// <summary>
        /// update the visible child in the ItemsHolder
        /// </summary>
        /// <param name="e"></param>
        protected override void OnSelectionChanged(SelectionChangedEventArgs e)
        {
            base.OnSelectionChanged(e);
            UpdateSelectedItem();
        }
 
        /// <summary>
        /// generate a ContentPresenter for the selected item
        /// </summary>
        void UpdateSelectedItem()
        {
            if (_itemsHolder == null)
            {
                return;
            }
 
            // generate a ContentPresenter if necessary
            TabItem item = GetSelectedTabItem();
            if (item != null)
            {
                CreateChildContentPresenter(item);
            }
 
            // show the right child
            foreach (ContentPresenter child in _itemsHolder.Children)
            {
                child.Visibility = ((child.Tag as TabItem).IsSelected) ? 
                     Visibility.Visible : Visibility.Collapsed;
            }
        }
 
        /// <summary>
        /// create the child ContentPresenter
        /// for the given item (could be data or a TabItem)
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        ContentPresenter CreateChildContentPresenter(object item)
        {
            if (item == null)
            {
                return null;
            }
 
            ContentPresenter cp = FindChildContentPresenter(item);
 
            if (cp != null)
            {
                return cp;
            }
 
            // the actual child to be added. cp.Tag is a reference to the TabItem
            cp = new ContentPresenter();
            cp.Content = (item is TabItem) ? (item as TabItem).Content : item;
            cp.ContentTemplate = this.SelectedContentTemplate;
            cp.ContentTemplateSelector = this.SelectedContentTemplateSelector;
            cp.ContentStringFormat = this.SelectedContentStringFormat;
            cp.Visibility = Visibility.Collapsed;
            cp.Tag = (item is TabItem) ? item : 
                     (this.ItemContainerGenerator.ContainerFromItem(item));
            _itemsHolder.Children.Add(cp);
            return cp;
        }
 
        /// <summary>
        /// Find the CP for the given object. 
        /// data could be a TabItem or a piece of data
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        ContentPresenter FindChildContentPresenter(object data)
        {
            if (data is TabItem)
            {
                data = (data as TabItem).Content;
            }
 
            if (data == null)
            {
                return null;
            }
 
            if (_itemsHolder == null)
            {
                return null;
            }
 
            foreach (ContentPresenter cp in _itemsHolder.Children)
            {
                if (cp.Content == data)
                {
                    return cp;
                }
            }
 
            return null;
        }
 
        /// <summary>
        /// copied from TabControl; wish it were protected
        /// in that class instead of private
        /// </summary>
        /// <returns></returns>
        protected TabItem GetSelectedTabItem()
        {
            object selectedItem = base.SelectedItem;
            if (selectedItem == null)
            {
                return null;
            }
            TabItem item = selectedItem as TabItem;
            if (item == null)
            {
                item = base.ItemContainerGenerator.ContainerFromIndex(
                            base.SelectedIndex) as TabItem;
            }
            return item;
        }
    }
}

But you will also need to use this Style for the TabControlEx to work:

XML
<Style x:Key="TabControlStyleVerticalTabs" TargetType="{x:Type local:TabControlEx}">
    <Setter Property="Foreground" 
       Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
    <Setter Property="Padding" Value="0"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="Background" Value="White"/>
    <Setter Property="HorizontalContentAlignment" Value="Center"/>
    <Setter Property="VerticalContentAlignment" Value="Center"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:TabControlEx}">
                <DockPanel >
                    <TabPanel x:Name="tabpanel" Margin="0,15,0,0"
                                    Visibility="Visible"
                                    DockPanel.Dock="Left"
                                    KeyboardNavigation.TabIndex="1"
                                    IsItemsHost="True" />
                    <Border  CornerRadius="10,0,0,10"
                                Margin="0,5,0,5"
                                Background="{TemplateBinding Background}">
                        <Grid DockPanel.Dock="Bottom" Margin="10,0,0,0"
                            Background="{TemplateBinding Background}"
                            x:Name="PART_ItemsHolder" />
                    </Border>
                </DockPanel>
                <!-- no content presenter -->
                <ControlTemplate.Triggers>
                    <Trigger Property="IsEnabled" Value="false">
                        <Setter Property="Foreground" 
                            Value="{DynamicResource {x:Static 
                                   SystemColors.GrayTextBrushKey}}"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Just for completeness, here is what I do in the WPF demo app to provide closeable TabItems, where the PART_Close Buttons is bound to the WorkspaceData that was used to create the DataTemplate being applied to the TabItem.

XML
<Style x:Key="TabItemStyleVerticalTabs" TargetType="{x:Type TabItem}">
    <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
    <Setter Property="Foreground" Value="Black"/>
    <Setter Property="Padding" Value="0"/>
    <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
    <Setter Property="VerticalContentAlignment" Value="Stretch"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TabItem}">
                <Grid SnapsToDevicePixels="true">
                    <Border x:Name="Bd" BorderThickness="0">
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="*"/>
                                <RowDefinition Height="2"/>
                            </Grid.RowDefinitions>
 
                            <Grid Grid.Row="0">
 
                                <Grid  x:Name="grid" Margin="0" 
                                  HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
 
                                <StackPanel Orientation="Horizontal" Margin="15,5,15,5" >
 
                                    <Button x:Name="PART_Close" 
                                        HorizontalAlignment="Left" Margin="2,0,2,0" 
                                        VerticalAlignment="Center" Width="16" 
                                        Height="16" 
                                        Command="{Binding Path=CloseWorkSpaceCommand}"   
                                        Visibility="{Binding IsCloseable, 
                                            Converter={StaticResource boolToVisConv}, 
                                            ConverterParameter=True}"
                                        Focusable="False"
                                        Style="{DynamicResource CloseableTabItemButtonStyle}" 
                                        ToolTip="Close Tab">
                                        <Path x:Name="Path" Stretch="Fill" 
                                            StrokeThickness="0.5" 
                                            Stroke="{DynamicResource closeTabCrossStroke}" 
                                            Fill="Black" 
                                            Data="F1 M 2.28484e-007,
                                                  1.33331L 1.33333,0L 4.00001,
                                                  2.66669L 6.66667,
                                                  6.10352e-005L 8,1.33331L 5.33334,
                                                  4L 8,6.66669L 6.66667,8L 4,
                                                  5.33331L 1.33333,8L 1.086e-007,
                                                  6.66669L 2.66667,4L 2.28484e-007,1.33331 Z " 
                                            HorizontalAlignment="Stretch" 
                                            VerticalAlignment="Stretch"/>
                                    </Button>
 
                                    <Image Source="{Binding ImagePath}" Width="32" 
                                        Height="32" Margin="2,0,2,0" 
                                        Visibility="{Binding HasImage, 
                                            Converter={StaticResource boolToVisConv}, 
                                            ConverterParameter=True}" 
                                        VerticalAlignment="Center"/>
 
                                    <Label x:Name="lbl" Margin="2,0,2,0" 
                                        FontSize="12"
                                        FontWeight="Bold"
                                        Content="{Binding Path=DisplayText}" 
                                        HorizontalAlignment="Left"
                                        VerticalAlignment="Center" />
 
                                </StackPanel>
 
                                <Label x:Name="lblArrow" FontFamily="Wingdings 3" 
                                        Content="t" FontSize="16" 
                                        Foreground="White" Margin="0,0,-9,0"
                                        Opacity="0"
                                        VerticalAlignment="Center" 
                                        VerticalContentAlignment="Center"
                                        HorizontalAlignment="Right" 
                                        HorizontalContentAlignment="Right"/>
                            </Grid>
 
                            <Rectangle x:Name="rectShine" Grid.Row="1" 
                                Opacity="0.5" Fill="#ff656565" 
                                StrokeThickness="0" HorizontalAlignment="Stretch"
                                VerticalAlignment="Stretch" Height="2" />
                        </Grid>
                    </Border>
                </Grid>
                <ControlTemplate.Triggers>
 
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <Condition Property="IsSelected" Value="true"/>
                        </MultiTrigger.Conditions>
                        <Setter Property="Panel.ZIndex" Value="1"/>
                        <Setter Property="Background" TargetName="Bd" 
                          Value="{StaticResource selectedBrush}"/>
                        <Setter Property="Background" TargetName="grid" 
                          Value="{StaticResource selectedGradientGlow}"/>
                        <Setter Property="Opacity" TargetName="lblArrow" Value="1"/>
                        <Setter Property="Height" TargetName="rectShine" Value="2"/>
 
 
                    </MultiTrigger>
 
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <Condition Property="IsSelected" Value="false"/>
                            <Condition Property="IsMouseOver" Value="true"/>
                        </MultiTrigger.Conditions>
                        <Setter Property="Panel.ZIndex" Value="1"/>
                        <Setter Property="Background" TargetName="Bd" 
                          Value="{StaticResource nonSelectedBrush}"/>
                        <Setter Property="Background" 
                          TargetName="grid" Value="Transparent"/>
                        <Setter Property="Opacity" 
                          TargetName="lblArrow" Value="0"/>
                        <Setter Property="Height" 
                          TargetName="rectShine" Value="2"/>
                    </MultiTrigger>
 
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <Condition Property="IsSelected" Value="false"/>
                            <Condition Property="IsMouseOver" Value="false"/>
                        </MultiTrigger.Conditions>
                        <Setter Property="Height" TargetName="rectShine" Value="0"/>
                        <Setter Property="Foreground" TargetName="lbl" Value="White"/>
                        <Setter Property="Fill" TargetName="Path" Value="White"/>
 
                    </MultiTrigger>
 
                    <Trigger Property="TabStripPlacement" Value="Right">
                        <Setter Property="Content" TargetName="lblArrow" Value="u"/>
                        <Setter Property="Margin" TargetName="lblArrow" Value="-9,0,0,0"/>
                        <Setter Property="HorizontalAlignment" 
                           TargetName="lblArrow" Value="Left"/>
                        <Setter Property="HorizontalContentAlignment" 
                           TargetName="lblArrow" Value="Left"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

As I say, the workspace support in Cinch V2 is only available for WPF, and I will not be supplying it for Silverlight, for several reasons:

  1. Silverlight's TabControl lacks some of the overrides and general internals that I need to make it work the same as in WPF.
  2. In Silvelight, I think there is more of a tendency to use NavigationFrame etc., to provide navigation, which I think is a great idea. I think desktop apps should look and work like desktop apps, and web apps should look like web apps, which would imply (at least to me) that TabControl like functionality belongs only to the desktop ...but that is just my opinion. You know if you disagree, implement something similar to the WPF version and check out the WPF version's IViewAwareStatus service implementation, which is different from the Silverlight version. The WPF one uses WeakEvents/WeakReference all over the place.

Extra Threading Helpers

Cinch V1 already had a few Dispatcher related helpers, and Extension Methods. If you missed some of the utilities in Cinch V1, here is brief description of what they did:

  • BackgroundTaskManager: A small wrapper around a BackgroundWorker
  • DispatcherExtensions: Some nice Dispatcher extensions (WPF only)
  • DispatcherNotifiedObservableCollection<T>: ObservableCollection<T> which marshals to a UI thread
  • ApplicationHelper: Provides DoEvents() (WPF only)

I decided to include one more within Cinch V2, which I borrowed from fellow WPF Disciple and good friend Daniel Vaughan. The original code is available in Daniel's article http://www.codeproject.com/KB/silverlight/Mtvt.aspx.

It is basically a UI synchronization context, similar to the one found in WinForms, but is tailored for use with WPF and Silverlight. It is strange that such an object does not already exist within the standard WPF/Silverlight base classes, ho hum.

There is an interface for this class, such that if you want to create a mock or test double, you can. Here is the interface:

ISynchronizationContext:
C#
using System;
using System.Threading;
using System.Windows.Threading;
 
/// <summary>
/// This class was obtained from Daniel Vaughan (a fellow WPF Discple)
/// http://www.codeproject.com/KB/silverlight/Mtvt.aspx
/// </summary>
namespace Cinch
{
    /// <summary>
    /// SynchronizationContext interface that provides
    /// various thread marshalling calls to be done
    /// </summary>
    public interface ISynchronizationContext
    {
        bool InvokeRequired { get; }
 
        void Initialize();
        void Initialize(Dispatcher dispatcher);
        void InvokeAndBlockUntilCompletion(Action action);
        void InvokeAndBlockUntilCompletion(SendOrPostCallback callback, object state);
        void InvokeWithoutBlocking(Action action);
        void InvokeWithoutBlocking(SendOrPostCallback callback, object state);
    }
}

And here is the implementation:

UISynchronizationContext:
C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Threading;
using System.Threading;
 
/// <summary>
/// This class was obtained from Daniel Vaughan (a fellow WPF Discple)
/// http://www.codeproject.com/KB/silverlight/Mtvt.aspx
/// </summary>
namespace Cinch
{
    /// <summary>
    /// Singleton class providing the default implementation 
    /// for the <see cref="ISynchronizationContext"/>, 
    /// specifically for the UI thread.
    /// </summary>
    public partial class UISynchronizationContext : ISynchronizationContext
    {
        #region Data
        private DispatcherSynchronizationContext context;
        private Dispatcher dispatcher;
        private readonly object initializationLock = new object();
        #endregion
 
        #region Singleton implementation
 
        static readonly UISynchronizationContext instance = 
        new UISynchronizationContext();
 
        /// <summary>
        /// Gets the singleton instance.
        /// </summary>
        /// <value>The singleton instance.</value>
        public static ISynchronizationContext Instance
        {
            get
            {
                return instance;
            }
        }
 
        #endregion
 
        #region Private Methods
        private void EnsureInitialized()
        {
            if (dispatcher != null && context != null)
            {
                return;
            }
 
            lock (initializationLock)
            {
                if (dispatcher != null && context != null)
                {
                    return;
                }
 
                try
                {
#if SILVERLIGHT
                    dispatcher = System.Windows.Deployment.Current.Dispatcher;
#else
                    dispatcher = Dispatcher.CurrentDispatcher;
#endif
                    context = new DispatcherSynchronizationContext(dispatcher);
                }
                catch (InvalidOperationException)
                {
                    throw new Exception("Initialised called from non-UI thread.");
                }
            }
        }
        #endregion
 
        #region ISynchronizationContext Methods
 
        public void Initialize()
        {
            EnsureInitialized();
        }
 
        public void Initialize(Dispatcher dispatcher)
        {
            ArgumentValidator.AssertNotNull(dispatcher, "dispatcher");
            lock (initializationLock)
            {
                this.dispatcher = dispatcher;
                context = new DispatcherSynchronizationContext(dispatcher);
            }
        }
 
        public void InvokeWithoutBlocking(
        SendOrPostCallback callback, object state)
        {
            ArgumentValidator.AssertNotNull(callback, "callback");
            EnsureInitialized();
 
            context.Post(callback, state);
        }
 
        public void InvokeWithoutBlocking(Action action)
        {
            ArgumentValidator.AssertNotNull(action, "action");
            EnsureInitialized();
 
            context.Post(state => action(), null);
        }
 
        public void InvokeAndBlockUntilCompletion(
        SendOrPostCallback callback, object state)
        {
            ArgumentValidator.AssertNotNull(callback, "callback");
            EnsureInitialized();
 
            context.Send(callback, state);
        }
 
        public void InvokeAndBlockUntilCompletion(Action action)
        {
            ArgumentValidator.AssertNotNull(action, "action");
            EnsureInitialized();
 
            if (dispatcher.CheckAccess())
            {
                action();
            }
            else
            {
                context.Send(delegate { action(); }, null);
            }
        }
 
        public bool InvokeRequired
        {
            get
            {
                EnsureInitialized();
                return !dispatcher.CheckAccess();
            }
        }
        #endregion
    }
}

Extra Utilities

Cinch V1 already had a number of handy utilities that I have added too. If you missed some of the utilities in Cinch V1, here is a brief description of what they did:

  • ObservableHelper: Provided a small class to get a property name string from an Expression tree
  • PropertyObserver: A nice weak reference INotifyPropertyChanged listener

Anyway, for Cinch V2, I have also included these extra utilities:

PropertyChangedEventManager

This is only available for Silverlight.

As Silverlight does not have a PropertyChangedEventManager, I thought I would provide one to fill the gap (WPF has this class, of course). In fact, when I say I thought I would provide one, I really mean that I stole it from fellow WPF Disciple Pete O'Hanlon. So if you find that you need the PropertyChangedEventManager in Silverlight, never fear, it is here.

ArgumentValidator

Which is a simple class that provides many methods that can be used to validate arguments to methods.

BindingEvaluator (WPF Only)

This is only available for WPF.

I have found this class to be quite useful from time to time. Basically, it is a dead simple class that allows you to find the value of Binding; here is the full code listing:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Data;
 
namespace Cinch
{
    /// <summary>
    /// Use this evaluator when you do not know the return type expected.
    /// The return type will always be Object, and you will have to deal with
    /// that outside of this class
    /// </summary>
    /// <remarks>
    /// Recommended usage:
    /// <code>
    ///     TextBox positionedTextBox = new TextBox();
    ///     Binding positionBinding = new Binding("Minute");
    ///     positionBinding.Source = System.DateTime.Now;
    ///     positionedTextBox.SetBinding(Canvas.TopProperty, positionBinding);
    ///     
    ///     //Use GenericBindingEvaluator to get Bound Value
    ///     BindingEvaluator be = new BindingEvaluator();
    ///     Object x = be.GetBoundValue(positionBinding);
    ///
    /// </code>
    /// </remarks>
    public class BindingEvaluator : DependencyObject
    {
        #region DPs
        /// <summary>
        /// Dummy internal DP, to bind and get value from
        /// </summary>
        public static readonly DependencyProperty DummyProperty = 
        DependencyProperty.Register(
            "Dummy", typeof(Object), typeof(DependencyObject), 
        new UIPropertyMetadata(null));
 
        public Object Dummy
        {
            get { return (Object)GetValue(DummyProperty); }
            set { SetValue(DummyProperty, value); }
        }
        #endregion
 
        #region Public Methods
        /// <summary>
        /// Evaluate the binding
        /// </summary>
        /// <param name="bindingToEvaluate">The BindingBase to get the value of</param>
        /// <returns>The result of the BindingBase</returns>
        public Object GetBoundValue(BindingBase bindingToEvaluate)
        {
            BindingOperations.SetBinding(this, 
        BindingEvaluator.DummyProperty, bindingToEvaluate);
            return this.Dummy;
        }
        #endregion
    }

    /// <summary>
    /// Use this evaluator when you know the return type expected
    /// </summary>
    /// <typeparam name="T">The return type expected from the Binding</typeparam>
    /// <remarks>
    /// Recommended usage:
    /// <code>
    ///     TextBox positionedTextBox = new TextBox();
    ///     Binding positionBinding = new Binding("Minute");
    ///     positionBinding.Source = System.DateTime.Now;
    ///     positionedTextBox.SetBinding(Canvas.TopProperty, positionBinding);
    ///     
    ///     //Use GenericBindingEvaluator to get Bound Value
    ///     GenericBindingEvaluator<Int32> be = 
    ///       new GenericBindingEvaluator<Int32>();
    ///     Int32 x = be.GetBoundValue(positionBinding);
    ///
    /// </code>
    /// </remarks>
    public class GenericBindingEvaluator<T> : DependencyObject
    {
        #region DPs
        /// <summary>
        /// Dummy internal DP, to bind and get value from
        /// </summary>
        public static readonly DependencyProperty DummyProperty = 
        DependencyProperty.Register(
            "Dummy", typeof(T), typeof(DependencyObject), 
        new UIPropertyMetadata(null));
 
        public T Dummy
        {
            get { return (T)GetValue(DummyProperty); }
            set { SetValue(DummyProperty, value); }
        }
        #endregion
 
        #region Public Methods
        /// <summary>
        /// Evaluate the binding
        /// </summary>
        /// <param name="bindingToEvaluate">The BindingBase to get the value of</param>
        /// <returns>The result of the BindingBase</returns>
        public T GetBoundValue(BindingBase bindingToEvaluate)
        {
            BindingOperations.SetBinding(this, 
        GenericBindingEvaluator<T>.DummyProperty, bindingToEvaluate);
            return this.Dummy;
        }
        #endregion
    }
}

See the recommended usage comments in the code chunk above to see how to use this class.

ObservableDictionary<TKey, TValue> (WPF Only)

This is only available for WPF.

I stole this directly from Dr. WPF, and it is a fantastically well written class that is basically a bindable ObservableDictionary, like it says on the tin.

TreeHelper (WPF Only)

This is only available for WPF.

When working with WPF, you will be working with the VisualTree, so it is useful to have some helper to aid in the drudgery. Fellow WPF Disciple Philip Sumi has a nice class called TreeHelper, which I have now included in Cinch V2, which offers various methods such as:

  • public static T TryFindParent<T>(this DependencyObject child) where T : DependencyObject
  • public static DependencyObject GetParentObject(this DependencyObject child)
  • public static IEnumerable<T> FindChildren<T>(this DependencyObject source) where T : DependencyObject
  • public static IEnumerable<DependencyObject> GetChildObjects(this DependencyObject parent)
  • public static T TryFindFromPoint<T>(UIElement reference, Point point) where T : DependencyObject

It really is quite a useful class.

That's It ....For Now

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 those 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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


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

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

Both of these at Sussex University UK.

Award(s)

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

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

Comments and Discussions

 
QuestionMy vote of 5 Pin
Kenneth Haugland21-Jan-16 16:19
mvaKenneth Haugland21-Jan-16 16:19 
AnswerRe: My vote of 5 Pin
Sacha Barber22-Jan-16 0:54
Sacha Barber22-Jan-16 0:54 
GeneralRe: My vote of 5 Pin
Kenneth Haugland22-Jan-16 3:16
mvaKenneth Haugland22-Jan-16 3:16 
GeneralRe: My vote of 5 Pin
Sacha Barber22-Jan-16 6:52
Sacha Barber22-Jan-16 6:52 
GeneralRe: My vote of 5 Pin
Kenneth Haugland22-Jan-16 19:15
mvaKenneth Haugland22-Jan-16 19:15 
GeneralRe: My vote of 5 Pin
Sacha Barber22-Jan-16 20:33
Sacha Barber22-Jan-16 20:33 
GeneralRe: My vote of 5 Pin
Kenneth Haugland25-Jan-16 7:41
mvaKenneth Haugland25-Jan-16 7:41 
GeneralRe: My vote of 5 Pin
Sacha Barber25-Jan-16 21:30
Sacha Barber25-Jan-16 21:30 
GeneralRe: My vote of 5 Pin
Kenneth Haugland27-Jan-16 23:22
mvaKenneth Haugland27-Jan-16 23:22 
GeneralRe: My vote of 5 Pin
Sacha Barber28-Jan-16 2:08
Sacha Barber28-Jan-16 2:08 
GeneralRe: My vote of 5 Pin
Kenneth Haugland28-Jan-16 3:42
mvaKenneth Haugland28-Jan-16 3:42 
GeneralRe: My vote of 5 Pin
Sacha Barber28-Jan-16 5:22
Sacha Barber28-Jan-16 5:22 
QuestionWhy is ObservableDictionary.CollectionChanged protected? Pin
Member 1182201630-Nov-15 22:41
Member 1182201630-Nov-15 22:41 
GeneralMy vote of 5 Pin
SuperJMN-CandyBeat24-Jun-13 23:29
SuperJMN-CandyBeat24-Jun-13 23:29 
GeneralRe: My vote of 5 Pin
Sacha Barber25-Jun-13 0:35
Sacha Barber25-Jun-13 0:35 
GeneralSimpleCommand Reflection Problem [modified] Pin
Tobias_H5-May-11 2:13
Tobias_H5-May-11 2:13 
GeneralRe: SimpleCommand Reflection Problem Pin
Sacha Barber6-May-11 5:52
Sacha Barber6-May-11 5:52 
GeneralRe: SimpleCommand Reflection Problem Pin
Sacha Barber6-May-11 20:35
Sacha Barber6-May-11 20:35 
GeneralAbout EventArgs in Commands + a vote of 5 Pin
jradxl314-Apr-11 6:03
jradxl314-Apr-11 6:03 
GeneralRe: About EventArgs in Commands + a vote of 5 Pin
Sacha Barber14-Apr-11 6:19
Sacha Barber14-Apr-11 6:19 
GeneralSimpleCommand Pin
seb-7916-Feb-11 23:11
seb-7916-Feb-11 23:11 
GeneralRe: SimpleCommand Pin
Sacha Barber17-Feb-11 20:05
Sacha Barber17-Feb-11 20:05 
GeneralMy vote of 5 Pin
Daniel Vaughan14-Jan-11 13:39
Daniel Vaughan14-Jan-11 13:39 
GeneralRe: My vote of 5 Pin
Sacha Barber14-Jan-11 20:19
Sacha Barber14-Jan-11 20:19 
Thanks Daniel, you should see the new article with it working with PRISM4, that is well cool I feel : Showcasing Cinch MVVM framework / PRISM 4 interoperability[^]

Best of both frameworks very easily.

As always my friend thanks.
Sacha Barber
  • Microsoft Visual C# MVP 2008-2011
  • Codeproject MVP 2008-2011
Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue

My Blog : sachabarber.net

GeneralRe: My vote of 5 Pin
Sacha Barber14-Jan-11 20:20
Sacha Barber14-Jan-11 20:20 

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

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