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 Area |
Class Name |
WPF |
Silverlight (4 or above) |
Both |
Business objects |
EditableValidatingObject.cs |
|
|
Yes |
Business objects |
ValidatingObject.cs |
|
|
Yes |
Business objects |
DataWrapper.cs |
|
|
Yes |
Commands |
EventToCommandArgs.cs |
|
|
Yes |
Commands |
SimpleCommand.cs |
|
|
Yes |
Commands |
WeakEventHandlerManager.cs |
|
|
Yes |
Events |
CloseRequestEventArgs.cs |
|
|
Yes |
Events |
UICompletedEventArgs.cs |
|
|
Yes |
WeakEvents |
WeakEvent.cs |
|
|
Yes |
WeakEvents |
WeakEventHelper.cs |
|
|
Yes |
WeakEvents |
WeakEventProxy.cs |
|
|
Yes |
Extension Methods |
DispatcherExtensions.cs |
Yes |
|
|
Extension Methods |
GenericListExtensions.cs |
|
Yes |
|
Interactivity Actions |
CommandDrivenGoToStateAction.cs |
|
|
Yes |
Interactivity Behaviours |
FocusBehaviourBase.cs |
Yes |
|
|
Interactivity Behaviours |
NumericTextBoxBehaviour.cs |
Yes |
|
|
Interactivity Behaviours |
SelectorDoubleClickCommandBehavior.cs |
Yes |
|
|
Interactivity Behaviours |
TextBoxFocusBehavior.cs |
Yes |
|
|
Interactivity Triggers |
CompletedAwareCommandTrigger.cs |
|
|
Yes |
Interactivity Triggers |
CompletedAwareGotoStateCommandTrigger.cs |
|
|
Yes |
Interactivity Triggers |
EventToCommandTrigger.cs |
|
|
Yes |
Messager Mediator |
MediatorMessageSinkAttribute.cs |
|
|
Yes |
Messager Mediator |
MediatorSingleton.cs |
|
|
Yes |
Services Implementation |
ChildWindowService.cs |
|
Yes |
|
Services Implementation |
SLMessageBoxService.cs |
|
Yes |
|
Services Implementation |
ViewAwareStatus.cs |
|
|
Yes |
Services Implementation |
ViewAwareStatusWindow.cs |
Yes |
|
|
Services Implementation |
VSMService.cs |
|
|
Yes |
Services Implementation |
WPFMessageBoxService.cs |
Yes |
|
|
Services Implementation |
WPFOpenFileService.cs |
Yes |
|
|
Services Implementation |
WPFSaveFileService.cs |
Yes |
|
|
Services Implementation |
WPFUIVisualizerService.cs |
Yes |
|
|
Services Interfaces |
IChildWindowService.cs |
|
Yes |
|
Services Interfaces |
IMessageBoxService.cs |
|
Yes |
|
Services Interfaces |
IViewAwareStatus.cs |
|
|
Yes |
Services Interfaces |
IViewAwareStatusWindow.cs |
Yes |
|
|
Services Interfaces |
IVSM.cs |
|
|
Yes |
Services Interfaces |
IMessageBoxService.cs |
Yes |
|
|
Services Interfaces |
IOpenFileService.cs |
Yes |
|
|
Services Interfaces |
ISaveFileService.cs |
Yes |
|
|
Services Interfaces |
IUIVisualizerService.cs |
Yes |
|
|
Services Test Implementations |
TestChildWindowService.cs |
|
Yes |
|
Services Test Implementations |
TestMessageBoxService.cs |
|
Yes |
|
Services Test Implementations |
TestViewAwareStatus.cs |
|
|
Yes |
Services Test Implementations |
TestViewAwareStatusWindow.cs |
Yes |
|
|
Services Test Implementations |
TestVSMService.cs |
|
|
Yes |
Services Test Implementations |
TestMessageBoxService.cs |
Yes |
|
|
Services Test Implementations |
TestOpenFileService.cs |
Yes |
|
|
Services Test Implementations |
TestSaveFileService.cs |
Yes |
|
|
Services Test Implementations |
TestUIVisualizerService.cs |
Yes |
|
|
Threading |
AddRangeObservableCollection.cs (this is a specific Silverlight implementation) |
|
Yes |
|
Threading |
AddRangeObservableCollection.cs (this is a specific WPF implementation) |
Yes |
|
|
Threading |
BackgroundTaskManager.cs |
|
|
Yes |
Threading |
ISynchronizationContext.cs |
|
|
Yes |
Threading |
UISynchronizationContext.cs |
|
|
Yes |
Threading |
ApplicationHelper.cs |
Yes |
|
|
Threading |
DispatcherNotifiedObservableCollection.cs |
Yes |
|
|
Menus |
CinchMenuItem.cs |
|
|
Yes |
Utilities |
ArgumentValidator.cs |
|
|
Yes |
Utilities |
IWeakEventListener.cs (this is a System class missing from Silverlight, so I created it) |
|
Yes |
|
Utilities |
ObservableHelper.cs |
|
|
Yes |
Utilities |
PropertyChangedEventManager.cs (this is a System class missing from Silverlight, so I created it) |
|
Yes |
|
Utilities |
PropertyObserver.cs |
|
|
Yes |
Utilities |
BindingEvaluator.cs |
Yes |
|
|
Utilities |
ObservableDictionary.cs |
Yes |
|
|
Utilities |
TreeHelper.cs |
Yes |
|
|
Validation |
RegexRule.cs |
|
|
Yes |
Validation |
Rule.cs |
|
|
Yes |
Validation |
SimpleRule.cs |
|
|
Yes |
ViewModels |
EditableValidatingViewModelBase.cs |
|
|
Yes |
ViewModels |
IViewStatusAwareInjectionAware.cs |
|
|
Yes |
ViewModels |
ValidatingViewModelBase.cs |
|
|
Yes |
ViewModels |
ViewMode.cs |
|
|
Yes |
ViewModels |
ViewModelBase.cs |
|
|
Yes |
ViewModels |
ViewModelBaseSLSpecific.cs |
|
Yes |
|
ViewModels |
ViewModelBaseWPFSpecific.cs |
Yes |
|
|
Workspaces |
ChildWindowResolver.cs |
|
Yes |
|
Workspaces |
CinchBootStrapper.cs (Silverlight version) |
|
Yes |
|
Workspaces |
CinchBootStrapper.cs (WPF version) |
Yes |
|
|
Workspaces |
PopupNameToViewLookupKeyMetadataAttribute.cs |
|
|
Yes |
Workspaces |
IWorkspaceAware.cs |
Yes |
|
|
Workspaces |
MockView.cs |
Yes |
|
|
Workspaces |
NavProps.cs |
Yes |
|
|
Workspaces |
PopupResolver.cs |
Yes |
|
|
Workspaces |
ViewnameToViewLookupKeyMetadataAttribute.cs |
Yes |
|
|
Workspaces |
ViewResolver.cs |
Yes |
|
|
Workspaces |
WorkspaceData.cs |
Yes |
|
|
Now that I have shown you what classes will work with WPF/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:
using System.Windows.Input;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Cinch
{
public interface ICompletionAwareCommand
{
WeakActionEvent<object> CommandCompleted { get; set; }
}
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);
}
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
public event EventHandler CanExecuteChanged;
#else
public event EventHandler CanExecuteChanged
{
add
{
if (canExecuteMethod != null)
{
CommandManager.RequerySuggested += value;
}
}
remove
{
if (canExecuteMethod != null)
{
CommandManager.RequerySuggested -= value;
}
}
}
#endif
[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:
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):
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:
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:
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:
<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:
<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:
public SimpleCommand<Object, Object> SelectorDoubleClickCommand { get; private set; }
SelectorDoubleClickCommand =
new SimpleCommand<Object, Object>(ExecuteSelectorDoubleClickCommand);
private void ExecuteSelectorDoubleClickCommand(Object args)
{
}
If however you want to know about the EventArgs
that caused the Command to fire, you would do something like this in your ViewModel:
public SimpleCommand<Object, EventToCommandArgs>
SelectorDoubleClickCommand { get; private set; }
SelectorDoubleClickCommand =
new SimpleCommand<Object, EventToCommandArgs>(ExecuteSelectorDoubleClickCommand);
private void ExecuteSelectorDoubleClickCommand(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, 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:
- The
ViewModelBase
class has a public method called RaiseFocusEvent(String focusProperty)
which raises a ViewModelBase
event called FocusRequested
- Then there is a Blend Behaviour called
TextBoxFocusBehavior
, which listens for the ViewModelBase FocusRequested
event
- 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:
public event Action<String> FocusRequested;
public void RaiseFocusEvent(String focusProperty)
{
FocusRequested(focusProperty);
}
And then we have something like this in the TextBoxFocusBehavior
:
public class TextBoxFocusBehavior : FocusBehaviorBase
{
#region Protected Methods
protected DependencyProperty GetSourceProperty()
{
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
public static readonly DependencyProperty IsUsingDataWrappersProperty =
DependencyProperty.Register("IsUsingDataWrappers",
typeof(bool), typeof(TextBoxFocusBehavior),
new FrameworkPropertyMetadata((bool)false));
public bool IsUsingDataWrappers
{
get { return (bool)GetValue(IsUsingDataWrappersProperty); }
set { SetValue(IsUsingDataWrappersProperty, value); }
}
#endregion
#endregion
}
You can see that this Blend Behaviour supports DataWrapper
s 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.
public abstract class FocusBehaviorBase : Behavior<FrameworkElement>
{
#region Protected Methods
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;
}
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 DataWrapper
s. 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:
RaiseFocusEvent("ImageRating");
And in your XAML for your TextBox
, have something like this:
<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:
public class CompletedAwareCommandTrigger : TriggerBase<FrameworkElement>
{
#region DPs
#region Command
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register("Command", typeof(ICompletionAwareCommand),
typeof(CompletedAwareCommandTrigger), null);
public ICompletionAwareCommand Command
{
get { return (ICompletionAwareCommand)GetValue(CommandProperty); }
set { SetValue(CommandProperty, value); }
}
#endregion
#endregion
#region Overrides
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)
{
InvokeActions(parameter);
}
#endregion
}
And this is how we might use this in some XAML:
<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:
public SimpleCommand<Object, Object> ShowActionsCommandReversed { get; private set; }
ShowActionsCommandReversed = new SimpleCommand<Object, Object>((input) => { });
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:
But sometimes you may require your VisualStateGroup
s 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:
public class CompletedAwareGoToStateCommandTrigger : TriggerBase<FrameworkElement>
{
#region DPs
#region Command
public static readonly DependencyProperty CommandProperty =
DependencyProperty.Register("Command", typeof(ICompletionAwareCommand),
typeof(CompletedAwareGoToStateCommandTrigger), null);
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
public bool IsBeingUsedAtRootLevel
{
get { return (bool)GetValue(IsBeingUsedAtRootLevelProperty); }
set { SetValue(IsBeingUsedAtRootLevelProperty, value); }
}
#endregion
#endregion
#region Overrides
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)
{
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):
<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:
public SimpleCommand<String, String> GoToStateCommand { get; private set; }
GoToStateCommand = new SimpleCommand<String, String>(
(parameter) => { return !string.IsNullOrEmpty(parameter); },
(input)=> {});
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:
ShowActionsCommand = new SimpleCommand<Object, Object>(ExecuteShowActionsCommand);
HideActionsCommand = new SimpleCommand<Object, Object>(ExecuteHideActionsCommand);
And then in your View, you might have something like this:
<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:
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:
public SimpleCommand<Object, Object> ShowActionsCommand { get; private set; }
ShowActionsCommand = new SimpleCommand<Object, Object>(ExecuteShowActionsCommand);
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:
public SimpleCommand<Object,EventToCommandArgs>
ViewEventToVMFiredCommand { get; private set; } }
ViewEventToVMFiredCommand =
new SimpleCommand<Object,EventToCommandArgs>(ExecuteViewEventToVMFiredCommand);
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:
<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 VisualStateGroup
s are directly under the root node in the VisualTree
, as the standard VisualStateManager
is used.
public SimpleCommand<String, String> GoToStateCommand { get; private set; }
GoToStateCommand = new SimpleCommand<String, String>(
(parameter) => { return !string.IsNullOrEmpty(parameter); },
(input)=> {});
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:
<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 DataTemplate
s 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 DataTemplate
s workspace approach that a Cinch V1'ish type app uses.
ViewModel design
The ViewModel first and DataTemplate
s 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 DataTemplate
s 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:
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:
<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 DataTemplate
s, where there is expected to be a ObservableCollection<ViewModelBase>
somewhere, and some DataTemplate
s to match against the specific ViewModelBase
instances. Something like this example that shows a View that has a ObservableCollection<ViewModelBase>
bound to a TabControl
:
<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:
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
{
public class DataContextAwareViewModelInitializer : BasicViewModelInializer
{
public DataContextAwareViewModelInitializer(MEFedMVVMResolver resolver)
: base (resolver )
{ }
public override void CreateViewModel(Export viewModelContext,
FrameworkElement containerElement)
{
if (!Designer.IsInDesignMode) {
#if SILVERLIGHT
RoutedEventHandler handler = null;
handler = delegate
{
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
{
if (containerElement.DataContext != null)
{
resolver.SatisfyImports(
containerElement.DataContext, containerElement);
}
containerElement.DataContextChanged -= handler;
};
if (containerElement.DataContext == null)
containerElement.DataContextChanged += handler;
else {
handler(null, default(DependencyPropertyChangedEventArgs));
}
#endif
}
if(Designer.IsInDesignMode)
{
base.CreateViewModel(viewModelContext, containerElement );
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 DataTemplate
s 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:
- The ability to use full View first design time data support offered by MeffedMVVM
- 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 DataTemplate
s; 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:
- 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
).
- Within the View that has the need to show the sub views, there is a single
DataTemplate
that matches against the type WorkspaceData
.
- Within the
DataTemplate
for the WorkspaceData
, there is an Attached DP which uses the bound WorkspaceData
to deduce what View should be loaded.
- 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:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
namespace Cinch
{
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
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
public Object ViewModelInstance { get; set; }
public SimpleCommand<Object, Object> CloseWorkSpaceCommand { get; private set; }
static PropertyChangedEventArgs isCloseableArgs =
ObservableHelper.CreateArgs<WorkspaceData>(x => x.IsCloseable);
public Boolean IsCloseable
{
get { return isCloseable; }
set
{
isCloseable = value;
NotifyPropertyChanged(isCloseableArgs);
}
}
public bool HasImage
{
get
{
return !string.IsNullOrEmpty(ImagePath);
}
}
static PropertyChangedEventArgs imagePathArgs =
ObservableHelper.CreateArgs<WorkspaceData>(x => x.ImagePath);
public string ImagePath
{
get { return imagePath; }
set
{
imagePath = value;
NotifyPropertyChanged(imagePathArgs);
}
}
static PropertyChangedEventArgs viewLookupKeyArgs =
ObservableHelper.CreateArgs<WorkspaceData>(x => x.ViewLookupKey);
public string ViewLookupKey
{
get { return viewLookupKey; }
set
{
viewLookupKey = value;
NotifyPropertyChanged(viewLookupKeyArgs);
}
}
static PropertyChangedEventArgs dataValueArgs =
ObservableHelper.CreateArgs<WorkspaceData>(x => x.DataValue);
public object DataValue
{
get { return dataValue; }
set
{
dataValue = value;
NotifyPropertyChanged(dataValueArgs);
}
}
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;
protected void NotifyPropertyChanged(PropertyChangedEventArgs args)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, args);
}
}
protected void NotifyPropertyChanged(String propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
#region IDisposable Members
public void Dispose()
{
Mediator.Instance.Unregister(this);
this.OnDispose();
}
protected virtual void OnDispose()
{
}
#if DEBUG
~WorkspaceData()
{
}
#endif
#endregion }
}
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.
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
{
public abstract partial class ViewModelBase
{
#region Data
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:
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);
}
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 (result == CustomDialogResults.No)
{
e.Cancel = true;
}
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:
<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).
*** 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:
- The
ShouldHideHostWhenNoItems
Attached DP: Can be used to hide the ItemsControl
host when there are no more displayable items left.
- 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).
<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:
#region ViewCreator
public static readonly DependencyProperty ViewCreatorProperty =
DependencyProperty.RegisterAttached("ViewCreator",
typeof(WorkspaceData), typeof(NavProps),
new FrameworkPropertyMetadata((WorkspaceData)null,
new PropertyChangedCallback(OnViewCreatorChanged)));
public static WorkspaceData GetViewCreator(DependencyObject d)
{
return (WorkspaceData)d.GetValue(ViewCreatorProperty);
}
public static void SetViewCreator(DependencyObject d, WorkspaceData value)
{
d.SetValue(ViewCreatorProperty, value);
}
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:
<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.
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
.
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
:
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 DataTemplate
s, 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:
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.ItemContainerGenerator.StatusChanged +=
ItemContainerGenerator_StatusChanged;
}
void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if (this.ItemContainerGenerator.Status ==
GeneratorStatus.ContainersGenerated)
{
this.ItemContainerGenerator.StatusChanged -=
ItemContainerGenerator_StatusChanged;
UpdateSelectedItem();
}
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_itemsHolder = GetTemplateChild("PART_ItemsHolder") as Panel;
UpdateSelectedItem();
}
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);
}
}
}
UpdateSelectedItem();
break;
case NotifyCollectionChangedAction.Replace:
throw new NotImplementedException("Replace not implemented yet");
}
}
protected override void OnSelectionChanged(SelectionChangedEventArgs e)
{
base.OnSelectionChanged(e);
UpdateSelectedItem();
}
void UpdateSelectedItem()
{
if (_itemsHolder == null)
{
return;
}
TabItem item = GetSelectedTabItem();
if (item != null)
{
CreateChildContentPresenter(item);
}
foreach (ContentPresenter child in _itemsHolder.Children)
{
child.Visibility = ((child.Tag as TabItem).IsSelected) ?
Visibility.Visible : Visibility.Collapsed;
}
}
ContentPresenter CreateChildContentPresenter(object item)
{
if (item == null)
{
return null;
}
ContentPresenter cp = FindChildContentPresenter(item);
if (cp != null)
{
return cp;
}
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;
}
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;
}
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:
<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>
-->
<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 TabItem
s, where the PART_Close
Button
s is bound to the WorkspaceData
that was used to create the DataTemplate
being applied to the TabItem
.
<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:
- Silverlight's
TabControl
lacks some of the overrides and general internals that I need to make it work the same as in WPF.
- 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 WeakEvent
s/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:
using System;
using System.Threading;
using System.Windows.Threading;
namespace Cinch
{
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:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Threading;
using System.Threading;
namespace Cinch
{
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();
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:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Data;
namespace Cinch
{
public class BindingEvaluator : DependencyObject
{
#region DPs
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
public Object GetBoundValue(BindingBase bindingToEvaluate)
{
BindingOperations.SetBinding(this,
BindingEvaluator.DummyProperty, bindingToEvaluate);
return this.Dummy;
}
#endregion
}
public class GenericBindingEvaluator<T> : DependencyObject
{
#region DPs
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
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.