Table of Contents
Introduction
The last time we talked about what's new and what's stayed the same in Cinch V2. In this article, we will be going through the Cinch V2 WPF 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 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, 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 WPF demo app do?
Well, I think this can be summarized by the following bullet points:
- Create a tabbed main interface that allows n-many closeable tabs to be shown, where each tab may be either an "About" tab or an "Image Viewer" tab.
- Create an Image Viewer view that displays images from a particular folder (specified in App.Config) and allows users to rate each image and save and load the rating that each image received.
- Create an About view that allows the user to open a popup window to see various web sites.
Now, that may not look like much, but believe me, that is enough to showcase most of Cinch's 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 (remember to change the App.Config to point to somewhere where you have some images).
What you can see from the figure above is that it is a single window app. The main window is called MainWindow
, which has a TabControl
which is hosting a number of Views. This TabControl
is populated via a ObservableCollection<WorkSpaceData>
from the MainWindowViewModel
.
The first view you can see below is called ImageLoaderView
and simply shows a number of images from your PC. The path used is configured in App.Config.
From the ImageLoaderView
, it is possible to use the "Add Rating" button to launch the AddImageRatingPopup
. Obviously, the showing of the popup is actually done in a ViewModel called ImageLoaderViewModel
.
The next view that is shown in the MainWindow
is called AboutView
which makes use of an AboutViewModel
.
From the AboutView
, it is also possible to launch the AboutViewLinkRequestedPopup
. This showing of the popup is done in the AboutViewModel
.
Overall Structure
The following diagram illustrates the overall structure of the Views/ViewModels and popup windows for the WPF 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 WPF 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 windows perform in the WPF demo app.
PopUps
In this section, we will talk about how you can show popups from your ViewModels.
Ensuring That a Popup is Available to Show
Some of you may be familiar with how this all works from earlier Cinch articles, or even because you have used Cinch V1, however some of you may not know, so for those of you new to this, the basic idea is as follows:
There is a service that deals with showing popups called IUIVisualizerService
, that holds a Dictionary<string, Type>
such that a consumer of this IUIVisualizerService
service can simply request a popup by name (string) from the internal Dictionary<string, Type>
, and then the IUIVisualizerService
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 IUIVisualizerService
service implementation for WPF:
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
}
}
For further reading, here is a link: CinchV2_2.aspx#CoreServices and read the WPFUIVisualizerService
section.
You might be wondering how the IUIVisualizerService Dictionary<string, Type>
gets populated in time to make sure that when a popup is requested, 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 IUIVisualizerService 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<IUIVisualizerService>().Value.Register(
"AddImageRatingPopup",
typeof(AddImageRatingPopup));
InitializeComponent();
}
}
That line will ensure that IUIVisualizerService Dictionary<string, Type>
is populated with the correct KeyValuePair
.
Automatically Finding Types That are Popups
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 IUIVisualizerService
, all we have to do is attribute it up as follows in its code-behind:
[PopupNameToViewLookupKeyMetadata("AddImageRatingPopup",typeof(AddImageRatingPopup))]
public partial class AddImageRatingPopup : Window
{
}
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 a IEnumerable<Assembly>
to examine for Type
s in the passed in IEnumerable<Assembly>
that have this PopupNameToViewLookupKeyMetadata
attribute on them, and if they do, they are added into the IUIVisualizerService
, 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 Popup
So once you have a popup window with the KeyValuePair
entry inside the IUIVisualizerService Dictionary<string, Type>
, it really is child's play to show a popup from a ViewModel. You would simply do something like this:
namespace CinchV2DemoWPF
{
[ExportViewModel("AboutViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class AboutViewModel : ViewModelBase
{
public IUIVisualizerService uiVisualizer;
[ImportingConstructor]
public AboutViewModel(IUIVisualizerService uiVisualizer)
{
this.uiVisualizer = uiVisualizer;
AboutViewEventToVMFiredCommand =
new SimpleCommand<Object, EventToCommandArgs>(
ExecuteAboutViewEventToVMFiredCommand);
}
#endregion
private void ExecuteAboutViewEventToVMFiredCommand(EventToCommandArgs args)
{
AboutViewLinkRequestedPopupViewModel aboutViewLinkRequestedPopupViewModel =
new AboutViewLinkRequestedPopupViewModel();
switch ((String)args.CommandParameter)
{
case "Home":
aboutViewLinkRequestedPopupViewModel.NavigateTo =
@"http://cinch.codeplex.com/";
break;
case "Source":
aboutViewLinkRequestedPopupViewModel.NavigateTo =
@"http://cinch.codeplex.com/SourceControl/list/changesets";
break;
}
uiVisualizer.ShowDialog("AboutViewLinkRequestedPopup",
aboutViewLinkRequestedPopupViewModel);
}
}
}
Cinch V2 also provides a test double of the IUIVisualizerService
that you can use for testing, and it works pretty much as described here: CinchV.aspx#UIVisualizer. The only difference being that you no longer need to resolve anything for the IUIVisualizerService
, you would simply inject your ViewModel under test with the TestUIVisualizerService
.
This difference is due to the way Cinch V1 handles services, using DI/IOC and a common ServiceLocator pattern. Whereas Cinch V2 just relies on everything being injected through constructor parameters or via property setters. So if you want to use a test version of one of the services, you simply need to inject the test version (TestUIVisualizerService
for the example above) from your unit test code rather than the real one.
Application Management
There are really only two things that are required to make the demo app work, and these are as follows:
App.Config
You must specify a valid image location in the App.Config in order for the app to work correctly. This is what my App.Config file looks like when I run the demo app at home:
="1.0" ="utf-8"
<configuration>
<appSettings>
<add key="YourImagePath"
value="C:\Users\Public\Pictures\Sample Pictures"/>
</appSettings>
</configuration>
App Construction
As I mentioned above in the Popups 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 WPF demo app. As such, we will examine each of these in turn and see how both the View/ViewModel work together.
MainWindow / MainWindowViewModel
The MainWindow
simply acts as a container to host a number of other Views within a specialized TabControl
which I mentioned in another Cinch V2 article: CinchV2_3.aspx#Workspaces.
Read that section first, and then you will understand this section a little bit better. As I said, the MainWindow
simply hosts other Views inside a specialized TabControl
, so let's see the XAML for the MainWindow
.
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:CinchV2="clr-namespace:Cinch;assembly=Cinch.WPF"
xmlns:meffed="http:\\www.codeplex.com\MEFedMVVM"
xmlns:local="clr-namespace:CinchV2DemoWPF;assembly="
xmlns:Microsoft_Windows_Themes=
"clr-namespace:Microsoft.Windows.Themes;
assembly=PresentationFramework.Aero"
x:Class="CinchV2DemoWPF.MainWindow"
Icon="/CinchV2DemoWPF;component/Images/CinchIcon.png"
Title="CinchV2 : WPF Demo app"
MinHeight="600"
MinWidth="800"
WindowState="Maximized"
WindowStartupLocation="CenterScreen"
meffed:ViewModelLocator.ViewModel="MainWindowViewModel">
<Window.Resources>
<DataTemplate DataType="{x:Type CinchV2:WorkspaceData}">
<AdornerDecorator>
<Border HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CinchV2:NavProps.ViewCreator="{Binding}"/>
</AdornerDecorator>
</DataTemplate>
</Window.Resources>
<Grid>
......
......
......
......
<local:TabControlEx Grid.Row="1" x:Name="tab1"
ItemsSource="{Binding Views}" TabStripPlacement="Left"
CinchV2:NavProps.ShouldHideHostWhenNoItems="true"
ItemContainerStyle="{StaticResource TabItemStyleVerticalTabs}"
Style="{DynamicResource TabControlStyleVerticalTabs}"
IsSynchronizedWithCurrentItem="True"
DisplayMemberPath="DisplayText">
<local:TabControlEx.ContextMenu>
<ContextMenu IsOpen="{Binding ShowContextMenu, Mode=OneWay}">
<Menu x:Name="menu" Margin="0,0,0,0"
Height="Auto" Foreground="Black"
ItemContainerStyle="{StaticResource ContextMenuItemStyle}"
ItemsSource="{Binding MainWindowOptions}"
BorderBrush="Transparent"
VerticalAlignment="Top"
Background="Transparent" />
</ContextMenu>
</local:TabControlEx.ContextMenu>
</local:TabControlEx>
</Grid>
</Window>
It can be seen that there is a TabControlEx
and also a ContextMenu
, as well as the MeffedMVVM ViewModelLocator.ViewModel
attached DP to resolve the ViewModel. So let us now turn our attention to the MainWindowViewModel
and see what that looks like, we are expecting it to provide the following functions:
ContextMenu
support.
- Some initial
WorkSpace
s for the TabControlEx
items.
So here is the pertinent code from the MainWindowViewModel
, we can indeed see that the two functions we just mentioned are catered for. This MainWindowViewModel
supplies a List<CinchMenuItem> MainWindowOptions
property, which is being used as the ContextMenu
in the MainWindow
, and it also adds WorkspaceData
items into a Views
property which is being used as the ItemsSource
for the TabControl
in the MainWindow
.
[ExportViewModel("MainWindowViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class MainWindowViewModel : ViewModelBase
{
#region Data
private bool showContextMenu = false;
private IViewAwareStatus viewAwareStatusService;
#endregion
#region Ctor
[ImportingConstructor]
public MainWindowViewModel(IViewAwareStatus viewAwareStatusService)
{
this.viewAwareStatusService = viewAwareStatusService;
this.viewAwareStatusService.ViewLoaded += ViewAwareStatusService_ViewLoaded;
}
#endregion
#region Private Methods
private List<CinchMenuItem> CreateMenus()
{
List<CinchMenuItem> menu = new List<CinchMenuItem>();
CinchMenuItem menuActions = new CinchMenuItem("Actions");
menu.Add(menuActions);
CinchMenuItem menuAbout = new CinchMenuItem("About CinchV2");
menuAbout.Command = new SimpleCommand<object, object>((x) =>
{
WorkspaceData workspace2 =
new WorkspaceData(@"/CinchV2DemoWPF;component/Images/About.png",
"AboutView", null, "About Cinch V2", true);
Views.Add(workspace2);
ShowContextMenu = false;
});
menuActions.Children.Add(menuAbout);
CinchMenuItem menuImages = new CinchMenuItem("ImageLoaderView");
menuImages.Command = new SimpleCommand<object, object>((x) =>
{
String imagePath =
ConfigurationManager.AppSettings["YourImagePath"].ToString();
WorkspaceData workspaceImages =
new WorkspaceData(@"/CinchV2DemoWPF;component/Images/imageIcon.png",
"ImageLoaderView", imagePath, "Image View", true);
Views.Add(workspaceImages);
ShowContextMenu = false;
});
menuActions.Children.Add(menuImages);
return menu;
}
private void ViewAwareStatusService_ViewLoaded()
{
if (Designer.IsInDesignMode)
return;
String imagePath =
ConfigurationManager.AppSettings["YourImagePath"].ToString();
WorkspaceData workspace1 =
new WorkspaceData(@"/CinchV2DemoWPF;component/Images/imageIcon.png",
"ImageLoaderView", imagePath, "Image View", true);
WorkspaceData workspace2 =
new WorkspaceData(@"/CinchV2DemoWPF;component/Images/About.png",
"AboutView", null, "About Cinch V2", true);
Views.Add(workspace1);
Views.Add(workspace2);
SetActiveWorkspace(workspace1);
}
#endregion
#region Public Properties
public List<CinchMenuItem> MainWindowOptions
{
get
{
return CreateMenus();
}
}
static PropertyChangedEventArgs showContextMenuArgs =
ObservableHelper.CreateArgs<MainWindowViewModel>(x => x.ShowContextMenu);
public bool ShowContextMenu
{
get { return showContextMenu; }
private set
{
showContextMenu = value;
NotifyPropertyChanged(showContextMenuArgs);
}
}
#endregion
}
ImageLoaderView / ImageLoaderViewModel
The ImageLoaderViewModel
is the most complex one in the Cinch V2 WPF demo app, and it carries out the following functions:
- Loads a set of images (folder specified in App.Config) which are loaded using a non-core service, for which there is a design time version also supplied.
- Allows the showing/hiding of an actions area using
SimpleCommand
in reverse along with a CompletedAwareCommandTrigger
.
- Allows the opening of a rating popup window (
AddImageRatingPopup
described below).
- Uses various other standard services such as
MessageBoxService/SaveFileService/OpenFileService
.
I will now explain how each of these parts is implemented in the Cinch V2 WPF demo app.
Loads a Set of Images Using Non-Core Service
As I stated in one of the previous Cinch V2 articles, Cinch V2 has a concept of core services, such as IMessageBoxService
, ISaveFileService
, IOpenFileService
, etc., but it also makes use of non-core (application specific) services. These application specific services are extra interfaces and implementations that are also marked up with the MeffedMVVM attributes to allow them to be imported into a ViewModel.
The basic rationale behind this thinking is that it is quite testable. Imagine that your ViewModel is getting data from an external source such as a Web Service or WCF service, and that the web/WCF service is being developed in parallel. To enable testing of your ViewModel, it is a good idea to communicate with the external code via a contract interface. Not only does this keep the contract between the client app and the Web Service/WCF service well known, it also facilitates testing. If the ViewModel accepts a ISomeInterface
service and expects to get data from that from somewhere, you can either use the real one, which would call the Web Service/WCF service, or you could inject a test double and simply test your ViewModel without there being a dependency on any Web Service/WCF service (that may not even be in a state to test against yet). It is all about the testability.
Anyway, the Cinch WPF demo code makes use of two such services, which are described below:
IImageDiskOperations
The ImageLoaderViewModel
makes use of the IImageDiskOperations
service to save and retrieve image ratings to/from an XML file of the user's choice. Where the IImageDiskOperations
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 the real IImageDiskOperations
(used at design time too) implementation looks like this:
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
{
#region IImageDiskOperations Members
public bool Save(string fileName,
IEnumerable<ImageViewModel> viewModelsToSave)
{
CreateInitialFile(fileName, viewModelsToSave.First());
IQueryable<ImageViewModel> allButFirst =
viewModelsToSave.Skip(1).AsQueryable<ImageViewModel>();
foreach (ImageViewModel imageVM in allButFirst)
{
AppendToFile(fileName, imageVM);
}
return true;
}
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();
}
#endregion
#region Private Methods
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();
}
}
private static void AppendToFile(string fullXmlPath, ImageViewModel imageVM)
{
XElement imagesVM_XMLDocument = XElement.Load(fullXmlPath);
imagesVM_XMLDocument.Add(new XElement("ImageVM",
new XElement("ImagePath", imageVM.ImagePath),
new XElement("FileName", imageVM.FileName),
new XElement("FileDate", imageVM.FileDate),
new XElement("FileExtension", imageVM.FileExtension),
new XElement("FileSize", imageVM.FileSize),
new XElement("Rating", imageVM.Rating)));
imagesVM_XMLDocument.Save(fullXmlPath);
}
private static void CreateInitialFile(string fullXmlPath, ImageViewModel imageVM)
{
XElement imagesVM_XMLDocument =
new XElement("AllImageViewModels",
new XElement("ImageVM",
new XElement("ImagePath", imageVM.ImagePath),
new XElement("FileName", imageVM.FileName),
new XElement("FileDate", imageVM.FileDate),
new XElement("FileExtension", imageVM.FileExtension),
new XElement("FileSize", imageVM.FileSize),
new XElement("Rating", imageVM.Rating))
);
imagesVM_XMLDocument.Save(fullXmlPath);
}
#endregion
}
}
And this IImageDiskOperations
service is imported into the ImageLoaderViewModel
like this:
[ExportViewModel("ImageLoaderViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class ImageLoaderViewModel : ViewModelBase
{
private IImageProvider imageProvider;
private IImageDiskOperations imageDiskOperations;
[ImportingConstructor]
public ImageLoaderViewModel(
IImageProvider imageProvider,
IImageDiskOperations imageDiskOperations)
{
this.imageProvider = imageProvider;
this.imageDiskOperations = imageDiskOperations;
}
As the ImageLoaderViewModel
simply expects a IImageDiskOperations
, you could easily inject a test double or Mock IImageDiskOperations
into it when doing your unit testing. See the beauty of this approach, this is the real good bit about MeffedMVVM I think.
IImageProvider
The IImageProvider
service simply provides images to the ImageLoaderViewModel
. Where the IImageProvider
service contract looks like this.
Notice how this one is expected to be an asynchronous service, where a callback delegate is called back on completion.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace CinchV2DemoWPF
{
public interface IImageProvider
{
void FetchImages(string imagePath,
Action<List<ImageData>> callback);
}
public class ImageData
{
public string ImagePath { get; set; }
public string FileName { get; set; }
public DateTime FileDate { get; set; }
public string FileExtension { get; set; }
public int FileSize { get; set; }
}
}
And the real IImageProvider
implementation looks like this, notice how it is using a Cinch multithreading helper, BackgroundTaskManager<T>
, to do the work.
[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
}
Whilst the design time IImageProvider
service looks like this. Note that we simply callback immediately, we do not use any multithreading at all. You could also do something like this in your unit test. Testing threading operations in Unit tests is not that easy and typically involves WaitHandle
s etc. I leave it up to you, but just so you know, you can do something like this, no problem.
[PartCreationPolicy(CreationPolicy.NonShared)]
[ExportService(ServiceType.DesignTime, typeof(IImageProvider))]
public class DesigntimeImageProvider : IImageProvider
{
#region Public Methods
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);
}
#endregion
}
And this IImageProvider
service is imported into the ImageLoaderViewModel
like this:
[ExportViewModel("ImageLoaderViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class ImageLoaderViewModel : ViewModelBase
{
private IImageProvider imageProvider;
private IImageDiskOperations imageDiskOperations;
[ImportingConstructor]
public ImageLoaderViewModel(
IImageProvider imageProvider,
IImageDiskOperations imageDiskOperations)
{
this.imageProvider = imageProvider;
this.imageDiskOperations = imageDiskOperations;
}
As the ImageLoaderViewModel
simply expects a IImageProvider
, you could easily inject a test double or Mock IImageProvider
into it.
Show/Hide Actions Area Using SimpleCommand / CompletedAwareCommandTrigger
The demo app has a small area on the ImageLoaderView
which is not always visible. It is only visible when the requested VisualState
has been asked for (default is "HideActionsState
").
The area I am talking about looks like this, where we use the two Label
controls at the top to Show/Hide the Actions area:
Where there are two Label
controls that use EventToCommandTrigger
to fire a SimpleCommand
in the ImageLoaderViewModel
.
<Label FontFamily="Wingdings" Foreground="Black"
VerticalAlignment="Center" Margin="10,5,5,5"
VerticalContentAlignment="Center"
FontSize="20" FontWeight="Normal"
Content="þ">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonUp">
<CinchV2:EventToCommandTrigger
Command="{Binding ShowActionsCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Label>
<Label FontFamily="Wingdings" Foreground="Black"
VerticalAlignment="Center" Margin="5"
VerticalContentAlignment="Center"
FontSize="20" FontWeight="Normal"
Content="ý">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonUp">
<CinchV2:EventToCommandTrigger
Command="{Binding HideActionsCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Label>
Where the SimpleCommand
s in the ImageLoaderViewModel
simply fire an empty delegate.
ShowActionsCommand = new SimpleCommand<Object, Object>(ExecuteShowActionsCommand);
HideActionsCommand = new SimpleCommand<Object, Object>(ExecuteHideActionsCommand);
ShowActionsCommandReversed = new SimpleCommand<Object, Object>((input) => { });
HideActionsCommandReversed = new SimpleCommand<Object, Object>((input) => { });
....
....
private void ExecuteShowActionsCommand(Object args)
{
ShowActionsCommandReversed.Execute(null);
}
private void ExecuteHideActionsCommand(Object args)
{
HideActionsCommandReversed.Execute(null);
}
Then in the XAML for ImageLoaderView
, there are some Blend Interactions for the whole UserControl
, which listen for these reverse SimpleCommand
s by using CompletedAwareCommandTrigger
, and will put the UserControl
into a new VisualState
, dependant on what reverse SimpleCommand
was fired from the ViewModel causing the CompletedAwareCommandTrigger
to react and change to a new VisualState
.
<i:Interaction.Triggers>
<CinchV2:CompletedAwareCommandTrigger
Command="{Binding ShowActionsCommandReversed}">
<ei:GoToStateAction StateName="ShowActionsState"/>
</CinchV2:CompletedAwareCommandTrigger>
<CinchV2:CompletedAwareCommandTrigger
Command="{Binding HideActionsCommandReversed}">
<ei:GoToStateAction StateName="HideActionsState"/>
</CinchV2:CompletedAwareCommandTrigger>
</i:Interaction.Triggers>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="RectangleStates">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0:0:0.5">
<VisualTransition.GeneratedEasingFunction>
<ElasticEase EasingMode="EaseInOut"
Oscillations="5" Springiness="6"/>
</VisualTransition.GeneratedEasingFunction>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="ShowActionsState">
<Storyboard>
<DoubleAnimation Duration="0" To="1"
Storyboard.TargetProperty=
"(UIElement.RenderTransform).(ScaleTransform.ScaleX)"
Storyboard.TargetName="bordActions"
d:IsOptimized="True"/>
<DoubleAnimation Duration="0" To="1"
Storyboard.TargetProperty=
"(UIElement.RenderTransform).(ScaleTransform.ScaleY)"
Storyboard.TargetName="bordActions"
d:IsOptimized="True"/>
</Storyboard>
</VisualState>
<VisualState x:Name="HideActionsState"/>
<VisualState x:Name="NullState"/>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
Allow the Opening of a Popup Window
As some of you may know, Cinch also provides a way to show a popup (we also covered this earlier), so by hooking up a SimpleCommand
to a button from the UI to show a popup is almost trivial. Here is what you do:
In your XAML, have a Button
hooked to a SimpleCommand
:
<Button Grid.Row="0"
Template="{StaticResource GlassButton}"
Margin="10"
HorizontalAlignment="Stretch"
Command="{Binding AddImageRatingCommand}">
<StackPanel Orientation="Horizontal">
<Label Style="{StaticResource selectedImageLabelStyle}"
Content="Add Rating"/>
<Label Style="{StaticResource selectedImageLabelStyle}"
FontFamily="Wingdings 2" Content="êêêêê"/>
</StackPanel>
</Button>
Where the ImageLoaderViewModel
declares the SimpleCommand
and SimpleCommand.Execute
handlers like this:
AddImageRatingCommand = new SimpleCommand<Object, Object>(ExecuteAddImageRatingCommand);
....
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;
}
}
This obviously relies on the IUIVisualizerService
which the ImageLoaderViewModel
imports using MeffedMVVM, as follows:
[ExportViewModel("ImageLoaderViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class ImageLoaderViewModel : ViewModelBase
{
private IMessageBoxService messageBoxService;
[ImportingConstructor]
public ImageLoaderViewModel(
IMessageBoxService messageBoxService,
IUIVisualizerService uiVisualizerService)
{
this.messageBoxService = messageBoxService;
this.uiVisualizerService = uiVisualizerService;
}
Use Various Other Services
The WPF demo app also showcases how to use several other core Cinch services such as IOpenFileService
and ISaveFileService
. Let's have a quick look at these in action:
SaveFileService
As Cinch supplies a ISaveFileService
, it is almost trivial to use, we can just do something like this in our ViewModel. It can also be seen that this code makes use of the IImageDiskOperations
that we discussed earlier.
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)
{
try
{
if (imageDiskOperations.Save(saveFileService.FileName,
loadedImages.AsEnumerable()))
{
messageBoxService.ShowInformation(string.Format(
"Successfully saved images to file\r\n{0}",
saveFileService.FileName));
}
}
catch (Exception ex)
{
messageBoxService.ShowError(string.Format(
"An error occurred saving images to file\r\n{0}",
ex.Message));
}
}
}
Note: Cinch also provides a TestSaveFileService
, which you can read more about in the Cinch V1 articles.
OpenFileService
As Cinch supplies a IOpenFileService
, it is almost trivial to use, we can just do something like this in our ViewModel. It can also be seen that this code makes use of the IImageDiskOperations
that we discussed earlier.
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);
if (xmlReadViewModels != null)
{
loadedImages = xmlReadViewModels;
LoadedImagesCV = CollectionViewSource.GetDefaultView(loadedImages);
if (loadedImages != null)
LoadedImagesCV.MoveCurrentTo(loadedImages.First());
messageBoxService.ShowInformation(string.Format(
"Successfully retreived images from file\r\n{0}",
saveFileService.FileName));
}
else
{
messageBoxService.ShowError(string.Format(
"Couldn't load any images from file\r\n{0}",
saveFileService.FileName));
}
}
catch (Exception ex)
{
messageBoxService.ShowError(
string.Format("An error occurred opening file\r\n{0}",
ex.Message));
}
}
}
Note: Cinch also provides a TestOpenFileService
, which you can read more about in the Cinch V1 articles.
AddImageRatingPopup / ImageRatingViewModel
AddImageRatingPopup
is simply used to add a rating between 1-5 for a selected ImageViewModel
in the ImageLoaderViewModel
. As such, the ImageLoaderViewModel
opens the AddImageRatingPopup
and pushes a newed up ImageRatingViewModel
at it. Here is the SimpleCommand
Execute code that is run when the "Add Rating" button is clicked.
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 a new instance of a ImageRatingViewModel
is created, and that the ImageRatingViewModel ImageRating DataWrapper<T>
within it representing the current rating is set to the current rating associated with the selected ImageViewModel
within the ImageLoaderViewModel
.
After that, the AddImageRatingPopup
popup window is shown using the IUIVisualizerService
, where the IUIVisualizerService
will create the popup and set its DataContext
to be the newly instantiated ImageRatingViewModel
.
So we now have a AddImageRatingPopup
popup window created that is using a ImageRatingViewModel
as its DataContext
, but what does a ImageRatingViewModel
do? Well, let's have a look at the code for it. Here it is in its entirety:
public class ImageRatingViewModel : ValidatingViewModelBase
{
#region Data
private DataWrapper<Int32> imageRating;
private IEnumerable<DataWrapperBase> cachedListOfDataWrappers;
private static SimpleRule imageRatingRule;
private IMessageBoxService messageBoxService;
#endregion
#region Ctor
public ImageRatingViewModel(IMessageBoxService messageBoxService)
{
this.messageBoxService = messageBoxService;
SaveImageRatingCommand =
new SimpleCommand<Object, Object>(ExecuteSaveImageRatingCommand);
#region Create DataWrappers
ImageRating = new DataWrapper<Int32>(this, imageRatingChangeArgs);
ImageRating.IsEditable = true;
cachedListOfDataWrappers =
DataWrapperHelper.GetWrapperProperties<ImageRatingViewModel>(this);
#endregion
#region Create Validation Rules
imageRating.AddRule(imageRatingRule);
#endregion
}
static ImageRatingViewModel()
{
imageRatingRule = new SimpleRule("DataValue",
"ImageRating must be between 1-5",
(Object domainObject)=>
{
DataWrapper<Int32> obj =
(DataWrapper<Int32>)domainObject;
return obj.DataValue < 0 || obj.DataValue > 5;
});
}
#endregion
#region Public Properties
public SimpleCommand<Object, Object> SaveImageRatingCommand { get; private set; }
static PropertyChangedEventArgs imageRatingChangeArgs =
ObservableHelper.CreateArgs<ImageRatingViewModel>(x => x.ImageRating);
public DataWrapper<Int32> ImageRating
{
get { return imageRating; }
private set
{
imageRating = value;
NotifyPropertyChanged(imageRatingChangeArgs);
}
}
#endregion
#region Private Methods
private void ExecuteSaveImageRatingCommand(Object args)
{
if (IsValid)
{
CloseActivePopUpCommand.Execute(true);
}
else
{
NotifyPropertyChanged(isValidChangeArgs);
RaiseFocusEvent("ImageRating");
messageBoxService.ShowError(
"The Rating entered is invalid it must be between 1-5");
}
}
#endregion
#region Overrides
static PropertyChangedEventArgs isValidChangeArgs =
ObservableHelper.CreateArgs<ImageRatingViewModel>(x => x.IsValid);
public override bool IsValid
{
get
{
return base.IsValid &&
DataWrapperHelper.AllValid(cachedListOfDataWrappers);
}
}
#endregion
}
There are a number of things to note with this code:
- It inherits from
ValidatingViewModelBase
, as such is expected to provide validation rules.
- It uses a
DataWrapper<T>
for its image rating data.
- It can set focus to a particular
TextBox
using the SetFocus
event, which we discussed in this previous article.
- That when the popup is considered valid, it will close itself using the
ViewModelBase.CloseActivePopupCommand
, which will return control to the ImageLoaderViewModel
, which showed the popup modally, and can now make use of the possibly modified values within the ImageRatingViewModel
that was passed in from the ImageLoaderViewModel
to the AddImageRatingPopup
.
Most of this can be seen directly in the ImageRatingViewModel
code above, the only things that can not are the TextBox
validations and how it uses the Focus behaviour.
Here is the relevant XAML:
<TextBox Text="{Binding ImageRating.DataValue, UpdateSourceTrigger=LostFocus,
ValidatesOnDataErrors=True, ValidatesOnExceptions=True}"
Style="{StaticResource ValidatingTextBox}"
IsEnabled="{Binding ImageRating.IsEditable}">
<i:Interaction.Behaviors>
<CinchV2:TextBoxFocusBehavior IsUsingDataWrappers="true" />
<CinchV2:NumericTextBoxBehaviour/>
</i:Interaction.Behaviors>
</TextBox>
Where the Style
called ValidatingTextBox
looks like this:
<Style x:Key="ValidatingTextBox" TargetType="{x:Type TextBoxBase}">
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="KeyboardNavigation.TabNavigation" Value="None"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="MinWidth" Value="120"/>
<Setter Property="MinHeight" Value="20"/>
<Setter Property="AllowDrop" Value="true"/>
<Setter Property="Validation.ErrorTemplate" Value="{x:Null}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBoxBase}">
<Border
Name="Border"
CornerRadius="5"
Padding="2"
Background="White"
BorderBrush="Black"
BorderThickness="2" >
<ScrollViewer Margin="0" x:Name="PART_ContentHost"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Border"
Property="Background" Value="LightGray"/>
<Setter TargetName="Border"
Property="BorderBrush" Value="Black"/>
<Setter Property="Foreground" Value="Gray"/>
</Trigger>
<Trigger Property="Validation.HasError" Value="true">
<Setter TargetName="Border" Property="BorderBrush"
Value="Red"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors).CurrentItem.ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
Which obviously results in the following being shown whenever the validation rules defined in the ImageRatingViewModel
are broken:
AboutView / AboutViewModel
The AboutView is pretty simple, it just holds a FlowDocument
, and also link buttons that make use of the EventToCommandTrigger
Blend Trigger that I discussed here: CinchV2_3.aspx#Interactivity.
Here is the pertinent XAML for the AboutView; as before, notice the MeffedMVVM ViewModelLocator.ViewModel
attached DP to resolve the ViewModel:
<UserControl x:Class="CinchV2DemoWPF.AboutView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:CinchV2="clr-namespace:Cinch;assembly=Cinch.WPF"
xmlns:meffed="http:\\www.codeplex.com\MEFedMVVM"
xmlns:local="clr-namespace:CinchV2DemoWPF;assembly="
xmlns:i="clr-namespace:System.Windows.Interactivity;
assembly=System.Windows.Interactivity"
mc:Ignorable="d"
d:DesignHeight="371" d:DesignWidth="533"
meffed:ViewModelLocator.ViewModel="AboutViewModel">
<Grid>
......
......
......
......
<Grid Grid.Column="1" Background="{StaticResource mainGridBrush}">
<StackPanel Orientation="Vertical"
VerticalAlignment="Top" Margin="30">
<Label Style="{StaticResource aboutLabelStyle}"
Content="Check Out Cinch:"/>
<StackPanel Orientation="Vertical">
<TextBlock Style="{StaticResource aboutTextBlockStyleLinks}"
Text="Home Page [At Codeplex]">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonDown">
<CinchV2:EventToCommandTrigger
Command="{Binding AboutViewEventToVMFiredCommand}"
CommandParameter="Home"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBlock>
<TextBlock Style="{StaticResource aboutTextBlockStyleLinks}"
Text="Source Code [At Codeplex]">
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonDown">
<CinchV2:EventToCommandTrigger
Command="{Binding AboutViewEventToVMFiredCommand}"
CommandParameter="Source"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</TextBlock>
</StackPanel>
</StackPanel>
</Grid>
</Grid>
</UserControl>
Not much going on there really, apart from the ViewModel resolution using MeffedMVVM, and the 2 x EventToCommandTrigger
s. So let's have a look at the AboutViewModel
for completeness.
[ExportViewModel("AboutViewModel")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class AboutViewModel : ViewModelBase
{
public IUIVisualizerService uiVisualizer;
[ImportingConstructor]
public AboutViewModel(IUIVisualizerService uiVisualizer)
{
this.uiVisualizer = uiVisualizer;
AboutViewEventToVMFiredCommand =
new SimpleCommand<Object, EventToCommandArgs>(
ExecuteAboutViewEventToVMFiredCommand);
}
public SimpleCommand<Object, EventToCommandArgs>
AboutViewEventToVMFiredCommand { get; private set; }
private void ExecuteAboutViewEventToVMFiredCommand(EventToCommandArgs args)
{
AboutViewLinkRequestedPopupViewModel aboutViewLinkRequestedPopupViewModel =
new AboutViewLinkRequestedPopupViewModel();
switch ((String)args.CommandParameter)
{
case "Home":
aboutViewLinkRequestedPopupViewModel.NavigateTo =
@"http://cinch.codeplex.com/";
break;
case "Source":
aboutViewLinkRequestedPopupViewModel.NavigateTo =
@"http://cinch.codeplex.com/SourceControl/list/changesets";
break;
}
uiVisualizer.ShowDialog("AboutViewLinkRequestedPopup",
aboutViewLinkRequestedPopupViewModel);
}
#endregion
}
So what do these two XAML declared EventToCommandTrigger
Blend Triggers actually do? Well, they both call the AboutViewModel
AboutViewEventToVMFiredCommand
SimpleCommand
passing in different CommandParameter
values. Then the AboutViewModel
uses its IUIVisualizerService
which was injected via MeffedMVVM, to show a popup window called AboutViewLinkRequestedPopup
. You can learn more about how popups work, by reading the Popups section later in the article.
AboutViewLinkRequestedPopup / AboutViewLinkRequestedPopupViewModel
The AboutViewLinkRequestedPopup
simply navigates to a requested web page in an embedded WebBrowser.
As we just saw, the AboutViewModel
is responsible for showing a popup window called AboutViewLinkRequestedPopup
, which is done using the IUIVisualizerService
. Now if were to examine the XAML for the popup window, we would not see any MeffedMVVM attached DP in there to resolve the ViewModel, which is different from before.
<Window x:Class="CinchV2DemoWPF.AboutViewLinkRequestedPopup"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="CinchV2 : WPF Demo app"
Icon="/CinchV2DemoWPF;component/Images/CinchIcon.png"
Height="700"
Width="700"
WindowStartupLocation="CenterOwner">
<Grid>
<WebBrowser x:Name="browser" Margin="0"/>
</Grid>
</Window>
The main reason being that popups 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 popup, the caller (parent ViewModel) has all the changes that were done in the popup 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 popup's ViewModel from the parent ViewModel, then push the newed up ViewModel into the popup using the IUIVisualizerService
. 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
But anyway, we digress for now; let's concentrate on normal usage, which is for a parent ViewModel to create a new popup ViewModel; we saw that in the AboutViewModel
s code, let's just revisit that quickly:
private void ExecuteAboutViewEventToVMFiredCommand(EventToCommandArgs args)
{
AboutViewLinkRequestedPopupViewModel aboutViewLinkRequestedPopupViewModel =
new AboutViewLinkRequestedPopupViewModel();
switch ((String)args.CommandParameter)
{
case "Home":
aboutViewLinkRequestedPopupViewModel.NavigateTo =
@"http://cinch.codeplex.com/";
break;
case "Source":
aboutViewLinkRequestedPopupViewModel.NavigateTo =
@"http://cinch.codeplex.com/SourceControl/list/changesets";
break;
}
uiVisualizer.ShowDialog("AboutViewLinkRequestedPopup",
aboutViewLinkRequestedPopupViewModel);
}
See how it creates a AboutViewLinkRequestedPopupViewModel
and passes that as the state to the IUIVisualizerService
for use with the new popup instance? Let's shift our attention to this AboutViewLinkRequestedPopupViewModel
, which looks like this in its entirety:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Cinch;
using System.ComponentModel;
using System.ComponentModel.Composition;
namespace CinchV2DemoWPF
{
public class AboutViewLinkRequestedPopupViewModel :
ViewModelBase, IViewStatusAwareInjectionAware
{
#region Data
private string navigateTo;
#endregion
#region Public Properties
private IViewAwareStatus ViewAwareStatusService { get; set; }
static PropertyChangedEventArgs navigateToArgs =
ObservableHelper.CreateArgs<AboutViewLinkRequestedPopupViewModel>(
x => x.NavigateTo);
public string NavigateTo
{
get { return navigateTo; }
set
{
navigateTo = value;
NotifyPropertyChanged(navigateToArgs);
}
}
#endregion
#region IViewStatusAwareInjectionAware Members
public void InitialiseViewAwareService(IViewAwareStatus viewAwareStatusService)
{
this.ViewAwareStatusService = viewAwareStatusService;
this.ViewAwareStatusService.ViewLoaded += ViewAwareStatusService_ViewLoaded;
}
#endregion
#region Private Methods
private void ViewAwareStatusService_ViewLoaded()
{
IWebBrowserNavigatable webBrowserNavigatable =
this.ViewAwareStatusService.View as IWebBrowserNavigatable;
if (webBrowserNavigatable != null)
{
((IWebBrowserNavigatable)webBrowserNavigatable).NavigateTo(NavigateTo);
}
}
#endregion
}
}
There are actually a few subtleties going on there, the first of which is the use of the IViewStatusAwareInjectionAware
interface. What that is for is, so that when the IUIVisualizerService
creates a popup, it examines the ViewModel is was passed in, and sees it wants to know about the IViewStatusAware
service (which is what the IViewStatusAwareInjectionAware
interface tells it), and if it does, a new IViewStatusAware
for the View is injected into the Viewmodel.
Here is the relevant code from the IUIVisualizerService
that deals with this:
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;
......
......
......
}
The other subtlety in the AboutViewLinkRequestedPopupViewModel
code is that it does something strange in the IViewAwareStatus.Loaded
handler:
private void ViewAwareStatusService_ViewLoaded()
{
IWebBrowserNavigatable webBrowserNavigatable =
this.ViewAwareStatusService.View as IWebBrowserNavigatable;
if (webBrowserNavigatable != null)
{
((IWebBrowserNavigatable)webBrowserNavigatable).NavigateTo(NavigateTo);
}
}
See how it gets the View from the IViewAwareStatus
instance and is expecting it to be a IWebBrowserNavigatable
? How is that possible?
Well, the IViewAwareStatus
service exposes the View (using a WeakReference
) so you can cast the View to whatever interface a View may implement and use it in your ViewModel. In this case, the AboutViewLinkRequestedPopup
View implements the IWebBrowserNavigatable
interface, as shown below.
[PopupNameToViewLookupKeyMetadata("AboutViewLinkRequestedPopup",
typeof(AboutViewLinkRequestedPopup))]
public partial class AboutViewLinkRequestedPopup :
Window,
IWebBrowserNavigatable
{
public void NavigateTo(string url)
{
browser.Navigate(url);
}
}
The ViewModel can now talk to the View using this IWebBrowserNavigatable
interface.
I don't normally use any interfaces on my Views, but sometimes it is the correct thing to do, so just have your ViewModel talk to the View using a well known contract, a.k.a. an interface.
That's It ....For Now
That is all I wanted to say for now. I have one more article in this new series and then I am done. The next one is on the Silverlight demo app, and just by the way will be my 100th article here at CodeProject, which will be quite something, so would be nice to get some votes/comments on that one.
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 Cinch V2 articles.