Table of Contents
Introduction
Last time we talked about how Cinch V2 makes use of MefedMVVM for its ViewModel/Service resolution, and we had a brief look at there being a distinction between a Core service and Design time/Runtime services which I am calling UI services. In this article, we will examine the Core services that Cinch V2 supports and discuss Design time/Runtime services (UI services) in a bit more detail.
As promised, within each article, I shall be showing the Cinch V2 compatibility matrix.
The compatibility matrix shows a list of classes along with their general work area, and whether they are compatible with WPF or SL or both.
Work Area |
Class Name |
WPF |
Silverlight (4 or above) |
Both |
Business objects |
EditableValidatingObject.cs |
|
|
Yes |
Business objects |
ValidatingObject.cs |
|
|
Yes |
Business objects |
DataWrapper.cs |
|
|
Yes |
Commands |
EventToCommandArgs.cs |
|
|
Yes |
Commands |
SimpleCommand.cs |
|
|
Yes |
Commands |
WeakEventHandlerManager.cs |
|
|
Yes |
Events |
CloseRequestEventArgs.cs |
|
|
Yes |
Events |
UICompletedEventArgs.cs |
|
|
Yes |
WeakEvents |
WeakEvent.cs |
|
|
Yes |
WeakEvents |
WeakEventHelper.cs |
|
|
Yes |
WeakEvents |
WeakEventProxy.cs |
|
|
Yes |
Extension Methods |
DispatcherExtensions.cs |
Yes |
|
|
Extension Methods |
GenericListExtensions.cs |
|
Yes |
|
Interactivity Actions |
CommandDrivenGoToStateAction.cs |
|
|
Yes |
Interactivity Behaviours |
FocusBehaviourBase.cs |
Yes |
|
|
Interactivity Behaviours |
NumericTextBoxBehaviour.cs |
Yes |
|
|
Interactivity Behaviours |
SelectorDoubleClickCommandBehavior.cs |
Yes |
|
|
Interactivity Behaviours |
TextBoxFocusBehavior.cs |
Yes |
|
|
Interactivity Triggers |
CompletedAwareCommandTrigger.cs |
|
|
Yes |
Interactivity Triggers |
CompletedAwareGotoStateCommandTrigger.cs |
|
|
Yes |
Interactivity Triggers |
EventToCommandTrigger.cs |
|
|
Yes |
Messager Mediator |
MediatorMessageSinkAttribute.cs |
|
|
Yes |
Messager Mediator |
MediatorSingleton.cs |
|
|
Yes |
Services Implementation |
ChildWindowService.cs |
|
Yes |
|
Services Implementation |
SLMessageBoxService.cs |
|
Yes |
|
Services Implementation |
ViewAwareStatus.cs |
|
|
Yes |
Services Implementation |
ViewAwareStatusWindow.cs |
Yes |
|
|
Services Implementation |
VSMService.cs |
|
|
Yes |
Services Implementation |
WPFMessageBoxService.cs |
Yes |
|
|
Services Implementation |
WPFOpenFileService.cs |
Yes |
|
|
Services Implementation |
WPFSaveFileService.cs |
Yes |
|
|
Services Implementation |
WPFUIVisualizerService.cs |
Yes |
|
|
Services Interfaces |
IChildWindowService.cs |
|
Yes |
|
Services Interfaces |
IMessageBoxService.cs |
|
Yes |
|
Services Interfaces |
IViewAwareStatus.cs |
|
|
Yes |
Services Interfaces |
IViewAwareStatusWindow.cs |
Yes |
|
|
Services Interfaces |
IVSM.cs |
|
|
Yes |
Services Interfaces |
IMessageBoxService.cs |
Yes |
|
|
Services Interfaces |
IOpenFileService.cs |
Yes |
|
|
Services Interfaces |
ISaveFileService.cs |
Yes |
|
|
Services Interfaces |
IUIVisualizerService.cs |
Yes |
|
|
Services Test Implementations |
TestChildWindowService.cs |
|
Yes |
|
Services Test Implementations |
TestMessageBoxService.cs |
|
Yes |
|
Services Test Implementations |
TestViewAwareStatus.cs |
|
|
Yes |
Services Test Implementations |
TestViewAwareStatusWindow.cs |
Yes |
|
|
Services Test Implementations |
TestVSMService.cs |
|
|
Yes |
Services Test Implementations |
TestMessageBoxService.cs |
Yes |
|
|
Services Test Implementations |
TestOpenFileService.cs |
Yes |
|
|
Services Test Implementations |
TestSaveFileService.cs |
Yes |
|
|
Services Test Implementations |
TestUIVisualizerService.cs |
Yes |
|
|
Threading |
AddRangeObservableCollection.cs (this is a specific SL implementation) |
|
Yes |
|
Threading |
AddRangeObservableCollection.cs (this is a specific WPF implementation) |
Yes |
|
|
Threading |
BackgroundTaskManager.cs |
|
|
Yes |
Threading |
ISynchronizationContext.cs |
|
|
Yes |
Threading |
UISynchronizationContext.cs |
|
|
Yes |
Threading |
ApplicationHelper.cs |
Yes |
|
|
Threading |
DispatcherNotifiedObservableCollection.cs |
Yes |
|
|
Menus |
CinchMenuItem.cs |
|
|
Yes |
Utilities |
ArgumentValidator.cs |
|
|
Yes |
Utilities |
IWeakEventListener.cs (this is a System class missing from SL, so I created it) |
|
Yes |
|
Utilities |
ObservableHelper.cs |
|
|
Yes |
Utilities |
PropertyChangedEventManager.cs (this is a System class missing from SL, so I created it) |
|
Yes |
|
Utilities |
PropertyObserver.cs |
|
|
Yes |
Utilities |
BindingEvaluator.cs |
Yes |
|
|
Utilities |
ObservableDictionary.cs |
Yes |
|
|
Utilities |
TreeHelper.cs |
Yes |
|
|
Validation |
RegexRule.cs |
|
|
Yes |
Validation |
Rule.cs |
|
|
Yes |
Validation |
SimpleRule.cs |
|
|
Yes |
ViewModels |
EditableValidatingViewModelBase.cs |
|
|
Yes |
ViewModels |
IViewStatusAwareInjectionAware.cs |
|
|
Yes |
ViewModels |
ValidatingViewModelBase.cs |
|
|
Yes |
ViewModels |
ViewMode.cs |
|
|
Yes |
ViewModels |
ViewModelBase.cs |
|
|
Yes |
ViewModels |
ViewModelBaseSLSpecific.cs |
|
Yes |
|
ViewModels |
ViewModelBaseWPFSpecific.cs |
Yes |
|
|
Workspaces |
ChildWindowResolver.cs |
|
Yes |
|
Workspaces |
CinchBootStrapper.cs (SL Version) |
|
Yes |
|
Workspaces |
CinchBootStrapper.cs (WPF version) |
Yes |
|
|
Workspaces |
PopupNameToViewLookupKeyMetadataAttribute.cs |
|
|
Yes |
Workspaces |
IWorkspaceAware.cs |
Yes |
|
|
Workspaces |
MockView.cs |
Yes |
|
|
Workspaces |
NavProps.cs |
Yes |
|
|
Workspaces |
PopupResolver.cs |
Yes |
|
|
Workspaces |
ViewnameToViewLookupKeyMetadataAttribute.cs |
Yes |
|
|
Workspaces |
ViewResolver.cs |
Yes |
|
|
Workspaces |
WorkspaceData.cs |
Yes |
|
|
Now that I have shown you what classes will work with WPF/SL, let's get on with the rest of this article, shall we? But first, here are the links to the old Cinch V1 articles.
In case you missed Cinch V1, and have an interest in MVVM, I would strongly recommend that you read all the Cinch V1 articles first, as it will give you a much deeper understanding of the content that will be presented in these Cinch V2 articles.
CinchV1 Article Links
Some of you may never have seen the old Cinch V1 articles, so I will also include a list of these here, as where the Cinch V2 still uses the same functionality as Cinch V1, I will be redirecting people to these articles:
CinchV2 Article Links
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:
Services/UI Services
In order to understand this article, you will need to have read the first article, so if you have not already read that, you should read that using this URL: CinchV2: Introduction and MEFedMVVM and ViewModel/Service Resolution.
Core Services
Core services within Cinch are services that are shared, and can be used application wide, across multiple ViewModels, and typically are singleton instances.
Cinch V1 always had support for many services such as MessageBox/SaveFile/OpenFile/UIVisualiser, and these have largely remained unchanged in Cinch V2, what is different is that there is no IOC container to deal with any more, as these services are MEFed into the consuming ViewModel constructor where required.
The following sections will show you how to get your own ViewModel to consume these MEF enabled Cinch V2 services.
Importing Services Into ViewModel
Importing the MEF enabled Cinch V2 services into a ViewModel is extremely easy, and can be accomplished using standard MEF attributes. Here is an example of how you might import a variety of MEF enabled Cinch V2 services. It is important to note that since these MEF enabled Cinch V2 services are marked up with [PartCreationPolicy(CreationPolicy.Shared)]
, they are effectively singleton instances that are shared between all consuming ViewModels.
Here is what a typical consuming ViewModel willl look like. Note: this is a WPF ViewModel, but the same principles apply when developing Cinch V2 Silverlight ViewModels.
[ExportViewModel("ImageLoaderViewModel")]
public class ImageLoaderViewModel : ViewModelBase
{
private IViewAwareStatus viewAwareStatusService;
private IMessageBoxService messageBoxService;
private IOpenFileService openFileService;
private ISaveFileService saveFileService;
private IUIVisualizerService uiVisualizerService;
[ImportingConstructor]
public ImageLoaderViewModel(
IMessageBoxService messageBoxService,
IOpenFileService openFileService,
ISaveFileService saveFileService,
IUIVisualizerService uiVisualizerService,
IViewAwareStatus viewAwareStatusService)
{
this.messageBoxService = messageBoxService;<
this.openFileService = openFileService;
this.saveFileService = saveFileService;
this.uiVisualizerService = uiVisualizerService;
this.viewAwareStatusService = viewAwareStatusService;
}
}
The beauty of this approach is that:
- There is no
ServiceResolver<T>
, so we do not break the single responsibility pattern.
- We are allowing the ViewModel to easily receive mocks or test double services.
- If we want to add more services, we can just create a new service contract, implement it, mark it up using the MefedMVVM attributes I showed in the last article, and consume it, as shown above. It is very extensible.
A Matrix of Services
Just before we get into the guts of the services, some of you may find this table helpful:
Contract |
Service Implementation |
Test Double Implementation |
Is View Aware |
WPF |
SL4 (or above) |
(Both) |
IMessageBox Service.cs (Cinch.WPF DLL) |
WPFMessage BoxService.cs (Cinch.WPF DLL) |
TestMessageBox Service.cs (Cinch.WPF DLL) |
|
Yes |
|
|
IOpenFileService.cs |
WPFOpenFileService.cs |
TestOpenFileService.cs |
|
Yes |
|
|
ISaveFileService.cs |
WPFSaveFileService.cs |
TestSaveFileService.cs |
|
Yes |
|
|
IUIVisualizerService.cs |
WPFUIVisualizer Service.cs |
TestUIVisualizerService.cs |
|
Yes |
|
|
IViewAwareStatus.cs |
ViewAwareStatus.cs |
TestViewAwareStatus.cs |
Yes |
|
|
Yes |
IViewAware StatusWindow.cs |
ViewAware StatusWindow.cs |
TestViewAware StatusWindow.cs |
Yes |
Yes |
|
|
IVSM.cs |
VSMService.cs |
TestVSMService.cs |
Yes |
|
|
Yes |
IMessageBox Service.cs (Cinch.SL DLL) |
SLMessageBox Service.cs (Cinch.SL DLL) |
TestMessageBox Service.cs (Cinch.SL DLL) |
|
|
Yes |
|
IChildWindowService.cs |
ChildWindowService.cs |
TestChildWindowService.cs |
|
|
Yes |
|
It can be seen that as well as the service contract and implementation, CinchV2 also provides a test double implementation, which you can use in your unit tests.
Common Services
Within CinchV2, there are two common services that can be used in WPF or SL. These two services are discussed below, but just before we get into them, I just want to go back slightly and mention something I talked about in article 1.
IContextAware
Within MefedMVVM and therfore CinchV2, there is a interface called IContextAware
that can be implemented by service contracts. When you implement this interface on a service, the MefedMVVM process of resolving a service that implements IContextAware
will also inject some context into the service that implements IContextAware
. The context will be the current view. As such, any service that implements IContextAware
will obviously need to be marked up like this:
[PartCreationPolicy(CreationPolicy.NonShared)]
As each IContextAware
implementing service should know about exactly one view, these types of services can not be shared.
As I stated above, CinchV2 provides two common (WPF and SL) services that implement IContextAware
, which we shall now go into below.
IViewAwareStatus
There is a service contract which looks like this:
public interface IViewAwareStatus : IContextAware
{
event Action ViewLoaded;
event Action ViewUnloaded;
#if !SILVERLIGHT
event Action ViewActivated;
event Action ViewDeactivated;
#endif
Dispatcher ViewsDispatcher { get; }
Object View { get; }
}
You can see that this service is indeed common for WPF and SL. In WPF, you basically get more events.
So how can we consume and use one of these types of services within a ViewModel? That is trivial. The following example shows you how:
[ExportViewModel("ImageLoaderViewModel")]
public class ImageLoaderViewModel : ViewModelBase
{
private IViewAwareStatus viewAwareStatusService;
[ImportingConstructor]
public ImageLoaderViewModel(
IViewAwareStatus viewAwareStatusService)
{
this.viewAwareStatusService = viewAwareStatusService;
this.viewAwareStatusService.ViewLoaded += ViewAwareStatusService_ViewLoaded;
}
private void ViewAwareStatusService_ViewLoaded()
{
}
}
You could do something similar using the other exposed events from the IViewAwareStatus
service. But what about some of the other properties available on the service? Well, let's have a look at some of them.
ViewsDispatcher
: Simply returns the Dispatcher object associated with the View, which could allow you to dispatch threaded code to the UI thread. In the test double, the current Dispatcher is used.
View
: Simply returns the View. This could be useful if your View implements a certain interface that you wish to use in your ViewModel, just get the View and cast it to your specific interface and job done. You know sometimes you need to talk directly to the View. In the test double, you can supply a value for this object (some IView
interface perhaps).
As with all the services within CinchV2, I have provided a test double for your usage within unit tests. Here is the full code listing for the TestViewAwareStatus class.
public class TestViewAwareStatus : IViewAwareStatus
{
#region Data
private object simulatedViewObject;
#endregion
#region Ctor
public TestViewAwareStatus()
{
#if SILVERLIGHT
ViewsDispatcher = System.Windows.Deployment.Current.Dispatcher;
#else
ViewsDispatcher = Dispatcher.CurrentDispatcher;
#endif
}
#endregion
#region IViewAwareStatus Members
public event Action ViewLoaded;
public event Action ViewUnloaded;
#if !SILVERLIGHT
public event Action ViewActivated;
public event Action ViewDeactivated;
#endif
public Dispatcher ViewsDispatcher { get; private set; }
public Object View
{
get
{
return simulatedViewObject;
}
set
{
simulatedViewObject = value;
}
}
#endregion
#region IViewAware Members
public void InjectContext(object view)
{
}
#endregion
#region Helpers
public void SimulateViewIsLoadedEvent()
{
if (ViewLoaded != null)
ViewLoaded();
}
public void SimulateViewIsUnloadedEvent()
{
if (ViewUnloaded != null)
ViewUnloaded();
}
#if !SILVERLIGHT
public void SimulateViewIsActivatedEvent()
{
if (ViewActivated != null)
ViewActivated();
}
public void SimulateViewIsDeactivatedEvent()
{
if (ViewDeactivated != null)
ViewDeactivated();
}
#endif
#endregion
}
I hope you can see from this that it is possible to simulate all the events and data from within your unit tests. For events, you can use SimulateXXXEvent
, and for the View (say if your ViewModel needs to talk to the View via a certain interface), your test can provide a mock view (IView
or something), which would keep your ViewModel thinking there actually was a View there.
I have now also included another service which is only for WPF, which works much the same way as the IViewAwareStatus
service shown above, except that it caters for a target view of type Window
and exposes some more Window
type events. Here is the service interface:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Threading;
using System.Windows;
using MEFedMVVM.Services.Contracts;
using System.ComponentModel;
namespace Cinch
{
public interface IViewAwareStatusWindow : IContextAware
{
event Action ViewLoaded;
event Action ViewUnloaded;
event Action ViewActivated;
event Action ViewDeactivated;
event Action ViewWindowClosed;
event Action ViewWindowContentRendered;
event Action ViewWindowLocationChanged;
event Action ViewWindowStateChanged;
event EventHandler<CancelEventArgs> ViewWindowClosing;
Dispatcher ViewsDispatcher { get; }
Object View { get; }
}
}
Which as you can see is the same as the IViewAwareStatus
but offers a few more events. One interesting thing is that you can use the ViewWindowClosing
event's CancelEventArgs
to possibly cancel the Window
from closing should you not wish it to be closed for any reason.
Important note: As this service is expecting to work with a Window
type View, this service can only be used on a Window
's ViewModel. Cinch itself does not enforce this but does expect a level of understanding from you lot, hope this is OK.
IVSM
The other IContextAware
service is a VisualStateManager
service, which allows you to tell the associated View to go to a particular state. Now I can not claim I wrote this code, as I did not, I simply copied it from MefedMVVM and also provided a test version of it, as I wanted to be able to debug it. Cheers Marlon.
Here is the service contract:
public interface IVSM : IContextAware
{
string LastStateExecuted { get; }
void GoToState(string stateName);
}
As you can see, it is pretty simple. So how do we go about consuming and using one of these services then? As before, it is very straightforward. Here is an example:
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")]
public class GameViewModel : ViewModelBase
{
private IVSM visualStateManagerService;
[ImportingConstructor]
public GameViewModel(
IVSM visualStateManagerService)
{
}
private void CheckForCompleted()
{
if (IsCompleted)
{
StoreGameState();
visualStateManagerService.GoToState("WinOrCompletedState");
}
}
}
}
See how easy that is? As always, there is a test double of this service that you can use in your unit tests, it is called TestVVSMService
. As you can imagine, without an actual View to show VisualStates in, there is not really too much one can do, so the TestVVSMService
simply does nothing whenever its InjectContext(object view)
/ GoToState(string stateName)
is called. Although that sounds weird, it is a totally valid thing to do; after all, it keeps your ViewModel happy and allows the testing of it, so it's all cool baby.
Important note: One thing I should mention is that the VisualStateManagerService
(IVSM
implementation) expects to find VisualStateGroups
as the first child of the injected context, as it makes use of the standard VisualStateManager
GotoStateAction
, which will only work with the first child as it uses the standard VisualStateManager
.
Now if you have used Expression Blend in anger, you will know that most of the time this is going to be just fine, but there are occasions where it is quite possible for your VisualStateGroups
not to be the first child in the main context container. For example, in a TabControl
with TabItem
s, one could easily imagine that each TabItem
has its own VisualStateGroups
and VisualState
s. If that is the case, using the VisualStateManagerService
(IVSM
implementation) will not work as it relies on using the standard VisualStateManager
, when what you really need to use is the ExtendedVisualStateManager
, as it can work with any FrameworkElement
.
In subsequent articles, I will show you how Cinch V2 copes with this, but as I say, in 90% of the cases, you should be fine.
WPF Services
The WPF services within Cinch V2 are pretty much the same as in Cinch V1, with the exception that they are now MEF attributed up and the way they are supplied to the ViewModels uses MEF instead of a ServiceResolver<T>
and an IOC container, so if you feel you are up to speed with how these services work from reading Cinch V1 articles, or working with Cinch V1, you may want to gloss over this section.
WPFMessageBoxService
This is a shared service that is designed to work across all ViewModels. The service contract has not changed much since Cinch V1, the only thing that is different is that this service is MEFed into the ViewModel. This is what the Service Contract looks like:
using System;
namespace Cinch
{
public enum CustomDialogButtons
{
OK,
OKCancel,
YesNo,
YesNoCancel
}
public enum CustomDialogIcons
{
None,
Information,
Question,
Exclamation,
Stop,
Warning
}
public enum CustomDialogResults
{
None,
OK,
Cancel,
Yes,
No
}
public interface IMessageBoxService
{
void ShowError(string message);
void ShowInformation(string message);
void ShowWarning(string message);
CustomDialogResults ShowYesNo(string message, CustomDialogIcons icon);
CustomDialogResults ShowYesNoCancel(string message, CustomDialogIcons icon);
CustomDialogResults ShowOkCancel(string message, CustomDialogIcons icon);
}
}
And this is what the actual service class outline looks like:
[PartCreationPolicy(CreationPolicy.Shared)]
[ExportService(ServiceType.Both, typeof(IMessageBoxService))]
public class WPFMessageBoxService : IMessageBoxService
{
}
And this is how you could import this into one of your own ViewModels:
namespace CinchV2DemoWPF
{
[ExportViewModel("ImageLoaderViewModel")]
public class ImageLoaderViewModel : ViewModelBase
{
private IMessageBoxService messageBoxService;
[ImportingConstructor]
public ImageLoaderViewModel(
IMessageBoxService messageBoxService)
{
this.messageBoxService = messageBoxService;
}
private void ExecuteSaveToFileCommand(Object args)
{
......
......
messageBoxService.ShowError(
string.Format("An error occurred saving images to file\r\n{0}",ex.Message));
......
......
}
}
}
Cinch provides a novel way of dealing with Unit Test service implementations. Whilst it is possible to use your favourite mocking framework (RhinoMocks/Moq, etc.), sometimes that is not enough. Imagine that you have a section of code in a ViewModel something like the following:
if (messageBoxService.ShowYesNo("You sure",
CustomDialogIcons.Question) == CustomDialogResults.Yes)
{
if (messageBoxService.ShowYesNo("You totally sure",
CustomDialogIcons.Question) == CustomDialogResults.Yes)
{
}
}
Where we have an atomic bit of code within a ViewModel that needs to be fully tested by Unit tests. Using Mocks, we could provide a Mock Cinch.IMessageBoxService
service implementation. But this would not work, as we would only be able to provide a single response, which is not the same as what the real WPF Cinch.IMessageBoxService
would do, as the user would be free to use an actual MessageBox and may pick Yes/No/Cancel at random. So clearly, Mocks is not enough. We need a better idea.
So what Cinch does is provides a Unit Test Cinch.IMessageBoxService
service implementation which allows the Unit Test to enqueue the response Func<CustomDialogResults>
(which are after all just delegates), which allows us to provide callback code that will be called by the ViewModel code. This allows us to do whatever the hell we want in the enqueued callback Func<CustomDialogResults>
, as supplied by the unit tests.
This diagram may help to explain this concept a bit better.
What happens is that the unit test enqueues all the responses that are required by using Func<CustomDialogResults>
(which are the callback delegates) which are then called from the Unit Test implementation of the Cinch.IMessageBoxService
service implementation.
Here is an example of what the Unit Test implementation of the Cinch.IMessageBoxService
service implementation looks like for a ShowYesNo()
Cinch.IMessageBoxService
service implementation method call:
public CustomDialogResults ShowYesNo(string message, CustomDialogIcons icon)
{
if (ShowYesNoResponders.Count == 0)
throw new ApplicationException(
"TestMessageBoxService ShowYesNo method expects " +
"a Func<CustomDialogResults> callback \r\n" +
"delegate to be enqueued for each Show call");
else
{
Func<CustomDialogResults> responder = ShowYesNoResponders.Dequeue();
return responder();
}
}
It can be seen that the Unit Test implementation of the Cinch.IMessageBoxService
service implementation for the ShowYesNo()
method simply dequeues the next Func<CustomDialogResults>
(which are after all just delegates) and calls the Func<CustomDialogResults>
(which is queued up in the actual Unit Test) and uses the result from the call to the Func<CustomDialogResults>
.
Here is an example of how you might set up Unit Test code to enqueue the correct Func<CustomDialogResults>
responses for the ViewModel code we saw above:
testMessageBoxService.ShowYesNoResponders.Enqueue
(() =>
{
return CustomDialogResults.Yes;
}
);
testMessageBoxService.ShowYesNoResponders.Enqueue
(() =>
{
return CustomDialogResults.Yes;
}
);
By using this method, we can guarantee we drive the ViewModel code through any test path we want to. It is a very powerful technique.
As always, Cinch V2 provides a test double that you can use in your unit tests which allows you to create code as just shown in the test code above. The test service is called TestMessageBoxService
. This is shown in action in the code above.
If you want to know more about testing using this service, you can examine the Cinch V1 article: CinchV.aspx, and in particular, this section: CinchV.aspx#Messager.
WPFOpenFileService
This is a shared service that is designed to work across all ViewModels. The service contract has not changed must since Cinch V1, the only thing that is different is that this service is MEFed into the ViewModel. This is what the service contract looks like:
public interface IOpenFileService
{
String FileName { get; set; }
String Filter { get; set; }
String InitialDirectory { get; set; }
bool? ShowDialog(Window owner);
}
And this is what the actual service class outline looks like:
[PartCreationPolicy(CreationPolicy.Shared)]
[ExportService(ServiceType.Both, typeof(IOpenFileService))]
public class WPFOpenFileService : IOpenFileService
{
}
And this is how you could import this into one of your own ViewModels:
namespace CinchV2DemoWPF
{
[ExportViewModel("ImageLoaderViewModel")]
public class ImageLoaderViewModel : ViewModelBase
{
private IOpenFileService openFileService;
[ImportingConstructor]
public ImageLoaderViewModel(
IOpenFileService openFileService)
{
this.openFileService = openFileService;
}
private void ExecuteOpenExistingFileCommand(Object args)
{
openFileService.InitialDirectory = @"C:\";
openFileService.Filter = ".xml | XML Files";
var result = openFileService.ShowDialog(null);
if (result.HasValue && result.Value == true)
{
string fileName = openFileService.FileName
}
}
}
}
This works roughly the same way as just outlined above for the Cinch.IMessageBoxService
service, but this time, the Enqueued values are Queue<Func<bool?>>
. Which means you can simulate a file being opened from within the unit test, by enqueing the required Func<bool?>
values as needed by the ViewModel code currently under test.
We could deal with this in a test, and supply a valid file name to the ViewModel (just like the user picked an actual file), as follows:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;
using Cinch;
namespace MVVM.Test
{
[TestFixture]
public class Tests
{
[Test]
public void OpenSomeFile_Tests()
{
TestOpenFileService testOpenFileService = new TestOpenFileService()
SomeViewModel x = new SomeViewModel(testOpenFileService);
testOpenFileService.ShowDialogResponders.Enqueue
(() =>
{
testOpenFileService.FileName = @"c:\test.txt";
return true
}
);
.....
.....
.....
.....
}
}
}
As always, Cinch V2 provides a test double that you can use in your unit tests which allows you to create code as just shown in the test code above. The test service is called TestOpenFileService
, this is shown in action in the code above.
If you want to know more about testing using this service, you can examine the Cinch V1 article: CinchV.aspx, and in particular, this section: CinchV.aspx#OpenFile.
WPFSaveFileService
This is a shared service that is designed to work across all ViewModels. The service contract has not changed must since Cinch V1, the only thing that is different is that this service is MEFed into the ViewModel. This is what the service contract looks like:
public interface ISaveFileService
{
Boolean OverwritePrompt { get; set; }
String FileName { get; set; }
String Filter { get; set; }
String InitialDirectory { get; set; }
bool? ShowDialog(Window owner);
}
And this is what the actual service class outline looks like:
[PartCreationPolicy(CreationPolicy.Shared)]
[ExportService(ServiceType.Both, typeof(ISaveFileService))]
public class WPFSaveFileService : ISaveFileService
{
}
And this is how you could import this into one of your own ViewModels:
namespace CinchV2DemoWPF
{
[ExportViewModel("ImageLoaderViewModel")]
public class ImageLoaderViewModel : ViewModelBase
{
private ISaveFileService saveFileService;
[ImportingConstructor]
public ImageLoaderViewModel(
ISaveFileService saveFileService)
{
this.saveFileService = saveFileService;
}
private void ExecuteSaveToFileCommand(Object args)
{
saveFileService.InitialDirectory = @"C:\";
saveFileService.OverwritePrompt = true;
saveFileService.Filter = ".xml | XML Files";
var result = saveFileService.ShowDialog(null);
if (result.HasValue && result.Value == true)
{
string savedFileName = saveFileService.FileName;
}
}
}
}
This works roughly the same way as just outlined above for the Cinch.IMessageBoxService
service, but this time, the enqueued values are Queue<Func<bool?>>
. Which means you can simulate a file being opened from within the unit test, by enqueing the required Func<bool?>
values as needed by the ViewModel code currently under test.
We could deal with this in a test, and supply a valid file name to the ViewModel (just like the user picked an actual file), as follows:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;
using Cinch;
namespace MVVM.Test
{
[TestFixture]
public class Tests
{
[Test]
public void SaveSomeFile_Tests()
{
TestSaveFileService testSaveFileService = new TestSaveFileService();
SomeViewModel x = new SomeViewModel(testSaveFileService);
testSaveFileService.ShowDialogResponders.Enqueue
(() =>
{
String path = @"c:\test.txt";
if (!File.Exists(path))
{
using (StreamWriter sw = File.CreateText(path))
{
sw.WriteLine("Hello");
sw.WriteLine("Cinch");
}
}
testSaveFileService.FileName = path ;
return true;
});
.....
.....
.....
.....
}
}
}
As always, Cinch V2 provides a test double that you can use in your unit tests which allows you to create code as just shown in the test code above. The test service is called TestSaveFileService
, this is shown in action in the code above.
If you want to know more about testing using this service, you can examine the Cinch V1 article: CinchV.aspx, and in particular, this section: CinchV.aspx#SaveFile.
WPFUIVisualizerService
I do not know about you a lot, but we are in the middle of a very large WPF project at work, and although I am not a fan of popup windows, we do have some nonetheless. Popups kind of don't play well with the normal way that most folk do MVVM. Most folk would make a View a UserControl that has a ViewModel as a DataContext
. Which is cool. But occasionally, we need to show a popup and have it edit some object within the current ViewModel, or allow the user to cancel the edit.
In Cinch I solve this by using a service that can handle showing popups. This is a shared service that is designed to work across all ViewModels. The service contract has not changed must since Cinch V1, the only thing that is different is that this service is MEFed into the ViewModel. This is what the service contract looks like:
public interface IUIVisualizerService
{
void Register(string key, Type winType);
bool Unregister(string key);
bool Show(string key, object state, bool setOwner,
EventHandler<UICompletedEventArgs> completedProc);
bool? ShowDialog(string key, object state);
}
And this is what the actual service class looks like in its entirety, which I have included as I feel it may help some people understand how the inner workings of these services work:
using System;
using System.Collections.Generic;
using System.Windows;
using System.ComponentModel.Composition;
using MEFedMVVM.ViewModelLocator;
namespace Cinch
{
[PartCreationPolicy(CreationPolicy.Shared)]
[ExportService(ServiceType.Both, typeof(IUIVisualizerService))]
public class WPFUIVisualizerService : IUIVisualizerService
{
#region Data
private readonly Dictionary<string, Type> _registeredWindows;
#endregion
#region Ctor
public WPFUIVisualizerService()
{
_registeredWindows = new Dictionary<string, Type>();
}
#endregion
#region Public Methods
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(Window).IsAssignableFrom(winType))
throw new ArgumentException("winType must be of type Window");
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 bool Show(string key, object state, bool setOwner,
EventHandler<UICompletedEventArgs> completedProc)
{
Window win = CreateWindow(key, state, setOwner, completedProc, false);
if (win != null)
{
win.Show();
return true;
}
return false;
}
public bool? ShowDialog(string key, object state)
{
Window win = CreateWindow(key, state, true, null, true);
if (win != null)
return win.ShowDialog();
return false;
}
#endregion
#region Private Methods
private Window CreateWindow(string key, object dataContext, bool setOwner,
EventHandler<UICompletedEventArgs> completedProc, bool isModal)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException("key");
Type winType;
lock (_registeredWindows)
{
if (!_registeredWindows.TryGetValue(key, out winType))
return null;
}
var win = (Window)Activator.CreateInstance(winType);
if (dataContext is IViewStatusAwareInjectionAware)
{
IViewAwareStatus viewAwareStatus =
ViewModelRepository.Instance.Resolver.Container.
GetExport<IViewAwareStatus>().Value;
viewAwareStatus.InjectContext((FrameworkElement)win);
((IViewStatusAwareInjectionAware)dataContext).
InitialiseViewAwareService(viewAwareStatus);
}
win.DataContext = dataContext;
if (setOwner)
win.Owner = Application.Current.MainWindow;
if (dataContext != null)
{
var bvm = dataContext as ViewModelBase;
if (bvm != null)
{
if (isModal)
{
bvm.CloseRequest += ((EventHandler<CloseRequestEventArgs>)((s, e) =>
{
try
{
win.DialogResult = e.Result;
}
catch (InvalidOperationException)
{
win.Close();
}
})).MakeWeak(eh => bvm.CloseRequest -= eh);
}
else
{
bvm.CloseRequest +=
((EventHandler<CloseRequestEventArgs>)((s, e) => win.Close()))
.MakeWeak(eh => bvm.CloseRequest -= eh);
}
bvm.ActivateRequest +=
((EventHandler<EventArgs>)((s, e) => win.Activate()))
.MakeWeak(eh => bvm.ActivateRequest -= eh);
}
}
win.Closed += (s, e) =>
{
if (completedProc != null)
{
completedProc(this, new UICompletedEventArgs()
{
State = dataContext,
Result = (isModal) ? win.DialogResult : null
});
}
};
return win;
}
#endregion
}
}
Its job is to set the newly requested popup window to have a DataContext
set to some object, and also to listen to close commands coming from the launching ViewModel (Cinch.ViewModelBase
) which instructs the popup to close.
So to use this service from a ViewModel to show a popup and set its DataContext
, we would do something like the following:
namespace CinchV2DemoWPF
{
[ExportViewModel("ImageLoaderViewModel")]
public class ImageLoaderViewModel : ViewModelBase
{
private IUIVisualizerService uiVisualizerService;
[ImportingConstructor]
public ImageLoaderViewModel(
IUIVisualizerService uiVisualizerService)
{
this.uiVisualizerService = uiVisualizerService;
}
private void ExecuteAddImageRatingCommand(Object args)
{
ImageRatingViewModel imageRatingViewModel =
new ImageRatingViewModel(messageBoxService);
imageRatingViewModel.ImageRating.DataValue =
((ImageViewModel)loadedImagesCV.CurrentItem).Rating;
bool? result = uiVisualizerService.ShowDialog(
"AddImageRatingPopup", imageRatingViewModel);
if (result.HasValue && result.Value)
{
((ImageViewModel)loadedImagesCV.CurrentItem).Rating =
imageRatingViewModel.ImageRating.DataValue;
}
}
}
}
It can be seen that this ViewModel code snippet is using one of the names of the registered popup windows, and then showing the popup modally, and waiting for a DialogResult
(bool?
), and if the DialogResult
was true, the ViewModel closes the popup.
The more observant of you may be thinking, OK, so how does the service know about these view strings. Well, there are several options here.
You can attribute up your popup windows using a Cinch V2 PopupNameToViewLookupKeyMetadata
attribute as shown below, and use the Cinch V2 boostrapper in App.xaml.cs. This will trawl all Types in the the Assemblies you specify that have this PopupNameToViewLookupKeyMetadata
attribute and add it to the service for you.
[PopupNameToViewLookupKeyMetadata("AddImageRatingPopup",typeof(AddImageRatingPopup))]
public partial class AddImageRatingPopup : Window
So you would have this in your App.xaml.cs:
CinchBootStrapper.Initialise(new List<Assembly> { typeof(App).Assembly });
The only issue with this approach is if you are using dynamically loaded assemblies or dynamic composition using MEF, you will not necessarily have all the Assemblies to pass to the Cinch V2 boostrapper. So what can we do in that case? Well, the answer is pretty simple, we just add them manually directly into the service. Recall, this is a shared service, so whenever we add something to it, every ViewModel that makes use of this service will see the effect of it. So let's have a look at how to add a View manually to the WPFUIVisualizerService
.
IUIVisualizerService uiVisualizerService =
ViewModelRepository.Instance.Resolver.Container.GetExport<IUIVisualizerService>().Value;
uiVisualizerService .Register("SomeString",typeof(SomeChildWindow));
See how we are adding stuff directly to the WPFUIVisualizerService
there? One thing to note is you obviously need to do this somewhere where you know about the Type
of the ChildWindow
.
Closing Popups Programmatically
Note that we are also able to programmatically close an active popup using the following logic inside a ViewModel:
CloseActivePopUpCommand.Execute(true);
This will set the DialogResult
value we would like returned when the ViewModelBase.CloseActivePopupCommand
is executed, which may be handy if you want to close the active popup programmatically, rather than let the user use the button that is linked to the CloseActivePopUpCommand
. This code will return true when the popup closes.
The actual ViewModelBase
code for the CloseActivePopupCommand.Execute
looks like this:
private void ExecuteCloseActivePopupCommand(Object param)
{
if (param is Boolean)
{
RaiseCloseRequest((bool)param);
return;
}
Boolean popupAction = true;
Boolean result = Boolean.TryParse(param.ToString(), out popupAction);
if (result)
{
RaiseCloseRequest(popupAction);
}
else
{
RaiseCloseRequest(true);
}
}
You may recall from the actual WPFUIVisualizerService
implementation that the WPFUIVisualizerService
is listening for the ViewModelBase.CloseRequest
event being raised, and when the IUIVisualizerService
sees this event being raised, it will close the active popup and use the ViewModelBase.CloseRequest
EventArgs
to determine what result the popup should exit with.
So just to go over that one more time, we have a WPFUIVisualizerService
that can show popups. The WPFUIVisualizerService
listens to the ViewModelBase.CloseRequest
. We also have a ViewModelBase.CloseActivePopupCommand
that we can run, passing it a bool
parameter, this command will then execute, and ViewModelBase
will raise the ViewModelBase.CloseRequest
event passing it some EventArgs
contained in the requested programmatic exit value for the popup. The WPFUIVisualizerService
implementation will then use the requested value as the exit value for the popup and will proceed to close the popup.
Easy peasy.
This is all cool, so we can now show popups from a ViewModel, set a DataContext
, listen for a DialogResult
, and then close the popup. Sounds cool. There is however a trick or two you need to know. These are as follows:
Tricks When Working With Popups and Cinch
Following these simple rules should help:
Make sure your Save and Cancel buttons have the IsDefault and IsCancel set like:
<Button Content="Save" IsDefault="True"
Command="{Binding SaveCommand}"
CommandParameter="True"/>
<Button Content="Cancel" IsCancel="True"/>
For more background reading on this service, I suggest you read the Cinch V1 article section: CinchIII.aspx#PopServ.
As always, Cinch V2 provides a test double that you can use in your unit tests which allows you to create code as just shown in the test code above. The test service is called TestUIVisualizerService
; this is shown in action in the code above.
If you want to know more about testing using this service, you can examine the Cinch V1 article: CinchV.aspx, and in particular, this section: CinchV.aspx#UIVisualizer.
SL Services
For Silverlight, there is obviously less you can do as you are effectively running in a sandbox. As such, there are less services available within Cinch V2. That said, the ones that there are should cover most cases. So let us go on to look at the available Silverlight services available inside of Cinch V2.
IMessageBoxService
This is a shared service between all ViewModels that use this service.
Which works much the same as its fuller WPF buddy. But as the MessageBox
API that is currently available within Silverlight is not as rich as the one in WPF, the things you can do with the service are obviously less. At present, in Silverlight, you can only use MessageBox.Show
with a single string or a string/caption and an OK/Cancel set of buttons.
Here is the complete IMessageBoxService
Silverlight contract, which as I say works much the same as the WPF MessageBoxService
we discussed above.
public interface IMessageBoxService
{
void ShowError(string message);
void ShowInformation(string message);
void ShowWarning(string message);
CustomDialogResults ShowOkCancel(string message);
}
As before, I have provided a TestMessageBoxService
which you may use in your tests. Again, this works much the same as its fuller WPF equivalent.
IChildWindowService
This is a shared service between all ViewModels that use this service.
In Silverlight 3 (or so I recall), there is a new object included called a ChildWindow
. By using one of these, you are able to show a popup window, which acts like a Modal dialog that you may use when showing a standard Window modally in other frameworks such as WPF/WinForms.
It does behave slightly differently, but it is not that bad once you get the hang of it. At any rate, Cinch V2 provides a service for this too. In theory, it is not that different to the WPFUIVisualizerService
found in Cinch V2 for WPF. Let's have a look at the service contract, shall we?
public interface IChildWindowService
{
void Register(string key, Type winType);
bool Unregister(string key);
void Show(string key, object state, EventHandler<UICompletedEventArgs> completedProc);
}
Inside the actual Silverlight implementation of this service is a Dictionary<string,Type>
where the ViewModel is able to ask this service to create a particular ChildWindow
using a string key, and the Silverlight implementation of this service will create and show an instance of that type, and pass us the state parameter value as the DataContext
to the newly constructed ChildWindow
that matches the requested string.
Here is an example of how to use this service from a Silverlight ViewModel. One thing to note is that although the ChildWindow
looks Modal, it does not block calling code from continuing, i.e., it is non blocking, so do not do anything else if you rely on the result from a ChildWindow
. Do all that work inside the callback from the ChildWindow
, which is called when it is closed. This is shown in the example below.
bool? dialogResult = null;
ChildWindowService.Show("PlayedGameChildWindow",
new PlayedGameViewModel(GameText), (s, e) =>
{
dialogResult = e.Result;
string result = dialogResult.HasValue &&
dialogResult.Value ? "ok" : "Cancel";
});
The more observant of you may be thinking, OK, so how does the service know about these view strings. Well, there are several options here.
You can attribute up your custom ChildWindows
using a Cinch V2 PopupNameToViewLookupKeyMetadata
attribute as shown below, and use the Cinch V2 boostrapper in App.xaml.cs. This will trawl all Types in the the Assemblies you specify that have this PopupNameToViewLookupKeyMetadata
attribute and add it to the service for you.
[PopupNameToViewLookupKeyMetadata("PlayedGameChildWindow",typeof(PlayedGameChildWindow))]
public partial class PlayedGameChildWindow : ChildWindow
So you would have this in your App.xaml.cs:
CinchBootStrapper.Initialise(new List<Assembly> { typeof(App).Assembly });
The only issue with this approach is, if you are using multiple XAPs which you are downloading using MEF DeploymentCatalogs
, you will not necessarily have all the Assemblies to pass to the Cinch V2 boostrapper. So what can we do in that case? Well, the answer is pretty simple, we just add them manually directly into the service. Recall, this is a shared service, so whenever we add something to it, every ViewModel that makes use of this service will see the effect of it. So let's have a look at how to add a view manually to the ChildWindowService
.
IChildWindowService childWindowService =
ViewModelRepository.Instance.Resolver.Container.GetExport<IChildWindowService>().Value;
childWindowService.Register("SomeString",typeof(SomeChildWindow));
See how are we adding stuff directly to the ChildWindowService
there? One thing to note is you obviously need to do this somewhere where you know about the Type
of the ChildWindow
.
There is also a test version of this service provided that you can use in your testing, that works much the same way as the IUIVisualizerService
found in Cinch V2 for WPF. Here is a quick snippet of what some test code may look like:.
testChildWindowService.ShowResultResponders.Enqueue
(() =>
{
return new UICompletedEventArgs()
{
State = WHATEVER STATE YOU LIKE,
Result = true
} ;
}
);
The idea being the same as most of the test Cinch V2 services, that we queue up some Func
delegates that we use in our test cases that would simulate what the user could do had they been using the actual UI. Anyway, as I say, this is similar to the IUIVisualizerService
found in Cinch V2.
UI Services
UI services are really non-shared services that can be used to provide either design time/runtime data to a particular ViewModel. Cinch V2 is written with the use of services in mind, to provide the ViewModel with data. This is good for many reasons such as:
- All services use an interface, so this facilitates them to be easily mocked, or even replaced (say you had to support two different WCF services, or two different databases, just swap in the appropriate service).
- They allow there to be a design time service and a runtime service.
- They allow for synchronous fetching in test code, whilst allowing asynchronous fetching within the actual runtime services.
I do not want to dive into this area too much within this article, but I will show you an example of how you might configure a synchronous service and an asynchronous service. We will be looking into this in much more detail when we discuss the demo apps.
Synchronous service example
Suppose we have a ViewModel that is expecting to make use of a UI service (this one is from the WPF demo app included with the Cinch V2 codebase) that needs to do some disk operations to save and load some XML data, where the service contract looks like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace CinchV2DemoWPF
{
public interface IImageDiskOperations
{
bool Save(string fileName, IEnumerable<ImageViewModel> viewModelsToSave);
List<ImageViewModel> Open(string fileName);
}
}
And we have some ViewModel code that looks like this:
[ExportViewModel("ImageLoaderViewModel")]
public class ImageLoaderViewModel : ViewModelBase
{
private IImageDiskOperations imageDiskOperations;
[ImportingConstructor]
public ImageLoaderViewModel(
IImageDiskOperations imageDiskOperations)
{
this.imageDiskOperations = imageDiskOperations;
}
private void ExecuteOpenExistingFileCommand(Object args)
{
openFileService.InitialDirectory = @"C:\";
openFileService.Filter = ".xml | XML Files";
var result = openFileService.ShowDialog(null);
if (result.HasValue && result.Value == true)
{
try
{
List<ImageViewModel> xmlReadViewModels =
imageDiskOperations.Open(openFileService.FileName);
....
....
}
catch (Exception ex)
{
messageBoxService.ShowError(
string.Format("An error occurred opening file\r\n{0}", ex.Message));
}
}
}
}
We could construct a design time service for this, but there is little point, as this code is happening based on a user clicking a button which fires the ICommand
, so for this particular service, we do not need to provide a design time service, but we obviously do need to create a runtime one, but we can mark it up as being shared between design time/runtime using MEFedMVVM goodness. Here is what the synchronous demo app service implementation looks like.
Note: I am only showing the IImageDiskOperations.Open(string fileName)
method for brevity.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel.Composition;
using System.Xml.Linq;
using System.Xml;
using MEFedMVVM.ViewModelLocator;
namespace CinchV2DemoWPF
{
public static class CustomXElementExtensions
{
public static string SafeValue(this XElement input)
{
return (input == null) ? string.Empty : (string)input.Value;
}
}
[PartCreationPolicy(CreationPolicy.Shared)]
[ExportService(ServiceType.Both, typeof(IImageDiskOperations))]
public class ImageDiskOperations : IImageDiskOperations
{
public bool Save(string fileName,
IEnumerable<ImageViewModel> viewModelsToSave)
{
}
public List<ImageViewModel> Open(string fileName)
{
var xmlImageViewModelResults =
from imageVM in StreamElements(fileName, "ImageVM")
select new ImageViewModel
{
ImagePath = imageVM.Element("ImagePath").SafeValue(),
FileName = imageVM.Element("FileName").SafeValue(),
FileDate = DateTime.Parse(imageVM.Element("FileDate").SafeValue()),
FileExtension = imageVM.Element("FileExtension").SafeValue(),
FileSize = int.Parse(imageVM.Element("FileSize").SafeValue()),
Rating = int.Parse(imageVM.Element("Rating").SafeValue())
};
return xmlImageViewModelResults.ToList();
}
public static IEnumerable<XElement> StreamElements(string uri, string name)
{
using (XmlReader reader = XmlReader.Create(uri))
{
reader.MoveToContent();
while (reader.Read())
{
if ((reader.NodeType == XmlNodeType.Element) &&
(reader.Name == name))
{
XElement element = (XElement)XElement.ReadFrom(reader);
yield return element;
}
}
reader.Close();
}
}
}
}
I hope you can see that because this service really just implements the service contract interface IImageDiskOperations
, it could be easily mocked using one of the standard mocking libraries such as Moq/RhinoMocks. In fact, we could even create a test double (manual mock), which would be a class that could be used in a test that simply implements the service contract interface IImageDiskOperations
.
So that shows you how you might use a synchronous service. I have not talked about design time vs. runtime yet, but I will get to that in the next part, and also in subsequent articles.
Asynchronous service example
Synchronous services are all well and good, but occasionally (more often than not), we need to make long running operations that could make our UI unresponsive, so we need to thread those calls. How can we create UI services that the ViewModel can make use of that could potentially take a long time? Well, it turns out it is not that different. Let's examine one of these.
Suppose we have a service contract interface (again, taken from the Cinch V2 WPF demo app) that needs to fetch data in the background, using whatever background fetch technique you prefer (BackgroundWorker
, ThreadPool
, Thread
, Task
, Cinch BackgroundTaskManager
.... your call).
public interface IImageProvider
{
void FetchImages(string imagePath, Action<List<ImageData>> callback);
}
And suppose we have a ViewModel that needs to make use of this service, something like this:
[ExportViewModel("ImageLoaderViewModel")]
public class ImageLoaderViewModel : ViewModelBase
{
private IImageProvider imageProvider;
[ImportingConstructor]
public ImageLoaderViewModel(
IImageProvider imageProvider)
{
this.imageProvider= imageProvider;
}
private void LoadImages(string imagePath)
{
imageProvider.FetchImages(imagePath, LoadImagesFromRetrievedData);
}
private void LoadImagesFromRetrievedData(List<ImageData> data)
{
}
}
We can see that the service is called but we provide a callback Action<List<ImageData>
delegate to it, so when it completes, it is able to call back the required ViewModel method using the callback delegate that is expecting the results.
Let's have a look at the runtime version of this service contract implementation, shall we? Here is one from the Cinch V2 WPF demo app. Note that I am using Cinch BackGroundTaskManager
to do the background work, but you can use what you like. Oh also, I am showing the complete service implementation here, as I think it is best to see the entire sample for this one.
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.Runtime, typeof(IImageProvider))]
public class RunTimeImageProvider : IImageProvider
{
#region Data
private BackgroundTaskManager<string, List<ImageData>> bgWorker =
new BackgroundTaskManager<string, List<ImageData>>();
#endregion
#region Public Methods/Properties
public void FetchImages(string imagePath, Action<List<ImageData>> callback)
{
bgWorker.TaskFunc = (argument) =>
{
return FetchImagesInternal(argument);
};
bgWorker.CompletionAction = (result) =>
{
callback(result);
};
bgWorker.WorkerArgument = imagePath;
bgWorker.RunBackgroundTask();
}
public BackgroundTaskManager<string,List<ImageData>> BgWorker
{
get { return bgWorker; }
}
#endregion
#region Private Methods
private List<ImageData> FetchImagesInternal(string imagePath)
{
List<string> imageFiles = new List<string>();
string strFilter = "*.jpg;*.png;*.gif";
string[] filters = strFilter.Split(';');
foreach (string filter in filters)
{
imageFiles.AddRange(Directory.GetFiles(imagePath, filter));
}
List<ImageData> images = new List<ImageData>();
if (imageFiles.Count > 0)
{
int maxImages = imageFiles.Count > 20 ? 20 : imageFiles.Count;
for (int i = 0; i < maxImages; i++)
{
FileInfo fi = new FileInfo(imageFiles[i]);
ImageData id = new ImageData();
id.ImagePath = imageFiles[i];
id.FileDate = fi.LastWriteTime;
id.FileExtension = fi.Extension;
id.FileName = fi.Name;
id.FileSize = (int)fi.Length / 1024;
images.Add(id);
}
}
return images;
}
#endregion
}
That is how the runtime service works, what about the design time one? Well, here it is in its entirety:
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.DesignTime, typeof(IImageProvider))]
public class DesigntimeImageProvider : IImageProvider
{
public void FetchImages(string imagePath, Action<List<ImageData>> callback)
{
List<ImageData> fakeImages = new List<ImageData>();
for (int i = 0; i < 10; i++)
{
ImageData id = new ImageData();
id.ImagePath = @"C:\Users\Public\Pictures\Sample Pictures\Desert.jpg";
id.FileDate = DateTime.Now;
id.FileExtension = "*.jpg";
id.FileName = "Desert.jpg";
id.FileSize = 223;
fakeImages.Add(id);
}
callback(fakeImages);
}
}
What about testing this? Well, you have several choices. Moq/RhinoMocks provides facilities to call callback delegates from mocks. Or you could just create a test double that would more than likely look quite similar to the design time service shown above. If you are doing a complete UI-database test (integration test), you will more than likely also want to test the threading, but I leave that as an exercise for the reader. What I will say is that Cinch V1 did provide support for testing its own BackGroundTaskManager
, which you can read more about using this link: CinchV.aspx.
As I just discussed, Moq/RhinoMocks does provide a way of dealing with callback delegates; in fact, here is a small example of how to test a callback delegate using Moq:
Mock<IImageProvider> mockImageProvider = new Mock<IImageProvider>();
Action<List<ImageData> action = null;
mockImageProvider.Setup(b => b.GetData(It.IsAny<Action<List<ImageData>>()))
.Callback<Action<List<ImageData>>(a => action = a);
ImageLoaderViewModel vm = new ImageLoaderViewModel(mockImageProvider.Object);
action.Invoke(GetFakeListImageData());
But like I say, you could use something similar to the design time service or a test double if you don't like mocking frameworks.
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 this to Marlon Grech either by using his blog C# Disciples, or by using the MEFedMVVM CodePlex site. Any other Cinch V2 questions will be answered by the next CinchV2 articles.