Contents
Cinch Article Series Links
Introduction
Last time we started looking at some of the Cinch internals, and this time we are going to finish up looking at the Cinch internals.
In this article, we will be looking at the following:
Prerequisites
The demo app makes use of:
- VS2008 SP1
- .NET 3.5 SP1
- SQL Server (see the README.txt in the MVVM.DataAccess project to learn what you have to setup for the demo app database)
Special Thanks
I guess the only way to do this is to just start, so let's get going, shall we? But before we do that, I just need to repeat the special thanks section, with one addition, Paul Stovell, who I forgot to include last time.
Before I start, I would specifically like to say a massive thanks to the following people without whom this article and the subsequent series of articles would never have been possible. Basically, what I have done with Cinch is studied most of these guys, seen what's hot, what's not, and come up with Cinch, which I hope addresses some new ground not covered in other frameworks.
- Mark Smith (Julmar Technology), for his excellent MVVM Helper Library, which has helped me enormously. Mark, I know I asked your permission to use some of your code, which you most kindly gave, but I just wanted to say a massive thanks for your cool ideas, some of which I genuinely had not thought of. I take my hat off to you mate.
- Josh Smith / Marlon Grech (as an atomic pair) for their excellent Mediator implementation. You boys rock, always a pleasure.
- Karl Shifflett / Jaime Rodriguez (Microsoft boys) for their excellent MVVM Lob tour, which I attended. Well done lads!
- Bill Kempf, for just being Bill and being a crazy wizard like programmer, who also has a great MVVM framework called Onyx, which I wrote an article about some time ago. Bill always has the answers to tough questions. Cheers Bill!
- Paul Stovell for his excellent delegate validation idea, which Cinch uses for the validation of business objects.
All of the WPF Disciples, for being the best online group to belong to, IMHO.
Thanks guys/girl, you know who you are.
Cinch Internals II
This section will finish the dive into the internals of Cinch, which should hopefully not bore you lot too much, and should allow you to fully understand the rest of the articles which deal with building a demo set of ViewModels/Unit Tests and showcase the actual attached demo app.
DI/IOC
The Cinch MVVM framework makes use of an IOC container. By default, this is the Microsoft Unity IOC container, which is freely available as a standalone application block or as part of the Enterprise Library. Cinch allows you to choose your own IOC Container and pass that into the ViewModel constructors; all you have to do is implement the IIOCProvider
interface, which looks like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Cinch
{
public interface IIOCProvider
{
void SetupContainer();
T GetTypeFromContainer<T>();
}
}
Cinch uses the Unity IOC container (as default, but as just stated, you can swap that out by creating your own IOC Container by implementing the IIOCProvider
interface) to allow different service implementations to be dynamically injected at runtime.
Here is what the default UnityProvider
looks like:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.Configuration;
using System.Configuration;
namespace Cinch
{
public class UnityProvider : IIOCProvider
{
#region Ctor
public UnityProvider()
{
}
#endregion
#region Private Methods
private static void RegisterDefaultServices()
{
try
{
UnitySingleton.Instance.Container.RegisterInstance(
typeof(ILogger), new WPFSLFLogger());
UnitySingleton.Instance.Container.RegisterInstance(
typeof(IUIVisualizerService), new WPFUIVisualizerService());
UnitySingleton.Instance.Container.RegisterInstance(
typeof(IMessageBoxService), new WPFMessageBoxService());
UnitySingleton.Instance.Container.RegisterInstance(
typeof(IOpenFileService), new WPFOpenFileService());
UnitySingleton.Instance.Container.RegisterInstance(
typeof(ISaveFileService), new WPFSaveFileService());
}
catch (ResolutionFailedException rex)
{
String err = String.Format(
"An exception has occurred in " +
"unityProvider.RegisterDefaultServices()\r\n{0}",
rex.StackTrace.ToString());
#if debug
Debug.WriteLine(err);
#endif
Console.WriteLine(err);
throw rex;
}
catch (Exception ex)
{
String err = String.Format(
"An exception has occurred in " +
"unityProvider.RegisterDefaultServices()\r\n{0}",
ex.StackTrace.ToString());
#if debug
Debug.WriteLine(err);
#endif
Console.WriteLine(err);
throw ex;
}
}
#endregion
#region IIOCProvider Members
public void SetupContainer()
{
try
{
RegisterDefaultServices();
UnityConfigurationSection section = (UnityConfigurationSection)
ConfigurationManager.GetSection("unity");
if (section != null && section.Containers.Count > 0)
{
section.Containers.Default.Configure(UnitySingleton.Instance.Container);
}
}
catch (Exception ex)
{
throw new ApplicationException(
"There was a problem configuring the Unity container\r\n" + ex.Message);
}
}
public T GetTypeFromContainer<T>()
{
return (T)UnitySingleton.Instance.Container.Resolve(typeof(T));
}
#endregion
}
}
There are some defaults assumed, but these can be overridden by specifying an entry in the App.Config, which will override the default service implementation that might otherwise have been used.
As Cinch is aimed at being a WPF framework, the defaults for most services are WPF implementations. As such, when you come to do a Unit Test project, you must supply test service implementations (Cinch has these available) via the App.Config of the Unit Test project.
This is achieved using the custom UnityConfigurationSection
which is filled in, in both the real UI project and also a Unit Test project. The Unity container simply examines the active project's App.Config and reads the Types from the UnityConfigurationSection
, and will then create and hold an instance of the configuration specified type.
Cinch wraps the unity container in a singleton, to ensure that there is only ever one Unity container available within a Cinch application.
The following diagram illustrates how the Unity IOC container works:
Optional App.Config Unity Items Required
The following table illustrates what you could supply in the App.Config when using Cinch.
Service Item |
WPF App |
Test Project |
ILogger |
Not required, default is used |
You can use the Cinch WPF service version. |
IMessageBoxService |
Not required, default is used |
You can use the Cinch test service version default, but you must provide an entry in the Unity config section to ensure the default WPF implementation is overridden in Cinch to use the Test version. |
IOpenFileService |
Not required, default is used |
You can use the Cinch test service version default, but you must provide an entry in the Unity config section to ensure the default WPF implementation is overridden in Cinch to use the Test version. |
ISaveFileService |
Not required, default is used |
You can use the Cinch test service version default, but you must provide an entry in the Unity config section to ensure the default WPF implementation is overridden in Cinch to use the Test version. |
IUIVisualizerService |
Not required, default is used, but you should provide the popups this service manages in the constructor of the WPF app's main window, or some other suitable place |
You can use the Cinch test service version default, but you must provide an entry in the Unity config section to ensure the default WPF implementation is overridden in Cinch to use the Test version. |
As shown above, if you are planning on using all the default Cinch services, you do not need to provide an App.Config for the main WPF app, though you must for any test project to ensure the default services (the WPF implementations) are overridden inside the Cinch service resolution code.
Actual Application
Any application based on Cinch does not really need to provide any service implementations as default WPF ones are added and used internally. If however you wish to change one of the default WPF services, you must supply a new Unity container App.Config section which overrides the default implementation of the service type.
Here is an example App.Config for a Cinch app. Remember this must include any services that you may have changed. Shown below is a specialization of the Cinch.IUIVisualizerService
service that may provide extra functionality.
You must also provide configuration for the logging within Cinch, which now makes use of a Logging Facade called Simple Logging Facade (SLF) which caters for many different loggers. For Cinch, I chose to use log4Net, so the App.Config for a Cinch based application should look quite similar to that shown below:
="1.0" ="utf-8"
<configuration>
<configSections>
<section name="unity"
type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,
Microsoft.Practices.Unity.Configuration" />
<section name="log4net"
type="log4net.Config.Log4NetConfigurationSectionHandler,log4net"/>
<section name="slf"
type="Slf.Config.SlfConfigurationSection, slf"/>
</configSections>
-->
<unity>
<containers>
<container>
<types>
<type
type="Cinch.IUIVisualizerService, Cinch"
mapTo="MVVM.Demo.MyFunkyWPFUIVisualizerService, MVVM.Demo"/>
</types>
</container>
</containers>
</unity>
<slf>
<factories>
-->
-->
<factory type="SLF.Log4netFacade.Log4netLoggerFactory,
SLF.Log4netFacade"/>
</factories>
</slf>
-->
<log4net>
-->
<appender name="MainAppender" type="log4net.Appender.FileAppender">
<param name="File" value="log.txt" />
<param name="AppendToFile" value="true" />
-->
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%logger: %date [%thread]
%-5level - %message %newline" />
</layout>
</appender>
<root>
<level value="ALL" />
<appender-ref ref="MainAppender" />
</root>
</log4net>
</configuration>
You can see that the app's implementation of the Cinch.IUIVisualizerService
service is injected using Unity into Cinch, and will override the default implementation of Cinch.IUIVisualizerService
within Cinch, where Cinch would have previously tried to have used the default WPF implementation.
Unit Tests
Cinch is designed to be Unit testable, and as such, you can supply alternative services to Cinch using Injection/Unity. But to be honest, it is more than likely the default implementations supplied with Cinch will be more than adequate, but you can decide that after you have read about them below.
Anyway, you either create your own test implementations of all the Cinch required services, or use the defaults supplied, and then you must inject them in using Unity into Cinch.
Here is what your Unit Test project App.Config should look like:
="1.0"
<configuration>
<configSections>
<section name="unity"
type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,
Microsoft.Practices.Unity.Configuration" />
<section name="log4net"
type="log4net.Config.Log4NetConfigurationSectionHandler,log4net"/>
<section name="slf" type="Slf.Config.SlfConfigurationSection, slf"/>
</configSections>
-->
<unity>
<containers>
<container>
<types>
<type
type="Cinch.IUIVisualizerService, Cinch"
mapTo="Cinch.TestUIVisualizerService, Cinch"/>
<type
type="Cinch.IMessageBoxService, Cinch"
mapTo="Cinch.TestMessageBoxService, Cinch"/>
<type
type="Cinch.IOpenFileService, Cinch"
mapTo="Cinch.TestOpenFileService, Cinch"/>
<type
type="Cinch.ISaveFileService, Cinch"
mapTo="Cinch.TestSaveFileService, Cinch"/>
</types>
</container>
</containers>
</unity>
<slf>
<factories>
-->
-->
<factory type="SLF.Log4netFacade.Log4netLoggerFactory, SLF.Log4netFacade"/>
</factories>
</slf>
-->
<log4net>
-->
<appender name="MainAppender" type="log4net.Appender.FileAppender">
<param name="File" value="log.txt" />
<param name="AppendToFile" value="true" />
-->
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%logger: %date [%thread] %-5level - %message %newline" />
</layout>
</appender>
<root>
<level value="ALL" />
<appender-ref ref="MainAppender" />
</root>
</log4net>
</configuration>
This ensures that the default WPF service implementations are overridden by the Unit Test service implementations.
Exposed Services
So you have now seen that there is a Unity IOC container that is responsible for locating and loading the right services as dictated by the current App.Config file. Well, in Cinch, the story doesn't end there. You see, the thing is, within Cinch, the general idea is that there is a kind of all powerful ViewModel base class (Cinch.ViewModelBase
) and providing you inherit from that, you will get some good stuff for free. Exposed services is one such thing.
You may be asking: Why do we need to do more stuff with the services? I thought that is what the Unity IOC container was doing for us. Well, that's half right, what the Unity IOC container does is get the current services (as dictated by the App.Config) into the Cinch.ViewModelBase
. Which is all cool. The problem with the Unity IOC container is that if you try and request a service from it, it appears that it gives you a different instance each time. Which may be cool if your service implementations do not require any state, but in Cinch, some of the test services do require state. So this just didn't cut it. So what happens is that the Unity read in services are added to a static available property on Cinch.ViewModelBase
. This property is a ServiceProvider, which is nothing more than a wrapper around a Dictionary
really. So when you request a service from the Cinch.ViewModelBase
using the Resolve<T>
method you will see in a minute, the ServiceProvider will examine its internal Dictionary
and return the single instance of the service. This ensures we are always working with the same instance of a service.
This diagram will help explain this a bit better.
And here is the relevant code from the Cinch.ViewModelBase
class:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Linq.Expressions;
namespace Cinch
{
public abstract class ViewModelBase : INotifyPropertyChanged,
IDisposable, IParentablePropertyExposer
{
private IIOCProvider iocProvider = null;
private static ILogger logger;
private static Boolean isInitialised = false;
private static Action<IUIVisualizerService> setupVisualizer = null;
private Boolean isCloseable = true;
public static readonly ServiceProvider ServiceProvider = new ServiceProvider();
public ViewModelBase() : this(new UnityProvider())
{
}
public ViewModelBase(IIOCProvider iocProvider)
{
if (iocProvider == null)
throw new InvalidOperationException(
String.Format(
"ViewModelBase constructor requires " +
"a IIOCProvider instance in order to work"));
this.iocProvider = iocProvider;
if (!ViewModelBase.isInitialised)
{
iocProvider.SetupContainer();
FetchCoreServiceTypes();
}
Mediator.Instance.Register(this);
}
protected T Resolve<T>()
{
return ServiceProvider.Resolve<T>();
}
public static Action<IUIVisualizerService> SetupVisualizer
{
get { return setupVisualizer; }
set { setupVisualizer=value; }
}
public ILogger Logger
{
get { return logger; }
}
private void FetchCoreServiceTypes()
{
try
{
ViewModelBase.isInitialised = false;
logger = (ILogger)this.iocProvider.GetTypeFromContainer<ILogger>();
ServiceProvider.Add(typeof(ILogger), logger);
IMessageBoxService messageBoxService =
(IMessageBoxService)
this.iocProvider.GetTypeFromContainer<imessageboxservice>();
ServiceProvider.Add(typeof(IMessageBoxService), messageBoxService);
IOpenFileService openFileService =
(IOpenFileService)
this.iocProvider.GetTypeFromContainer<iopenfileservice>();
ServiceProvider.Add(typeof(IOpenFileService), openFileService);
ISaveFileService saveFileService =
(ISaveFileService)
this.iocProvider.GetTypeFromContainer<isavefileservice>();
ServiceProvider.Add(typeof(ISaveFileService), saveFileService);
IUIVisualizerService uiVisualizerService =
(IUIVisualizerService)
this.iocProvider.GetTypeFromContainer<iuivisualizerservice>();
ServiceProvider.Add(typeof(IUIVisualizerService), uiVisualizerService);
if (SetupVisualizer != null)
SetupVisualizer(uiVisualizerService);
ViewModelBase.isInitialised = true;
}
catch (Exception ex)
{
LogExceptionIfLoggerAvailable(ex);
}
}
private static void LogExceptionIfLoggerAvailable(Exception ex)
{
if (logger != null)
logger.Error("An error occurred", ex);
throw new ApplicationException(ex.Message);
}
}
}
Cinch Services Available
Services are really nothing more than an interface that can be implemented any way you like. So to make a WPF implementation of a service, you would implement the service interface for WPF. To do a test service, you would implement the service for a Unit test, etc.
The following subsections shall outline what actual Test/WPF services are available.
Note: I did say WPF not Silverlight, Cinch is a WPF framework, it is not targeting Silverlight, I guess it could be made to do so, but that was not the intention of it.
Logging Service
By default, Cinch uses a ILogger
based service, which by default uses the logging facade work called Simple Logging Facade (SLF), and makes use of the log4Net facade, so the log entries use the log4Net style, which can be configured through the App.Config.
If you do not like this and would rather use a different logging facility, all you have to do is inject a new ILogger
implementation in Cinch using the IOC container of your choice.
Note: Cinch does not provide a test version of this service as it was deemed that both test and runtime code could use the same Logger implementation.
The ILogger
service looks like this:
using System;
namespace Cinch
{
public interface ILogger
{
void Error(Exception exception);
void Error(object obj);
void Error(string message);
void Error(Exception exception, string message);
void Error(string format, params object[] args);
void Error(Exception exception, string format, params object[] args);
void Error(IFormatProvider provider, string format, params object[] args);
void Error(Exception exception, string format,
IFormatProvider provider, params object[] args);
}
}
MessageBox Service
- Available for: WPF UI Project
- Available for: Unit Test Project
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:
var messageBoxService = this.Resolve<imessageboxservice>();
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 to provide 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.
So 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.
The Cinch.IMessageBoxService
service interface looks like this:
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);
}
Open File Service
- Available for: WPF UI Project
- Available for: Unit Test Project
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 enquing the required Func<bool?>
values as needed by the ViewModel code currently under test.
The Cinch.IOpenFileService
service interface looks like this:
public interface IOpenFileService
{
String FileName { get; set; }
String Filter { get; set; }
String InitialDirectory { get; set; }
bool? ShowDialog(Window owner);
}
Save File Service
- Available for: WPF UI Project
- Available for: Unit Test Project
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 saved from within the unit test, by enquing the required Func<bool?>
values as needed by the ViewModel code currently under test.
For example, you may wish to actually create a file in the enqueued Func<bool?>
that you queued up in the unit test, and only then return true, which a ViewModel can then check, and proceed to use the file that you actually saved within the Unit Test.
For example, you could do something like this inside a Unit test:
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;
}
);
The Cinch.ISaveFileService
service interface looks like this:
public interface ISaveFileService
{
Boolean OverwritePrompt { get; set; }
String FileName { get; set; }
String Filter { get; set; }
String InitialDirectory { get; set; }
bool? ShowDialog(Window owner);
}
Popup Window Service
- Available for: WPF UI Project (but not inside Cinch code, see demo app)
- Available for: Unit Test Project
I do not know about you, 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.
How Cinch does this, is it provides a service called Cinch.IUIVisualizerService
which is a fairly complex beast. But it has to be. Here is the basic idea:
The WPF app that has the popups must provide the popups to the Cinch.IUIVisualizerService
(the attached demo app shows this). This provided implementation is expected to be injected via Unity into Cinch.
The Cinch.IUIVisualizerService
WPF implementation as supplied in the demo app supplies popup window types that are not available to any other project except the actual WPF app. Which is why Cinch can not provide these popups to the Cinch.IUIVisualizerService
WPF implementation. So what has to happen is that the WPF app must tell the Cinch.IUIVisualizerService
WPF implementation what popups are expected. This can be done in the constructor of the app's main window as follows, via a callback delegate that Cinch calls when the Cinch.IUIVisualizerService
service has been injected:
namespace MVVM.Demo
{
public partial class MainWindow : Window
{
public MainWindow()
{
ViewModelBase.SetupVisualizer = (x) =>
{
x.Register("AddEditOrderPopup", typeof(AddEditOrderPopup));
};
this.DataContext = new MainWindowViewModel();
InitializeComponent();
}
}
}
The demo app supplied WPF implementation of the Cinch.IUIVisualizerService
service looks like this:
using System;
using System.Collections.Generic;
using System.Windows;
using Cinch;
namespace MVVM.Demo
{
public class WPFUIVisualizerService : Cinch.IUIVisualizerService
{
#region Data
private readonly Dictionary<string, Type> _registeredWindows;
#endregion
#region Ctor
public WPFUIVisualizerService()
{
_registeredWindows = new Dictionary<string, Type>();
Register("AddEditOrderPopup", typeof(AddEditOrderPopup));
}
#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);
win.DataContext = dataContext;
if (setOwner && Application.Current != null)
win.Owner = Application.Current.MainWindow;
if (dataContext != null)
{
var bvm = dataContext as ViewModelBase;
if (bvm != null)
{
if (isModal)
{
bvm.CloseRequest += ((s, e) =>
{
try
{
win.DialogResult = e.Result;
}
catch (InvalidOperationException)
{
win.Close();
}
});
}
else
{
bvm.CloseRequest += ((s, e) => win.Close());
}
bvm.ActivateRequest += ((s, e) => win.Activate());
}
}
if (completedProc != null)
{
win.Closed +=
(s, e) =>
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 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:
addEditOrderVM.CurrentViewMode = ViewMode.AddMode;
addEditOrderVM.CurrentCustomer = CurrentCustomer;
bool? result = uiVisualizerService.ShowDialog("AddEditOrderPopup", addEditOrderVM);
if (result.HasValue && result.Value)
{
CloseActivePopUpCommand.Execute(true);
}
It can be seen that this ViewModel code snippet is using one of the names of the registered (from the WPF app's implementation of Cinch.IUIVisualizerService
) 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.
Note that we are able to set the DialogResult
value we would like returned when CloseActivePopupCommand
is executed, which may be handy if you programmatically want to close the active popup rather than let the user use the button that is linked to the CloseActivePopUpCommand
, which will always return true as shown below.
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 CloseActivePopUpCommand}"
CommandParameter="True"/>
<Button Content="Cancel" IsCancel="True"/>
The Cinch.IUIVisualizerService
looks like this:
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);
}
If your app doesn't use popups, you can:
- Simply provide a dummy implementation of this service in your app and ensure that it is injected into Cinch using Unity
- Edit the
Cinch.ViewModelBase
class to remove all references of IUIVisualizerService
Threading Helpers
Threading is one of those things that we don't have to do that often (or maybe you do), but every time we need to do it again, it seems to bite our ass all over again. To this end, Cinch provides a couple of useful threading helper classes which are described below.
Dispatcher Extension Methods
Cinch contains several extension methods that are quite useful when working with the Dispatcher
; these extension methods allow the user of the extension method to invoke a block of code using the correct UI Dispatcher
thread, and optionally at a given DispatcherPriority
. It is fairly simple and very easy to use. Here is the entire code for the Dispatcher
Extension Methods:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Threading;
namespace Cinch
{
public static class DispatcherExtensions
{
#region Dispatcher Extensions
public static void InvokeIfRequired(this Dispatcher dispatcher,
Action action, DispatcherPriority priority)
{
if (!dispatcher.CheckAccess())
{
dispatcher.Invoke(priority, action);
}
else
{
action();
}
}
public static void InvokeIfRequired(this Dispatcher dispatcher, Action action)
{
if (!dispatcher.CheckAccess())
{
dispatcher.Invoke(DispatcherPriority.Normal, action);
}
else
{
action();
}
}
public static void InvokeInBackgroundIfRequired(
this Dispatcher dispatcher,
Action action)
{
if (!dispatcher.CheckAccess())
{
dispatcher.Invoke(DispatcherPriority.Background, action);
}
else
{
action();
}
}
public static void InvokeAsynchronouslyInBackground(
this Dispatcher dispatcher, Action action)
{
if (dispatcher != null)
dispatcher.BeginInvoke(DispatcherPriority.Background, action);
else
action();
}
#endregion
}
}
To use this Extension Method is dead easy, you would just do something like this:
Dispatcher.InvokeIfRequired(() =>
{
},DispatcherPriority.Background);
Obviously, if you wanted to do something with the Dispatcher
for a View from a ViewModel, you would need to use the CurrentDispatcher
static property.
App.DoEvents
Within WPF, there is no App.DoEvents()
that some WinForms converts might be expecting. For those that have not used the the old WinForms App.DoEvents()
, what that method used to do was force the message pump to process all the queued messages. This would sometimes help with things like selection changes, pending events, etc.
Basically, it was quite a useful feature, but as I say, there is nothing like this supplied out of the box in WPF. As luck would have it, it is not too much bother to fashion your own using the Dispatcher (the place where messages are queued) and a DispatcherFrame
. Cinch provides two flavours of App.DoEvents
:
- One where the user specifies a
DispatcherPriority
for the lower limit that all pending Dispatcher
messages should be effectively pumped.
- One where all
Dispatcher
messages should be effectively pumped.
The following code shows how this works:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Threading;
using System.Security.Permissions;
namespace Cinch
{
public static class ApplicationHelper
{
#region DoEvents
[SecurityPermissionAttribute(SecurityAction.Demand,
Flags = SecurityPermissionFlag.UnmanagedCode)]
public static void DoEvents(DispatcherPriority priority)
{
DispatcherFrame frame = new DispatcherFrame();
DispatcherOperation dispatcherOperation =
Dispatcher.CurrentDispatcher.BeginInvoke(priority,
new DispatcherOperationCallback(ExitFrameOperation), frame);
Dispatcher.PushFrame(frame);
if (dispatcherOperation.Status != DispatcherOperationStatus.Completed)
{
dispatcherOperation.Abort();
}
}
[SecurityPermissionAttribute(SecurityAction.Demand,
Flags = SecurityPermissionFlag.UnmanagedCode)]
public static void DoEvents()
{
DoEvents(DispatcherPriority.Background);
}
private static object ExitFrameOperation(object obj)
{
((DispatcherFrame)obj).Continue = false;
return null;
}
#endregion
}
}
So to use this WPF App.DoEvents()
, all you need to do is, call it like the following:
ApplicationHelper.DoEvents();
Or to process all message of a particular DispatcherPriority
of above, use the following:
ApplicationHelper.DoEvents(DispatcherPriority.Background);
Background Tasks
One difficulty I have had when working with large DataSets and MVVM was how to UnitTest and synchronize using background tasks. Some use the ThreadPool
(or the very cool SmartThreadPool hosted here at CodeProject), others use BackgroundWorker
. I will try and use what fits the job generally. However, for Cinch, I made the decision to include a BackgroundWorker wrapper class that I found on the internet. I did make a few changes to it to change the completed callback ordering. I have to say it is quite neat.
Is it called BackgroundTaskManager<T>
and takes a generic which indicates the return value when the background task is run. So ideally, you would only do an operation that returns a single type in the background, and use that as the result. Of course, you could have multiple BackgroundTaskManager<T>
objects within a single ViewModel, each doing a different background activity.
What it allows you to do is hook up a Func<T> taskFunc, Action<T> completionAction
within the constructor. The Func<T> taskFunc
is used wired up against the BackgroundWorker.DoWork()
method, whilst the Action<T> completionAction
is called when the BackgroundWorker.Completed
event is raised.
One thing that I added was a AutoResetEvent WaitHandle
property, which a Unit test may set to allow the Unit test to wait for a signal on the AutoResetEvent WaitHandle
property supplied via the Unit test. More on this later.
For now, let's continue to look at what this BackgroundTaskManager<T>
class looks like; here it is:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Threading;
namespace MVVM.ViewModels
{
public class BackgroundTaskManager<T>
{
#region Data
private Func<T> TaskFunc { get; set; }
private Action<T> CompletionAction { get; set; }
#endregion
#region Ctor
public BackgroundTaskManager(Func<T> taskFunc, Action<T> completionAction)
{
this.TaskFunc = taskFunc;
this.CompletionAction = completionAction;
}
#endregion
#region Public Properties
[SuppressMessage("Microsoft.Usage",
"CA2211:NonConstantFieldsShouldNotBeVisible",
Justification = "Add/remove is thread-safe for events in .NET.")]
public EventHandler<EventArgs> BackgroundTaskStarted;
[SuppressMessage("Microsoft.Usage",
"CA2211:NonConstantFieldsShouldNotBeVisible",
Justification = "Add/remove is thread-safe for events in .NET.")]
public EventHandler<EventArgs> BackgroundTaskCompleted;
public AutoResetEvent CompletionWaitHandle { get; set; }
#endregion
#region Public Methods
public void RunBackgroundTask()
{
var backgroundWorker = new BackgroundWorker();
backgroundWorker.DoWork += delegate(object sender, DoWorkEventArgs e)
{
e.Result = TaskFunc();
};
backgroundWorker.RunWorkerCompleted +=
delegate(object sender, RunWorkerCompletedEventArgs e)
{
CompletionAction((T)e.Result);
var backgroundTaskFinishedHandler = BackgroundTaskCompleted;
if (null != backgroundTaskFinishedHandler)
{
backgroundTaskFinishedHandler.Invoke(null, EventArgs.Empty);
}
};
var backgroundTaskStartedHandler = BackgroundTaskStarted;
if (null != backgroundTaskStartedHandler)
{
backgroundTaskStartedHandler.Invoke(null, EventArgs.Empty);
}
backgroundWorker.RunWorkerAsync();
}
#endregion
}
}
So how do you use one of these BackgroundTaskManager<T>
classes? Well, it is fairly easy actually. I am not going to cover unit testing yet, as that is the subject for another article. For now, I will just show you how to use it from your ViewModel, which is done as follows:
- Create a private property
- Create a public property to allow Unit tests access to the
BackgroundTaskManager<T>
- Wire up the
BackgroundTaskManager<T> Func<T> taskFunc, Action<T> completionAction
within the constructor of BackgroundTaskManager<T>
- Have some method do something with the
BackgroundTaskManager<T>
This is all demonstrated in the code snippet shown below. We will see more of this when we come to create a ViewModel using Cinch and Unit testing with Cinch in later articles.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Windows.Threading;
using System.Windows.Data;
using Cinch;
using MVVM.Models;
using MVVM.DataAccess;
namespace MVVM.ViewModels
{
public class SomeViewModel : Cinch.WorkspaceViewModel
{
private
BackgroundTaskManager<DispatcherNotifiedObservableCollection<OrderModel>>
bgWorker = null;
public AddEditCustomerViewModel()
{
SetUpBackgroundWorker();
}
public BackgroundTaskManager
<DispatcherNotifiedObservableCollection<OrderModel>> BgWorker
{
get { return bgWorker; }
set
{
bgWorker = value;
OnPropertyChanged(() => BgWorker);
}
}
private void SetUpBackgroundWorker()
{
bgWorker = new BackgroundTaskManager
<DispatcherNotifiedObservableCollection<OrderModel>>(
() =>
{
return new DispatcherNotifiedObservableCollection<OrderModel>(
DataAccess.DataService.FetchAllOrders(
CurrentCustomer.CustomerId.DataValue).ConvertAll(
new Converter<Order, OrderModel>(
OrderModel.OrderToOrderModel)));
},
(result) =>
{
CurrentCustomer.Orders = result;
if (customerOrdersView != null)
customerOrdersView.CurrentChanged -=
CustomerOrdersView_CurrentChanged;
customerOrdersView =
CollectionViewSource.GetDefaultView(CurrentCustomer.Orders);
customerOrdersView.CurrentChanged +=
CustomerOrdersView_CurrentChanged;
customerOrdersView.MoveCurrentToPosition(-1);
HasOrders = CurrentCustomer.Orders.Count > 0;
});
}
private void LazyFetchOrdersForCustomer()
{
bgWorker.RunBackgroundTask();
}
}
}
ObservableCollection
Another thing that occurs occasionally is that you may have an ObservableCollection
that has items added to it that need to be marshaled back to a UI thread to make use of the new items. Cinch uses the following code to achieve this:
public class DispatcherNotifiedObservableCollection<T> : ObservableCollection<T>
{
#region Ctors
public DispatcherNotifiedObservableCollection()
: base()
{
}
public DispatcherNotifiedObservableCollection(List<T> list)
: base(list)
{
}
public DispatcherNotifiedObservableCollection(IEnumerable<T> collection)
: base(collection)
{
}
#endregion
#region Overrides
public override event NotifyCollectionChangedEventHandler CollectionChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
var eh = CollectionChanged;
if (eh != null)
{
Dispatcher dispatcher =
(from NotifyCollectionChangedEventHandler nh in eh.GetInvocationList()
let dpo = nh.Target as DispatcherObject
where dpo != null
select dpo.Dispatcher).FirstOrDefault();
if (dispatcher != null && dispatcher.CheckAccess() == false)
{
dispatcher.Invoke(DispatcherPriority.DataBind,
(Action)(() => OnCollectionChanged(e)));
}
else
{
foreach (NotifyCollectionChangedEventHandler nh
in eh.GetInvocationList())
nh.Invoke(this, e);
}
}
}
#endregion
}
Doing MenuItems the MVVM Way
Newbies to the MVVM pattern will probably struggle with doing menus using the MVVM pattern. Which is a shame as they are actually fairly simple and quite easy to tame. After all, they are simply a hierarchical structure (think tree) that allows some code to be run either using a Click or using an ICommand
. They actually sound (at least operationally) a lot like Button
objects, which also have a Click and can use an ICommand
, and we know how to deal with those. We just bind their Command
property to a ViewModel exposed ICommand
property. So why should menus be any different? It turns out they are not, it's just how we create the collection of menus and Style them in the View that represents any difficulty at all.
Representing MenuItems in the ViewModel
Let's start with having a look at how we might represent a ViewModel friendly MenuItem
object, shall we? We could do something like the following:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
namespace Cinch
{
public class WPFMenuItem
{
#region Public Properties
public String Text { get; set; }
public String IconUrl { get; set; }
public List<WPFMenuItem> Children { get; private set; }
public SimpleCommand Command { get; set; }
#endregion
#region Ctor
public WPFMenuItem(string item)
{
Text = item;
Children = new List<WPFMenuItem>();
}
#endregion
}
}
Simple enough, right? So how do we expose a menu from a ViewModel that the View can use? Let's look at that next.
Exposing MenuItems From the ViewModel
Well, this too is actually quite simple. We just expose a property of our ViewModel for the MenuItem
s we want to expose. Here is an example property within a ViewModel.
public List<WPFMenuItem> OrderMenuOptions
{
get
{
return CreateMenus();
}
}
The only part that really needs explaining is the call to the CreateMenus()
method. This is nothing clever; it just creates the MenuItem
s to expose from the ViewModel. Here is an example of what this might look like:
private List<WPFMenuItem> CreateMenus()
{
var menu = new List<WPFMenuItem>();
var miAddOrder = new WPFMenuItem("Add Order");
miAddOrder.Command = AddOrderCommand;
menu.Add(miAddOrder);
var miEditOrder = new WPFMenuItem("Edit Order");
miEditOrder.Command = EditOrderCommand;
menu.Add(miEditOrder);
var miDeleteOrder = new WPFMenuItem("Delete Order");
miDeleteOrder.Command = DeleteOrderCommand;
menu.Add(miDeleteOrder);
return menu;
}
You can see that in this method we are not only creating the structure of the MenuItem
s but we are also wiring up the MenuItem
s to the correct ViewModel available ICommand
s. It is that easy.
Rendering the ViewModel Exposed MenuItems Within the View
The last part of the puzzle is how to render the ViewModel exposed MenuItem
s within a particular View. Again, this is not hard. This can be achieved by firstly declaring a Menu
in the View and binding that to the ViewModels exposed MenuItem
s:
<Menu x:Name="menu" Margin="0,0,0,0"
Height="Auto" Foreground="White"
ItemContainerStyle="{StaticResource ContextMenuItemStyle}"
ItemsSource="{Binding MenuOptions}"
VerticalAlignment="Top" Background="#FF000000">
</Menu>
Where each individual MenuItem
is styled using the following Style
:
<Style x:Key="ContextMenuItemStyle">
<Setter Property="MenuItem.Header" Value="{Binding Text}"/>
<Setter Property="MenuItem.ItemsSource" Value="{Binding Children}"/>
<Setter Property="MenuItem.Command" Value="{Binding Command}" />
<Setter Property="MenuItem.Icon" Value="{Binding IconUrl,
Converter={StaticResource MenuIconConv}}" />
</Style>
Actually, one thing that lots of people have requested is the ability to create a separator. Now I did not allow for this, but luckily, a Cinch user alerted me to a cool way to do this. So you would have something like this in your XAML:
<local:SeparatorStyleSelector x:Key="separatorSelector" />
<Style x:Key="ContextMenuItemStyle">
<Setter Property="MenuItem.Header" Value="{Binding Text}"/>
<Setter Property="MenuItem.ItemsSource" Value="{Binding Children}"/>
<Setter Property="MenuItem.Command" Value="{Binding Command}" />
<Setter Property="MenuItem.Icon" Value="{Binding IconUrl,
Converter={StaticResource MenuIconConv}}" />
</Style>
<Style x:Key="SeparatorStyle" TargetType="{x:Type MenuItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Separator />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Where you make use of the following StyleSelector
to create an alternative Style
for Seperator
s.
public class SeparatorStyleSelector : StyleSelector
{
public override Style SelectStyle(object item, DependencyObject container)
{
if (item is ViewModels.MenuItem)
{
ViewModels.MenuItem mi =
item as ViewModels.MenuItem;
if (mi.Text.Equals("--", StringComparison.OrdinalIgnoreCase))
{
return (Style)((FrameworkElement)
container).FindResource("SeparatorStyle");
}
else
{
return (Style)((FrameworkElement)
container).FindResource("ContextMenuItemStyle");
}
}
return null;
}
}
The full entry can be found at one of the new Cinch article forum entries http://www.codeproject.com/Messages/3555500/Separator-menu-item.aspx.
Closeable ViewModels
I don't know how many of the readers of this article are new to WPF and have come from WinForms, or how many of you are actually doing WPF, but I can tell you one thing. When WPF came out, it did not come with a way to do MDI interfaces out of the box. In fact, if you look at Expression Blend which is a Microsoft WPF tool for working with WPF, which by the way was also written in WPF, you will see that it looks radically different from the previous Microsoft developer tools such as Visual Studio. Expression Blend is a single window application that uses some simple tricks to manage the content. These tricks are really clever layout such as using lots of expanders/tabs. It is however a one window app.
Most WPF apps you see out there are also one window apps. When I do a new WPF app, I try and make it a one window app. Of course, sometimes popups are hard to avoid. In fact, the demo code has a popup which you will see in a later article.
But for now, let us imagine we want to build a one window app using tabs. How do we do that the MVVM way?
Let's have a look into that, shall we?
Cinch actually provides a base ViewModel called WorkspaceViewModel
, which provides a single Closed
event, which can be used as a base class for your own ViewModels. Here is the code for that:
using System;
namespace Cinch
{
public abstract class WorkspaceViewModel : ViewModelBase
{
#region Data
private SimpleCommand closeWorkSpaceCommand;
private Boolean isCloseable = true;
#endregion
#region Constructor
protected WorkspaceViewModel()
{
closeWorkSpaceCommand = new SimpleCommand
{
CanExecuteDelegate = x => true,
ExecuteDelegate = x => ExecuteCloseWorkSpaceCommand()
};
}
#endregion
#region Public Properties
public SimpleCommand CloseWorkSpaceCommand
{
get
{
return closeWorkSpaceCommand;
}
}
public Boolean IsCloseable
{
get { return isCloseable; }
set
{
isCloseable = value;
OnPropertyChanged(() => IsCloseable);
}
}
#endregion
#region Private Methods
private void ExecuteCloseWorkSpaceCommand()
{
CloseWorkSpaceCommand.CommandSucceeded = false;
EventHandler<EventArgs> handlers = CloseWorkSpace;
if (handlers != null)
{
try
{
handlers(this, EventArgs.Empty);
CloseWorkSpaceCommand.CommandSucceeded = true;
}
catch
{
Logger.Log(LogType.Error, "Error firing CloseWorkSpace event");
}
}
}
#endregion
#region CloseWorkSpace Event
public event EventHandler<EventArgs> CloseWorkSpace;
#endregion
}
}
To make use of this, simply inherit from this class. Now what we need to do is build up a collection of these WorkspaceViewModel
ViewModels and bind them to a TabControl
. Lets us see that next. Here is an example ViewModel that holds a collection of WorkspaceViewModel
ViewModels and caters for them being added/removed:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows;
using System.Windows.Data;
using Cinch;
using MVVM.Models;
namespace MVVM.ViewModels
{
public class MainWindowViewModel : Cinch.ViewModelBase
{
private ObservableCollection<WorkspaceViewModel> workspaces;
public MainWindowViewModel()
{
Workspaces = new ObservableCollection<WorkspaceViewModel>();
Workspaces.CollectionChanged += this.OnWorkspacesChanged;
StartPageViewModel startPageViewModel = new StartPageViewModel();
startPageViewModel.IsCloseable = false;
Workspaces.Add(startPageViewModel);
}
private void OnWorkspacesChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null && e.NewItems.Count != 0)
foreach (WorkspaceViewModel workspace in e.NewItems)
workspace.CloseWorkSpace +=
new EventHandler<EventArgs>(OnCloseWorkSpace).
MakeWeak(eh => workspace.CloseWorkSpace -= eh);
}
private void OnCloseWorkSpace(object sender, EventArgs e)
{
WorkspaceViewModel workspace = sender as WorkspaceViewModel;
workspace.Dispose();
this.Workspaces.Remove(workspace);
}
private void SetActiveWorkspace(WorkspaceViewModel workspace)
{
ICollectionView collectionView =
CollectionViewSource.GetDefaultView(this.Workspaces);
if (collectionView != null)
collectionView.MoveCurrentTo(workspace);
}
public ObservableCollection<WorkspaceViewModel> Workspaces
{
get { return workspaces; }
set
{
if (workspaces == null)
{
workspaces = value;
OnPropertyChanged("Workspaces");
}
}
}
}
}
Now that we have a collection of WorkspaceViewModel
ViewModels to bind to, we simply use an appropriate View control to represent them, such as a TabControl
. Here is an example:
<local:TabControlEx x:Name="tabControl"
Grid.Row="2" Grid.Column="0"
IsSynchronizedWithCurrentItem="True"
ItemsSource="{Binding Path=Workspaces}"
RenderTransformOrigin="0.5,0.5"
Template="{StaticResource MainTabControlTemplateEx}">
</local:TabControlEx>
Where the TabControlEx
template looks like this:
<!---->
<ControlTemplate x:Key="MainTabControlTemplateEx"
TargetType="{x:Type controls:TabControlEx}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="4"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TabPanel x:Name="tabpanel"
Background="{StaticResource OutlookButtonHighlight}"
Margin="0"
Grid.Row="0"
IsItemsHost="True" />
<Grid Grid.Row="1" Background="Black"
HorizontalAlignment="Stretch"/>
<Grid x:Name="PART_ItemsHolder"
Grid.Row="2"/>
</Grid>
<!---->
<ControlTemplate.Triggers>
<Trigger Property="TabStripPlacement"
Value="Top">
<Setter TargetName="tabpanel"
Property="DockPanel.Dock" Value="Top"/>
<Setter TargetName="PART_ItemsHolder"
Property="DockPanel.Dock" Value="Bottom"/>
</Trigger>
<Trigger Property="TabStripPlacement"
Value="Bottom">
<Setter TargetName="tabpanel"
Property="DockPanel.Dock" Value="Bottom"/>
<Setter TargetName="PART_ItemsHolder"
Property="DockPanel.Dock" Value="Top"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
The very last piece in the puzzle is making sure the correct View is shown for the correct bound TabItem.Content
(ViewModels that inherit from WorkspaceViewModel
that are in the bound list). Basically, this is just a case of making sure that the correct View DataTemplate
s are available within the Resources
section somewhere.
<DataTemplate DataType="{x:Type VM:StartPageViewModel}">
<AdornerDecorator>
<local:StartPageView />
</AdornerDecorator>
</DataTemplate>
<DataTemplate DataType="{x:Type VM:AddEditCustomerViewModel}">
<AdornerDecorator>
<local:AddEditCustomerView />
</AdornerDecorator>
</DataTemplate>
<DataTemplate DataType="{x:Type VM:SearchCustomersViewModel}">
<AdornerDecorator>
<local:SearchCustomersView />
</AdornerDecorator>
</DataTemplate>
Setting Focus
Cinch contains a little helper class that can be used to set focus to a particular UI element, which in WPF is a lot harder than you think it should be. You need to ensure that there is a message pumped via the Dispatcher
to really ensure setting focus on a control works. Anyway, here is the relevant Cinch code to do this:
public class FocusHelper
{
public static void Focus(UIElement element)
{
ThreadPool.QueueUserWorkItem(delegate(Object theElement)
{
UIElement elem = (UIElement)theElement;
elem.Dispatcher.Invoke(DispatcherPriority.Normal,
(Action)delegate()
{
elem.Focus();
Keyboard.Focus(elem);
});
}, element);
}
}
What's Coming Up?
In the subsequent articles, I will be showcasing it roughly like this:
- How to develop ViewModels using Cinch
- How to Unit test ViewModels using the Cinch app, including how to test background worker threads which may run within Cinch ViewModels
- A demo app using Cinch
That's it, Hope You Liked it
That is actually all I wanted to say right now, but I hope from this article you can see where Cinch is going and how it could help you with MVVM. As we continue our journey, we will be covering the remaining items within Cinch and then we will move on to see how to develop an app with Cinch.
Thanks
As always, votes / comments are welcome.
History
- Initial issue.
- 24/12/09: Replaced
ILoggingService
with Simple Logging Facade.
- 07/05/10: Added text to show
ILogger
and also talks about how an IOC container can also be set in a ViewModel constructor.