Table of Contents
Introduction
Last time we talked about the Cinch V2 WPF demo app. In this article, we will be going
through the Cinch V2 Silvelight demo app, provided with the Cinch V2 codebase
over at Cinch's CodePlex site.
As promised, within each article, I shall be showing the Cinch V2 compatibility matrix.
The compatibility matrix shows a list of classes along with their general work area, and whether they are compatible with WPF or SL or both.
Work Area |
Class Name |
WPF |
Silverlight (4 or above) |
Both |
Business objects |
EditableValidatingObject.cs |
|
|
Yes |
Business objects |
ValidatingObject.cs |
|
|
Yes |
Business objects |
DataWrapper.cs |
|
|
Yes |
Commands |
EventToCommandArgs.cs |
|
|
Yes |
Commands |
SimpleCommand.cs |
|
|
Yes |
Commands |
WeakEventHandlerManager.cs |
|
|
Yes |
Events |
CloseRequestEventArgs.cs |
|
|
Yes |
Events |
UICompletedEventArgs.cs |
|
|
Yes |
WeakEvents |
WeakEvent.cs |
|
|
Yes |
WeakEvents |
WeakEventHelper.cs |
|
|
Yes |
WeakEvents |
WeakEventProxy.cs |
|
|
Yes |
Extension Methods |
DispatcherExtensions.cs |
Yes |
|
|
Extension Methods |
GenericListExtensions.cs |
|
Yes |
|
Interactivity Actions |
CommandDrivenGoToStateAction.cs |
|
|
Yes |
Interactivity Behaviours |
FocusBehaviourBase.cs |
Yes |
|
|
Interactivity Behaviours |
NumericTextBoxBehaviour.cs |
Yes |
|
|
Interactivity Behaviours |
SelectorDoubleClickCommandBehavior.cs |
Yes |
|
|
Interactivity Behaviours |
TextBoxFocusBehavior.cs |
Yes |
|
|
Interactivity Triggers |
CompletedAwareCommandTrigger.cs |
|
|
Yes |
Interactivity Triggers |
CompletedAwareGotoStateCommandTrigger.cs |
|
|
Yes |
Interactivity Triggers |
EventToCommandTrigger.cs |
|
|
Yes |
Messager Mediator |
MediatorMessageSinkAttribute.cs |
|
|
Yes |
Messager Mediator |
MediatorSingleton.cs |
|
|
Yes |
Services Implementation |
ChildWindowService.cs |
|
Yes |
|
Services Implementation |
SLMessageBoxService.cs |
|
Yes |
|
Services Implementation |
ViewAwareStatus.cs |
|
|
Yes |
Services Implementation |
ViewAwareStatusWindow.cs |
Yes |
|
|
Services Implementation |
VSMService.cs |
|
|
Yes |
Services Implementation |
WPFMessageBoxService.cs |
Yes |
|
|
Services Implementation |
WPFOpenFileService.cs |
Yes |
|
|
Services Implementation |
WPFSaveFileService.cs |
Yes |
|
|
Services Implementation |
WPFUIVisualizerService.cs |
Yes |
|
|
Services Interfaces |
IChildWindowService.cs |
|
Yes |
|
Services Interfaces |
IMessageBoxService.cs |
|
Yes |
|
Services Interfaces |
IViewAwareStatus.cs |
|
|
Yes |
Services Interfaces |
IViewAwareStatusWindow.cs |
Yes |
|
|
Services Interfaces |
IVSM.cs |
|
|
Yes |
Services Interfaces |
IMessageBoxService.cs |
Yes |
|
|
Services Interfaces |
IOpenFileService.cs |
Yes |
|
|
Services Interfaces |
ISaveFileService.cs |
Yes |
|
|
Services Interfaces |
IUIVisualizerService.cs |
Yes |
|
|
Services Test Implementations |
TestChildWindowService.cs |
|
Yes |
|
Services Test Implementations |
TestMessageBoxService.cs |
|
Yes |
|
Services Test Implementations |
TestViewAwareStatus.cs |
|
|
Yes |
Services Test Implementations |
TestViewAwareStatusWindow.cs |
Yes |
|
|
Services Test Implementations |
TestVSMService.cs |
|
|
Yes |
Services Test Implementations |
TestMessageBoxService.cs |
Yes |
|
|
Services Test Implementations |
TestOpenFileService.cs |
Yes |
|
|
Services Test Implementations |
TestSaveFileService.cs |
Yes |
|
|
Services Test Implementations |
TestUIVisualizerService.cs |
Yes |
|
|
Threading |
AddRangeObservableCollection.cs (this is specific SL implementation) |
|
Yes |
|
Threading |
AddRangeObservableCollection.cs (this is specific WPF implementation) |
Yes |
|
|
Threading |
BackgroundTaskManager.cs |
|
|
Yes |
Threading |
ISynchronizationContext.cs |
|
|
Yes |
Threading |
UISynchronizationContext.cs |
|
|
Yes |
Threading |
ApplicationHelper.cs |
Yes |
|
|
Threading |
DispatcherNotifiedObservableCollection.cs |
Yes |
|
|
Menus |
CinchMenuItem.cs |
|
|
Yes |
Utilities |
ArgumentValidator.cs |
|
|
Yes |
Utilities |
IWeakEventListener.cs (this is a System class missing from SL, so I created it) |
|
Yes |
|
Utilities |
ObservableHelper.cs |
|
|
Yes |
Utilities |
PropertyChangedEventManager.cs (this is a System class missing from SL, so I created it) |
|
Yes |
|
Utilities |
PropertyObserver.cs |
|
|
Yes |
Utilities |
BindingEvaluator.cs |
Yes |
|
|
Utilities |
ObservableDictionary.cs |
Yes |
|
|
Utilities |
TreeHelper.cs |
Yes |
|
|
Validation |
RegexRule.cs |
|
|
Yes |
Validation |
Rule.cs |
|
|
Yes |
Validation |
SimpleRule.cs |
|
|
Yes |
ViewModels |
EditableValidatingViewModelBase.cs |
|
|
Yes |
ViewModels |
IViewStatusAwareInjectionAware.cs |
|
|
Yes |
ViewModels |
ValidatingViewModelBase.cs |
|
|
Yes |
ViewModels |
ViewMode.cs |
|
|
Yes |
ViewModels |
ViewModelBase.cs |
|
|
Yes |
ViewModels |
ViewModelBaseSLSpecific.cs |
|
Yes |
|
ViewModels |
ViewModelBaseWPFSpecific.cs |
Yes |
|
|
Workspaces |
ChildWindowResolver.cs |
|
Yes |
|
Workspaces |
CinchBootStrapper.cs (SL Version) |
|
Yes |
|
Workspaces |
CinchBootStrapper.cs (WPF version) |
Yes |
|
|
Workspaces |
PopupNameToViewLookupKeyMetadataAttribute.cs |
|
|
Yes |
Workspaces |
IWorkspaceAware.cs |
Yes |
|
|
Workspaces |
MockView.cs |
Yes |
|
|
Workspaces |
NavProps.cs |
Yes |
|
|
Workspaces |
PopupResolver.cs |
Yes |
|
|
Workspaces |
ViewnameToViewLookupKeyMetadataAttribute.cs |
Yes |
|
|
Workspaces |
ViewResolver.cs |
Yes |
|
|
Workspaces |
WorkspaceData.cs |
Yes |
|
|
Now that I have shown you what classes will work with WPF/SL, let's get on with the rest of this article, shall we? But first, here are the links
to the old Cinch V1 articles.
In case you missed Cinch V1, and have an interest in MVVM, I would strongly recommend that you read
all the Cinch V1 articles first, as it will give you a much deeper understanding of the content that will be presented
in these Cinch V2 articles.
CinchV1 Article Links
Some of you may never have seen the old Cinch V1 articles, so I will also include a list of these here,
and where the Cinch V2 still uses the same functionality as Cinch V1, I will be redirecting people
to these articles.
CinchV2 Article Links
OK, so that is what the article roadmap looks like. I guess it is now time to dive into the guts of this article, so let's go:
What Does It Do
For Cinch V1, I created a LOB (line of business) application, but at work, I am working on a
massive LOB app, and truth be known, I was just tired of creating yet another LOB app, and the stuff that is common between Cinch
V1 and V2 can be seen quite clearly in the old Cinch V1 demos. The things that have really changed are the UI Services, and the
attached properties have now become Blend behaviours.
So this time I decided I would do something a little bit more creative, much to the dismay of several readers. However, some
readers may be pleased to know that I have also been contacted by two other CodeProject users who use Cinch,
and one will be writing a Cinch V2 LOB article, and another will be writing a VB.NET Cinch
V2 application, both of which I will be linking to from the Cinch
CodePlex site when these CodeProject users let me know that they are done writing the articles.
Anyway. that is neither here nor there. As I say, what I decided to do was write something a bit different. So without further ado, what does the Silverlight demo app do?
Well, I think this can be summarized by the following bullet points:
- There is a main
TabControl
that allows you to enter a username to play a game of O's and X's as against the computer
- There is also a O's and X's control
- There is a list of previously played games that have been played where the user can call up a
ChildWindow
representing the played game
Now, that may not look like much, but believe me, that is enough to showcase most of Cinch's Silverlight functionality.
What Does It Look Like
Now that I have talked about what it does, let's have a look at what it looks like, shall we?
When you start the app, it should look something like this:
MainWindow
is sitting there waiting for you to type in a username to play a game as:
When you enter a name and press OK, you can use the Flip button (top right) to show a new GameView
and GameStatView
.
GameView
allows you to play a game by clicking on the grid buttons:
GameStatView
shows you a list of all the previously played games, which you can choose to view using the buttons provided.
If you click one of the buttons of the GameStatView
, you will be presented with a PlayedGameChildWindow ChildWindow
which will show you the previous game state.
Overall Structure
The following diagram illustrates the overall structure of the Views/ViewModels and popup ChildWindow
s for the Silverlight demo app. There are a number
of helper classes and services but I will discuss those as we get to them. For now, just take note of the overall structure of the Silverlight demo app as shown below:
How Does It Work
The next three sections shall attempt to outline all the functions that the Views/ViewModels and popup ChildWindow
s perform in the Silverlight demo app.
ChildWindows
In this section, we will talk about how you can show popups from your ViewModels.
Ensuring That ChildWindow is Available to Show
There is a service that deals with showing ChildWindow
s called IChildWindowService
, that holds a Dictionary<string, Type>
such that a consumer of this IChildWindowService
service can simply request a ChildWindow
by name (string) from the internal
Dictionary<string, Type>
, and then the IChildWindowService
will locate that entry in the Dictionary<string, Type>
and create a new instance of that Type
and show it.
For clarity, here is the full IChildWindowService
service implementation for SL:
using System;
using System.Collections.Generic;
using System.Windows;
using System.ComponentModel.Composition;
using System.Windows.Controls;
using System.Threading;
using MEFedMVVM.ViewModelLocator;
using System.ComponentModel;
namespace Cinch
{
[PartCreationPolicy(CreationPolicy.Shared)]
[ExportService(ServiceType.Both, typeof(IChildWindowService))]
public class ChildWindowService : IChildWindowService
{
#region Data
private readonly Dictionary<string, Type> _registeredWindows;
#endregion
#region Ctor
public ChildWindowService()
{
_registeredWindows = new Dictionary<string, Type>();
}
#endregion
#region Public Methods
public void Register(Dictionary<string, Type> startupData)
{
foreach (var entry in startupData)
Register(entry.Key, entry.Value);
}
public void Register(string key, Type winType)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException("key");
if (winType == null)
throw new ArgumentNullException("winType");
if (!typeof(ChildWindow).IsAssignableFrom(winType))
throw new ArgumentException("winType must be of type ChildWindow");
lock (_registeredWindows)
{
_registeredWindows.Add(key, winType);
}
}
public bool Unregister(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException("key");
lock (_registeredWindows)
{
return _registeredWindows.Remove(key);
}
}
public void Show(string key, object state, EventHandler<UICompletedEventArgs> completedProc)
{
ChildWindow win = CreateChildWindow(key, state, completedProc);
if (win != null)
{
win.Show();
}
}
#endregion
#region Private Methods
private ChildWindow CreateChildWindow(string key, object dataContext,
EventHandler<UICompletedEventArgs> completedProc)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException("key");
Type winType;
lock (_registeredWindows)
{
if (!_registeredWindows.TryGetValue(key, out winType))
return null;
}
var win = (ChildWindow)Activator.CreateInstance(winType);
if (dataContext is IViewStatusAwareInjectionAware)
{
IViewAwareStatus viewAwareStatus =
ViewModelRepository.Instance.Resolver.Container.GetExport<IViewAwareStatus>().Value;
viewAwareStatus.InjectContext((FrameworkElement)win);
((IViewStatusAwareInjectionAware)dataContext).InitialiseViewAwareService(viewAwareStatus);
}
win.DataContext = dataContext;
ViewModelBase bvm=null;
EventHandler<CloseRequestEventArgs> handler = ((s, e) =>
{
try
{
win.DialogResult = e.Result;
}
catch (InvalidOperationException)
{
win.Close();
}
});
if (dataContext != null)
{
bvm = dataContext as ViewModelBase;
if (bvm != null)
{
bvm.CloseRequest += handler;
}
}
win.Closed += (s, e) =>
{
bvm.CloseRequest -= handler;
if (completedProc != null)
{
completedProc(this, new UICompletedEventArgs()
{
State = dataContext,
Result = win.DialogResult
});
GC.Collect();
}
};
return win;
}
#endregion
}
}
For further reading, have a look at this link CinchV2_2.aspx#CoreServices and read
the IChildWindowService
section.
So you might be wondering how does this IChildWindowServiceDictionary<string, Type>
get populated in time to make sure that when a ChildWindow
is requested, that it is present in the Dictionary<string, Type>
. Well this can happen in two different ways.
Manually Adding Items to the Dictionary
You can add popup items manually to the IChildWindowService Dictionary<string, Type>
, at a suitable time such as on App construction or possibly even startup.
So you might have something like:
public partial class App: Application
{
public App()
{
ViewModelRepository.Instance.Resolver.Container.
GetExport<IChildWindowService >().Value.Register("ChildWindow1",
typeof(ChildWindow1));
InitializeComponent();
}
}
That line will ensure that the IChildWindowService Dictionary<string, Type>
is populated with the correct KeyValuePair
.
Automatically Finding Types That Are ChildWindow
Manually adding things is all well and good, but Cinch V2 does provide a better way, by the use of attributes, and a bootstrapper
which is run at startup. So if we have a popup window which we know we are going to use with the IChildWindowService
, all we have to do is attribute it up
as follows in its code-behind:
[PopupNameToViewLookupKeyMetadata("PlayedGameChildWindow",typeof(PlayedGameChildWindow))]
public partial class PlayedGameChildWindow : ChildWindow
{
}
So we now have a popup window which is attributed up, but that is only half the story, we need to make sure something examines these PopupNameToViewLookupKeyMetadata
attributes.
That is what the CinchBootStrapper
's job is. Basically, the CinchBootStrapper
takes an IEnumerable<Assembly>
to examine for any Type
s
in the passed-in IEnumerable<Assembly>
that have the PopupNameToViewLookupKeyMetadata
attribute on them, and if they do, they are added into
the IChildWindowService
ready for later use. All you must do is make sure to call the CinchBootStrapper
, again either on App construction or App startup.
Here is an example from the Cinch V2 WPF demo app:
public partial class App : Application
{
#region Initialisation
public App()
{
CinchBootStrapper.Initialise(new List<Assembly> { typeof(App).Assembly });
InitializeComponent();
}
#endregion
}
Showing a Specific ChildWindow
Once you have a ChildWindow
KeyValuePair
entry inside the IChildWindowService Dictionary<string, Type>
, it really
is child's play (pardon the pun) to show a ChildWindow
from a ViewModel. You would simply do something like this:
using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel;
using Cinch;
using MEFedMVVM.Common;
using MEFedMVVM.ViewModelLocator;
namespace CinchV2DemoSL
{
public class GameStatViewModel : ViewModelBase
{
private ICommand viewGameCommand;
private IChildWindowService ChildWindowService { get; set; }
public GameStatViewModel(string winnerName, string gameText)
{
ViewGameCommand = new SimpleCommand<Object, Object>(ExecuteViewGameCommand);
}
public SimpleCommand<Object, Object> ViewGameCommand { get; private set; }
private void ExecuteViewGameCommand(object o)
{
bool? dialogResult = null;
ChildWindowService.Show("PlayedGameChildWindow",
new PlayedGameViewModel(GameText), (s, e) =>
{
dialogResult = e.Result;
string result = dialogResult.HasValue && dialogResult.Value ? "ok" : "Cancel";
MessageBoxService.ShowInformation("You clicked " + result);
});
}
}
}
Important: ChildWindow
s in Silverlight are not the same as Modal dialogs, in WPF say, they look Modal and act Modal, but any code
after showing a ChildWindow
will continue to run. So when we show a ChildWindow
, we need to ensure no more code is run. How do we deal with the modified state
that the ChildWindow
may have manipulated I hear you ask. Well, quite simple, we use a callback, as shown in the code snippet above. We can simply hook up a
lambda there, and run some code when the ChildWindow
is closed.
Cinch V2 also provides a test double of the IChildWindowService
that you can use for testing.
So if you want to use a test version of one of the services, you simply need to inject the test version (TestChildWindowService
for the example above)
from your unit test code rather than the real one.
Application Management
There is really only one thing that is required to make the demo app work, and that is as follows:
App Construction
As I mentioned above in the ChildWindows section, Cinch V2 supports popup lookups and various other lookups by the use
of attributes, where Type
s that have these attributes are found on startup. But in order for this to work correctly, Cinch
needs to be told what Assemblies to look at. For the demo app, all Views/Popups are defined in the same Assembly
as the demo, so I only need
to tell Cinch to look for Cinch
attributed Type
s in that Assembly
. This is done by the following code in the App constructor:
public partial class App : Application
{
#region Initialisation
public App()
{
CinchBootStrapper.Initialise(new List<Assembly> { typeof(App).Assembly });
InitializeComponent();
}
#endregion
}
The Cinch BootStrapper accepts a IEnumerable<Assembly>
so you can pass in other DLLs if
you split your popups into different Assemblies.
Views/ViewsModels
There a number of ViewModels in the Cinch V2 Silverlight demo app. As such, we will examine each of these in turn and see how both
the View/ViewModel work together.
UserEntryView / UserEntryViewModel
This view is the first view that is presented when the Silverlight demo app starts. Its purpose is simple, get a name from the user which can be used to
store against games of O's and X's that the user plays using the rest of the Silverlight application.
There is really not too much to this view, let's have a look at the View first, shall we? Here is the relevant code from the UserEntryView
XAML:
<UserControl x:Class="CinchV2DemoSL.UserEntryView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="400"
Width="650" Height="370"
xmlns:CinchV2="clr-namespace:Cinch;assembly=Cinch.SL"
xmlns:meffed="http:\\www.codeplex.com\MEFedMVVM"
meffed:ViewModelLocator.ViewModel="UserEntryViewModel">
<Border BorderBrush="#ff656565" BorderThickness="2"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="VisualStateGroup">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0" To="InValidState"/>
</VisualStateGroup.Transitions>
<VisualState x:Name="InValidState">
<Storyboard>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetProperty="(UIElement.Visibility)"
Storyboard.TargetName="image">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="ValidState"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid x:Name="grid">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" Background="{StaticResource ViewBackGrounds}">
.......
.......
<TextBox Height="26" Margin="10,79,0,0" TextWrapping="Wrap"
VerticalAlignment="Top" Width="300"
Text="{Binding UserName.DataValue, Mode=TwoWay,
ValidatesOnDataErrors=True,
ValidatesOnExceptions=True,
ValidatesOnNotifyDataErrors=True}" HorizontalAlignment="Left"/>
<Image x:Name="image" Margin="0,-29,23,152"
HorizontalAlignment="Right" Width="134"
Source="/CinchV2DemoSL;component/Images/error.png"
Visibility="Collapsed">
<Image.Effect>
<DropShadowEffect Color="#7F979797"
ShadowDepth="12" Opacity="0.5"/>
</Image.Effect>
</Image>
</Grid>
......
......
......
<Button Content="Ok" Height="25" Margin="5"
Width="70" Foreground="Black"
Command="{Binding SaveUserNameCommand}"
HorizontalAlignment="Right"/>
......
......
......
</Grid>
</Border>
</UserControl>
There is not too much in there apart from a Button
which fires a SimpleCommand
and a TextBox
which binds
to the UserEntryViewModel.UserName
property, but there is also some VisualState
s that are used to show an error icon if the user doesn't
enter a valid user name (basically, can't be empty).
The other thing to note is the standard ViewModel resolution which is done using MeffedMVVM.
Now, how about the UserEntryViewModel
? Let's just recap on what it does:
- Allows a user to enter a name.
- The user name is validated, using a validation rule.
- When the user clicks the OK button, if the user has entered a valid name, the control ViewModel is deemed valid and the username is saved to a
file in
IsolatedStorage
using a non-core UI service, and the View is transitioned to the "ValidState" VisualState
.
- If however the user does not enter a username and clicks the OK button, the ViewModel is deemed invalid and the View is transitioned to the
"InvalidState"
VisualState
.
Let's examine each of these elements, shall we?
Entering a Valid UserName / Validation
This is easily achieved by inheriting from a ValidatingViewModelBase
and adding a Cinch SimpleRule
validation rule to a DataWrapper<T>
.
using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel.Composition;
using System.Threading;
using System.Collections.Generic;
using System.ComponentModel;
using Cinch;
using MEFedMVVM.ViewModelLocator;
namespace CinchV2DemoSL
{
[ExportViewModel("UserEntryViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class UserEntryViewModel : ValidatingViewModelBase
{
private DataWrapper<String> userName;
private IEnumerable<DataWrapperBase> cachedListOfDataWrappers;
private static SimpleRule userNameRule;
[ImportingConstructor]
public UserEntryViewModel(
.....
.....
.....)
{
UserName = new DataWrapper<String>(this, userNameChangeArgs);
UserName.IsEditable = true;
cachedListOfDataWrappers =
DataWrapperHelper.GetWrapperProperties<UserEntryViewModel>(this);
userName.AddRule(userNameRule);
}
static UserEntryViewModel()
{
userNameRule = new SimpleRule("DataValue", "UserName can not be empty",
(Object domainObject) =>
{
DataWrapper<String> obj = (DataWrapper<String>)domainObject;
return String.IsNullOrEmpty(obj.DataValue);
});
}
static PropertyChangedEventArgs userNameChangeArgs =
ObservableHelper.CreateArgs<UserEntryViewModel>(x => x.UserName);
public DataWrapper<String> UserName
{
get { return userName; }
private set
{
userName = value;
NotifyPropertyChanged(userNameChangeArgs);
}
}
static PropertyChangedEventArgs isValidChangeArgs =
ObservableHelper.CreateArgs<UserEntryViewModel>(x => x.IsValid);
public override bool IsValid
{
get
{
return base.IsValid &&
DataWrapperHelper.AllValid(cachedListOfDataWrappers);
}
}
}
}
Where we have the following declared in the XAML:
<TextBox Height="26" Margin="10,79,0,0" TextWrapping="Wrap"
VerticalAlignment="Top" Width="300"
Text="{Binding UserName.DataValue, Mode=TwoWay,
ValidatesOnDataErrors=True,
ValidatesOnExceptions=True,
ValidatesOnNotifyDataErrors=True}" HorizontalAlignment="Left"/>
Checking Whether the User Entry Was Valid
This is very simple. All there is to it is to use a SimpleCommand<T1,T2>
for the "OK" button and check the IsValid
property
as shown above. And if the user entered a valid name, transition the View to the "ValidState" VisualState
using the IVSM
service,
and save a IsolatedStorage
file with the user name in it. If the user entered some invalid data (as defined by the Cinch SimpleRule
validation rule),
simply transition the View to the "InvalidState" VisualState
using the IVSM
service.
The important parts of the UserEntryViewModel
are shown below:
using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.ComponentModel.Composition;
using System.Threading;
using System.Collections.Generic;
using System.ComponentModel;
using Cinch;
using MEFedMVVM.ViewModelLocator;
namespace CinchV2DemoSL
{
[ExportViewModel("UserEntryViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class UserEntryViewModel : ValidatingViewModelBase
{
private IMessageBoxService messageBoxService;
private IViewAwareStatus viewAwareStatusService;
private IChildWindowService childWidowService;
private IGameStorerProvider gameStorerProvider;
private IVSM visualStateManagerService;
[ImportingConstructor]
public UserEntryViewModel(
IMessageBoxService messageBoxService,
IViewAwareStatus viewAwareStatusService,
IChildWindowService childWidowService,
IGameStorerProvider gameStorerProvider,
IVSM visualStateManagerService)
{
this.messageBoxService = messageBoxService;
this.viewAwareStatusService = viewAwareStatusService;
this.childWidowService = childWidowService;
this.gameStorerProvider = gameStorerProvider;
this.visualStateManagerService = visualStateManagerService;
SaveUserNameCommand = new SimpleCommand<Object, Object>(ExecuteSaveUserNameCommand);
}
public SimpleCommand<Object, Object> SaveUserNameCommand { get; private set; }
private void ExecuteSaveUserNameCommand(Object args)
{
if (IsValid)
{
visualStateManagerService.GoToState("ValidState");
gameStorerProvider.WriteUserNameToFile(UserName.DataValue);
messageBoxService.ShowError("Successfully saved username, flip to play a game!");
}
else
{
visualStateManagerService.GoToState("InValidState");
messageBoxService.ShowError("The UserName entered is invalid it must contain a value");
}
}
......
......
}
}
The more eagle eyed amongst you may be thinking, but hey hang on a minute, what if I am Unit testing my ViewModel, I will not be in a web context and may
not even want my ViewModel to be using an IsolatedStorage
file. No problem. This saving of the user name to file is done by the use of a non-core service, called
IGameStorerProvider
, which looks like this:
public interface IGameStorerProvider
{
void StoreGameResults(string winnerName, String completeGameText);
void WriteUserNameToFile(string username);
string ReadUserNameFromFile();
void FetchGameResults(Action<List<Tuple<string, string>>> callback);
}
I have provided a runtime version of this, which looks like this:
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.Runtime, typeof(IGameStorerProvider))]
public class RunTimeGameStorerProvider : IGameStorerProvider
{
#region Data
private BackgroundTaskManager<object,List<Tuple<string, string>>> bgWorker =
new BackgroundTaskManager<object, List<Tuple<string, string>>>();
#endregion
#region IGameStorerProvider Members
public void StoreGameResults(string winnerName, string completeGameText)
{
IsolatedStorageHelper.StoreGameResults(winnerName, completeGameText);
}
public void WriteUserNameToFile(string username)
{
IsolatedStorageHelper.WriteUserNameToFile(username);
}
public string ReadUserNameFromFile()
{
return IsolatedStorageHelper.ReadUserNameFromFile();
}
public void FetchGameResults(Action<List<Tuple<string, string>>> callback)
{
bgWorker.TaskFunc = (argument) =>
{
return IsolatedStorageHelper.FetchGameResults();
};
bgWorker.CompletionAction = (result) =>
{
callback(result);
};
bgWorker.RunBackgroundTask();
}
#endregion
#region Public Properties
public BackgroundTaskManager<object, List<Tuple<string, string>>> BgWorker
{
get { return bgWorker; }
}
#endregion
}
And a design time version to supply design time data to Blend, which looks like this:
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.DesignTime, typeof(IGameStorerProvider))]
public class DesignTimeGameStorerProvider : IGameStorerProvider
{
#region IGameStorerProvider Members
public void StoreGameResults(string winnerName, string completeGameText)
{
}
public void WriteUserNameToFile(string username)
{
}
public string ReadUserNameFromFile()
{
return "Sacha";
}
public void FetchGameResults(Action<List<Tuple<string, string>>> callback)
{
List<Tuple<string, string>> results =
new List<Tuple<string, string>>();
results.Add(new Tuple<string,string>("Sacha won","O:X:X:O:O:X:X:O:O:"));
results.Add(new Tuple<string,string>("Computer won","O:O:O:X:X:X:X:O:O:"));
results.Add(new Tuple<string,string>("Sacha won","O:X:X:O:O:X:X:O:O:"));
callback(results);
}
#endregion
}
Now in your unit test, you can simply pass in a Mock or a test double of IGameStorerProvider
to do what you want to do in your ViewModel under test.
GameView / GameViewModel
At its most basic level, the GameView
simple allows users to play the computer in a game of O's and X's. When the game ends,
the winner (or draw) is stored to IsolatedStorage
. The user may choose to play again using the "Play Again" Image
. When a winner is found,
the GameView
shows a new VisualState
which does a little animation for the Winner text, using the winner's name (either computer
or the UserName typed in, in the first UserNameEntryView
).
A lot of the code in the GameViewModel
is about the logic to determine who the winner was, and managing the game state, which strictly has nothing to do with
Cinch, so I will not be writing about that, I will just concentrate on the bits that cover the goodies from the
Cinch framework.
Let's start with the "Play Again" Image
. If we examine the XAML, we can see that this uses the
Cinch EventToCommandTrigger
to fire a Cinch SimpleCommand when a MouseDown
event
occurs on the Image
.
<Image Height="30" Width="30" Margin="5,2,5,2"
Source="/CinchV2DemoSL;component/Images/repeat.png"
VerticalAlignment="Center">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonDown">
<CinchV2:EventToCommandTrigger
Command="{Binding RestartCommand}"
CommandParameter="5"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Image>
And this is what the relevant parts of the GameViewModel
look like to support this SimpleCommand
:
using System;
using System.Collections.Generic;
using System.Windows.Input;
using System.ComponentModel.Composition;
using System.ComponentModel;
using System.Linq;
using Cinch;
using MEFedMVVM.ViewModelLocator;
namespace CinchV2DemoSL
{
[ExportViewModel("GameViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class GameViewModel : ViewModelBase
{
private IVSM visualStateManagerService;
private IViewAwareStatus viewAwareStatusService;
private IGameStorerProvider gameStorerProvider;
[ImportingConstructor]
public GameViewModel(
IVSM visualStateManagerService,
IGameStorerProvider gameStorerProvider)
{
this.visualStateManagerService = visualStateManagerService;
this.gameStorerProvider = gameStorerProvider;
RestartCommand = new SimpleCommand<Object,Object>(ExecuteRestartCommand);
}
public SimpleCommand<Object, Object> RestartCommand { get; private set; }
private void ExecuteRestartCommand(object o)
{
....
....
visualStateManagerService.GoToState("RestartedState");
}
}
}
It can be seen that the Execute
delegate for the SimpleCommand
also makes use of the IVSM
service, to go to a "RestartedState"
VisualState
when the RestartCommand
is executed. So what else does this ViewModel do?
Well, when a game is completed, it needs to be saved, which we can see in this code snippet from the GameViewModel
:
private void SetWinState(string winnerText)
{
HaveWinner = true;
WinnerName = winnerText + " Wins";
DisableRemainingCells();
StoreGameState();
visualStateManagerService.GoToState("WinOrCompletedState");
}
private void StoreGameState()
{
string actualWinnerName = WinnerName.StartsWith(GameCellViewModel.PlayersText) ?
IsolatedStorageHelper.ReadUserNameFromFile() + " won" : "Computer won";
string winner = HaveWinner ? actualWinnerName : "no winner";
string gameText = "";
foreach (GameCellViewModel cell in gameCells)
{
gameText += cell.CellText + ":";
}
gameText.TrimEnd(":".ToCharArray());
gameStorerProvider.StoreGameResults(actualWinnerName, gameText);
Mediator.Instance.NotifyColleagues<bool>("RefreshGameStats", true);
}
There are a couple of things of note in there. Firstly, in the StoreGameState()
method, we are making use of the IGameStorerProvider
non-core service
that we discussed earlier, to store the results to a IsolatedStorageFile
. We are also making use of the IVSM
service to push the GameView
into a "WinOrCompletedState" VisualState
when the game is deemed to be complete.
There is one more thing of note, in that at runtime, both the GameView
and the ListGameStatView
are within a single TabControl
,
so every time a new game is completed within the GameViewModel
, a Mediator
message is sent to the ListGameStatViewModel
to update
its winner's data using the runtime IGameStorerProvider
, which is shown through bindings on the ListGameStatView
.
ListGameStatView / ListGameStatViewModel
The ListGameStatView
really just shows a List<GameStateViewModel>
of who won a particular game of O's and X's. There is not really much to say about
this ViewModel apart from the fact that it makes use of the IGameStorerProvider
to get data for creating the List<GameStateViewModel>
from. Here is the
ListGameStateViewModel
in its entirety. Please forgive the method mentioning images that was copied from the WPF demo, I forgot to rename it,
the intention should still be clear.
using System;
using System.Collections.Generic;
using System.Windows.Input;
using System.ComponentModel.Composition;
using System.ComponentModel;
using System.Linq;
using System.Collections.ObjectModel;
using Cinch;
using MEFedMVVM.ViewModelLocator;
namespace CinchV2DemoSL
{
[ExportViewModel("ListGameStatViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class ListGameStatViewModel : ViewModelBase
{
#region Data
private ObservableCollection<GameStatViewModel> gameStats=
new ObservableCollection<GameStatViewModel>();
private IGameStorerProvider gameStorerProvider;
private IViewAwareStatus viewAwareStatusService;
#endregion
#region Ctor
[ImportingConstructor]
public ListGameStatViewModel(
IGameStorerProvider gameStorerProvider,
IViewAwareStatus viewAwareStatusService)
{
this.gameStorerProvider = gameStorerProvider;
this.viewAwareStatusService = viewAwareStatusService;
this.viewAwareStatusService.ViewLoaded += viewAwareStatusService_ViewLoaded;
Mediator.Instance.Register(this);
}
#endregion
#region Mediator Message Sinks
[MediatorMessageSinkAttribute("RefreshGameStats")]
public void RefreshGameStatsMessageSink(bool dummy)
{
FetchData();
}
#endregion
#region Public Methods/Properties
static PropertyChangedEventArgs gameStatsChangeArgs =
ObservableHelper.CreateArgs<ListGameStatViewModel>(x => x.GameStats);
public ObservableCollection<GameStatViewModel> GameStats
{
get { return gameStats; }
set
{
gameStats = value;
NotifyPropertyChanged(gameStatsChangeArgs);
}
}
#endregion
#region Private Methods
private void viewAwareStatusService_ViewLoaded()
{
FetchData();
}
private void FetchData()
{
gameStorerProvider.FetchGameResults(LoadImagesFromRetrievedData);
}
private void LoadImagesFromRetrievedData(List<Tuple<string, string>> data)
{
ObservableCollection<GameStatViewModel> newStats =
new ObservableCollection<GameStatViewModel>();
foreach (Tuple<String, String> result in data)
{
newStats.Add(new GameStatViewModel(result.Item1, result.Item2));
}
GameStats = newStats;
}
#endregion
#region Overrides
protected override void OnDispose()
{
base.OnDispose();
Mediator.Instance.Unregister(this);
}
#endregion
}
}
One thing to note is that because this ViewModel makes use of the IGameStorerProvider
, design time data can be supplied. Let's have a quick look at that.
Recall, I had a design time IGameStorerProvider
service implementation like this:
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.DesignTime, typeof(IGameStorerProvider))]
public class DesignTimeGameStorerProvider : IGameStorerProvider
{
#region IGameStorerProvider Members
public void StoreGameResults(string winnerName, string completeGameText)
{
}
public void WriteUserNameToFile(string username)
{
}
public string ReadUserNameFromFile()
{
return "Sacha";
}
public void FetchGameResults(Action<List<Tuple<string, string>>> callback)
{
List<Tuple<string, string>> results =
new List<Tuple<string, string>>();
results.Add(new Tuple<string,string>("Sacha won","O:X:X:O:O:X:X:O:O:"));
results.Add(new Tuple<string,string>("Computer won","O:O:O:X:X:X:X:O:O:"));
results.Add(new Tuple<string,string>("Sacha won","O:X:X:O:O:X:X:O:O:"));
callback(results);
}
#endregion
}
Which results in this in Blend:
Where it uses the UserName that was entered on the UserNameEntryView
/ UserNameEntryViewModel
.
There is one more thing of note, in that at runtime, both the GameView
and the ListGameStatView
are within a single TabControl
,
so every time a new game is completed within the GameViewModel
, a Mediator
message is sent to the
ListGameStatViewModel
to update its winner's data using the runtime IGameStorerProvider
, which is shown through bindings on the ListGameStatView
.
The ListGameStatViewModel
holds a list of individual GameStatViewModel
s, each of which can be used to show the PlayedGameChildWindow ChildWindow
.
There is not much to say about the GameStatView/
GameStatViewModel
, the view really only contains a DataTemplate that has a single button that fires
a Cinch SimpleCommand<T1,T2>
to show the previously saved game state that was stored
in IsolatedStorage
(using the IGameStorerProvider
service). So let's just have a look at the SimpleCommand<T1,T2>
that
is used to show the PlayedGameChildWindow ChildWindow
, as that is the only really important bit of functionality within the GameStatView/GameStatViewModel
; here it is:
private void ExecuteViewGameCommand(object o)
{
bool? dialogResult = null;
ChildWindowService.Show("PlayedGameChildWindow",
new PlayedGameViewModel(GameText), (s, e) =>
{
dialogResult = e.Result;
string result = dialogResult.HasValue && dialogResult.Value ? "ok" : "Cancel";
MessageBoxService.ShowInformation("You clicked " + result);
});
}
It is important to remember that ChildWindow
s in Silverlight are not the same as Modal dialogs, in WPF say, they look Modal and kinda act Modal, but any code
after showing a ChildWindow
will continue to run. So when we show a ChildWindow
, we need to ensure no more code is run. How do we deal with the modified state
that the ChildWindow
may have manipulated, I hear you ask. Well, quite simple, we use a callback as shown in the code snippet above. We can simply hook up a lambda there,
and run some code when the ChildWindow
is closed.
And the PlayedGameChildWindow ChildWindow
code-behind looks like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using Cinch;
namespace CinchV2DemoSL.ChildWindows
{
[PopupNameToViewLookupKeyMetadata("PlayedGameChildWindow",
typeof(PlayedGameChildWindow))]
public partial class PlayedGameChildWindow : ChildWindow
{
public PlayedGameChildWindow()
{
InitializeComponent();
}
private void OKButton_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = true;
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
this.DialogResult = false;
}
}
}
Note: if we were using WPF, we could use the IsDefault/IsCancel
properties in XAML, but as Silverlight does not support these,
we must have a few code-behind handlers to close the ChildWindow
.
As we just saw, the GameStatViewModel
is responsible for showing the PlayedGameChildWindow ChildWindow
, which it does using
the IChildWindowService
. Now if were to examine the XAML for the PlayedGameChildWindow ChildWindow
, we will not see any
MeffedMVVM attached DP in there to resolve the ViewModel, which is different from before.
<controls:ChildWindow x:Class="CinchV2DemoSL.ChildWindows.PlayedGameChildWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:System.Windows.Controls;
assembly=System.Windows.Controls"
xmlns:local="clr-namespace:CinchV2DemoSL"
Width="400" Height="400"
Title="Played Game"
Background="{StaticResource verticalTabHeaderBackground}">
<Grid x:Name="LayoutRoot" Margin="2">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ItemsControl Grid.Row="0"
HorizontalAlignment="Center"
VerticalAlignment="Stretch"
ItemsSource="{Binding GameCells}"
Background="Transparent"
ItemTemplate="{StaticResource cellDataTemplate}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<local:UniformGrid Background="Transparent"
Columns="3" Rows="3"
Width="300" Height="300"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<Button x:Name="CancelButton" Content="Cancel"
Click="CancelButton_Click"
Width="75" Height="23"
HorizontalAlignment="Right" Margin="0,12,0,0"
Grid.Row="1" />
<Button x:Name="OKButton" Content="OK"
Click="OKButton_Click" Width="75"
Height="23" HorizontalAlignment="Right"
Margin="0,12,79,0" Grid.Row="1" />
</Grid>
</controls:ChildWindow>
The main reason being that ChildWindow
s in Cinch are expected to have some state (Viewodel) pushed at them by the caller.
The popup manipulates the ViewModel pushed at it, and then more than likely closes, but since the caller was the one that made the initial ViewModel to push at the ChildWindow
,
the caller (parent ViewModel) has all the changes that were done in the ChildWindow
in the ViewModel it pushed at the popup window.
So that is why you do not see any MeffedMVVM attached DP, basically popup ViewModels are expected to be created by other ViewModels.
What I typically do is push in the expected services into the ChildWindow
ViewModel from the parent ViewModel, then push the newed up ViewModel into the ChildWindow
using the IChildWindowService
. This approach does mean that the parent ViewModel needs to have a reference to the service it is intending to push at the child ViewModel,
but hey I am fine with that.
Actually, there is a way you can still use the MeffedMVVM attached DP/attributes to simply get
MeffedMVVM to inject property setters for your expected services, but it's kind of an advanced topic and you probably
will not have much need to do that. However, if you do have a need to let MeffedMVVM inject property setters (such as for services),
this Cinch forum post makes interesting reading:
http://www.codeproject.com/Messages/3533572/Question-about-ViewModel-constructors-with-MEF.aspx
Special Note
This is my 100th article here at CodeProject, so I just thought I would ask that if you appreciate the articles I have brought you up until now, some nice votes would be
most welcome. Anyway it's up to you, but they would be most welcome, a nice way to mark my 100th article.
That's It ....For Now
That is all I wanted to say for now, I can now get back to working on loads of other stuff that I have ear marked for myself. I may do a few
enhancements (there are three people that have asked for), but they are enhancements, not bugs, so not vital, I may or may not do them.
Could I just ask if you have enjoyed this article, and feel it is going to help you out, could you please show your support by leaving a vote/comment?
As before, if you have any deep MEF related questions, you should direct them to Marlon Grech either by using his blog
C# Disciples, or by using the MefedMVVM CodePlex site; any other
Cinch V2 questions will be answered by the next Cinch V2 articles.