![]() |
Platforms, Frameworks & Libraries »
Windows Presentation Foundation »
General
Intermediate
License: The Code Project Open License (CPOL)
WPF : If Heineken did MVVM Frameworks Part 3 of nBy Sacha BarberIt would probably be like Cinch a MVVM framework for WPF |
C# (C# 3.0, C# 4.0), .NET (.NET 3.5, .NET 4.0), WPF, Dev, Design
|
||||||||||||
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
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 I will be looking at the following:
The demo app makes use of :
So I guess the only way to do this is to just start, so lets 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 adresses 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 persmission 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 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
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.
The Cinch MVVM framework makes use of an IOC container, namely the Microsoft Unity IOC container, which is freely available as a standalone application block pr as part of Enterprise Library.
Cinch uses the Unity IOC container to allow different service implementations to be dynamically injected at runtime. 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 made 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 projects 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.

The following table illustrates what you could supply in the App.Config when using Cinch.
| Service Item | WPF App | Test Project |
|---|---|---|
| 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 override 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 override 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 override 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 apps 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 override 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 projects to ensure the default services (the WPF implementations) are overrided inside the Cinch service resolution code.
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.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="unity"
type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,
Microsoft.Practices.Unity.Configuration" />
</configSections>
<!-- Unity Config Section -->
<unity>
<containers>
<container>
<types>
<type
type="Cinch.IUIVisualizerService, Cinch"
mapTo="MVVM.Demo.MyFunkyWPFUIVisualizerService, MVVM.Demo"/>
</types>
</container>
</containers>
</unity>
</configuration>
You can see that the apps implementation of the Cinch.IUIVisualizerService
service is injected using Unity into Cinch, and will override
the default implementation of Cinch.IUIVisualizerService within
Cinch. Where previously Cinch would have previously
tried to have used the default WPF implementation.
Cinch is designed to Unit testable, 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 using Unity into Cinch.
Here is what your Unit Test project App.Config should look like:
<?xml version="1.0"?>
<configuration>
<configSections>
<section name="unity"
type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,
Microsoft.Practices.Unity.Configuration" />
</configSections>
<!-- Unity Config Section -->
<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>
</configuration>
This ensures that the default WPF service implementations are overriden by Unit Test service implementations.
So you have now seen that there is 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 that 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 1/2
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 the 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 made 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 Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.Configuration;
namespace Cinch
{
/// <summary>
/// Provides a base class for ViewModels to inherit from. This
/// base class provides the following
/// <list type="Bullet">
/// <item>Mediator pattern implementation</item>
/// <item>Service resolution</item>
/// <item>Window lifetime virtual method hooks</item>
/// <item>INotifyPropertyChanged</item>
/// </list>
/// </summary>
public abstract class ViewModelBase :
INotifyPropertyChanged, IDisposable
{
/// <summary>
/// Service resolver for view models. Allows derived types to add/remove
/// services from mapping.
/// </summary>
public static readonly ServiceProvider ServiceProvider = new ServiceProvider();
/// <summary>
/// Registers the default service implemenations with the Unity container, and
/// then configures Unity container (which allows for changes to be made to pick
/// up overriden services within Unity configuration).
/// And finally add all services found to a list of Core services which are available
/// to the ViewModelBase class
/// </summary>
static ViewModelBase()
{
try
{
//regiser defaults
RegisterDefaultServices();
//configure Unity (there could be some different Service implementations
//in the config that override the defaults just setup
UnityConfigurationSection section = (UnityConfigurationSection)
ConfigurationManager.GetSection("unity");
if (section != null && section.Containers.Count > 0)
{
section.Containers.Default.Configure(UnitySingleton.Instance.Container);
}
//fetch the core service
FetchCoreServiceTypes();
}
catch(Exception ex)
{
throw new ApplicationException(
"There was a problem configuring the Unity container\r\n" + ex.Message);
}
}
/// <summary>
/// This resolves a service type and returns the implementation.
/// </summary>
/// <typeparam name="T">Type to resolve</typeparam>
/// <returns>Implementation</returns>
protected T Resolve<T>()
{
return ServiceProvider.Resolve<T>();
}
/// <summary>
/// This method registers default services with the service provider.
/// These can be overriden by providing a new service implementation
/// and a new Unity config section in the project where the new service
/// implementation is defined
/// </summary>
private static void RegisterDefaultServices()
{
//try and add Logger if there is one available
try
{
UnitySingleton.Instance.Container.RegisterInstance(
typeof(ILoggerService),
new WPFLoggerService());
logger = (ILoggerService)UnitySingleton.Instance.Container.Resolve(
typeof(ILoggerService));
//Although the ILoggerService is exposed as a regular property, we can
//also add it, in case user want to get it using Resolve<T> method as
//they do for resolving other services
ServiceProvider.Add(typeof(ILoggerService), logger);
}
catch
{
}
//try add other default services
//users can override this using specific Unity App.Config
//section entry
try
{
//IUIVisualizerService : Register a default WPFUIVisualizerService
UnitySingleton.Instance.Container.RegisterInstance(
typeof(IUIVisualizerService), new WPFUIVisualizerService());
//IMessageBoxService : Register a default WPFMessageBoxService
UnitySingleton.Instance.Container.RegisterInstance(
typeof(IMessageBoxService), new WPFMessageBoxService());
//IOpenFileService : Register a default WPFOpenFileService
UnitySingleton.Instance.Container.RegisterInstance(
typeof(IOpenFileService), new WPFOpenFileService());
//ISaveFileService : Register a default WPFSaveFileService
UnitySingleton.Instance.Container.RegisterInstance(
typeof(ISaveFileService), new WPFSaveFileService());
}
catch (ResolutionFailedException rex)
{
LogExceptionIfLoggerAvailable(rex);
}
catch (Exception ex)
{
LogExceptionIfLoggerAvailable(ex);
}
}
/// <summary>
/// This method registers services with the service provider.
/// </summary>
private static void FetchCoreServiceTypes()
{
try
{
//IUIVisualizerService : Allows popup management
IUIVisualizerService uiVisualizerService =
(IUIVisualizerService)UnitySingleton.Instance.Container.Resolve(
typeof(IUIVisualizerService));
ServiceProvider.Add(typeof(IUIVisualizerService), uiVisualizerService);
//IMessageBoxService : Allows MessageBoxs to be shown
IMessageBoxService messageBoxService =
(IMessageBoxService)UnitySingleton.Instance.Container.Resolve(
typeof(IMessageBoxService));
ServiceProvider.Add(typeof(IMessageBoxService), messageBoxService);
//IOpenFileService : Allows Opening of files
IOpenFileService openFileService =
(IOpenFileService)UnitySingleton.Instance.Container.Resolve(
typeof(IOpenFileService));
ServiceProvider.Add(typeof(IOpenFileService), openFileService);
//ISaveFileService : Allows Saving of files
ISaveFileService saveFileService =
(ISaveFileService)UnitySingleton.Instance.Container.Resolve(
typeof(ISaveFileService));
ServiceProvider.Add(typeof(ISaveFileService), saveFileService);
}
catch (ResolutionFailedException rex)
{
LogExceptionIfLoggerAvailable(rex);
}
catch (Exception ex)
{
LogExceptionIfLoggerAvailable(ex);
}
}
}
}
Services are really nothing more than an interface that can 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 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.
Whilst IU totally abhor the Windows EventLog, I do find that it is very easy to write to, so there is a simple EventLogger service that logs to the Windows event log. The service interface for ILoggerService is very simple and looks like this.
public interface ILoggerService
{
void Log(LogType logType, String logEntry);
void Log(LogType logType, Exception ex);
}
So to use this from a ViewModel you would simply do something like the following:
Resolve.<ILoggerService>().Log(LogType.Information,"Saved client Id");
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 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();
if (messageBoxService.ShowYesNo("You sure",
CustomDialogIcons.Question) == CustomDialogResults.Yes)
{
if (messageBoxService.ShowYesNo("You totally sure",
CustomDialogIcons.Question) == CustomDialogResults.Yes)
{
//DO IT
}
}
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 could 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 are 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 enque 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 enques 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.
/// <summary>
/// Returns the next Dequeue ShowYesNo response expected. See the tests for
/// the Func callback expected values
/// </summary>
/// <param name="message">The message to be displayed.</param>
/// <param name="icon">The icon to be displayed.</param>
/// <returns>User selection.</returns>
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, simple dequeues
the next Func<CustomDialogResults> (which are after all just
delegates) and calls the Func<CustomDialogResults> (which
is queud up in the actaul 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 Yes for "Are sure" ViewModel prompt
return CustomDialogResults.Yes;
}
);
testMessageBoxService.ShowYesNoResponders.Enqueue
(() =>
{
//return Yes for "Are totally sure" ViewModel prompt
return CustomDialogResults.Yes;
}
);
By using this method we can gaurentee 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
/// <summary>
/// This interface defines a interface that will allow
/// a ViewModel to show a messagebox
/// </summary>
public interface IMessageBoxService
{
/// <summary>
/// Shows an error message
/// </summary>
/// <param name="message">The error message</param>
void ShowError(string message);
/// <summary>
/// Shows an information message
/// </summary>
/// <param name="message">The information message</param>
void ShowInformation(string message);
/// <summary>
/// Shows an warning message
/// </summary>
/// <param name="message">The warning message</param>
void ShowWarning(string message);
/// <summary>
/// Displays a Yes/No dialog and returns the user input.
/// </summary>
/// <param name="message">The message to be displayed.</param>
/// <param name="icon">The icon to be displayed.</param>
/// <returns>User selection.</returns>
CustomDialogResults ShowYesNo(string message, CustomDialogIcons icon);
/// <summary>
/// Displays a Yes/No/Cancel dialog and returns the user input.
/// </summary>
/// <param name="message">The message to be displayed.</param>
/// <param name="icon">The icon to be displayed.</param>
/// <returns>User selection.</returns>
CustomDialogResults ShowYesNoCancel(string message, CustomDialogIcons icon);
/// <summary>
/// Displays a OK/Cancel dialog and returns the user input.
/// </summary>
/// <param name="message">The message to be displayed.</param>
/// <param name="icon">The icon to be displayed.</param>
/// <returns>User selection.</returns>
CustomDialogResults ShowOkCancel(string message, CustomDialogIcons icon);
}
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
/// <summary>
/// This interface defines a interface that will allow
/// a ViewModel to open a file
/// </summary>
public interface IOpenFileService
{
/// <summary>
/// FileName
/// </summary>
String FileName { get; set; }
/// <summary>
/// Filter
/// </summary>
String Filter { get; set; }
/// <summary>
/// Filter
/// </summary>
String InitialDirectory { get; set; }
/// <summary>
/// This method should show a window that allows a file to be selected
/// </summary>
/// <param name="owner">The owner window of the dialog</param>
/// <returns>A bool from the ShowDialog call</returns>
bool? ShowDialog(Window owner);
}
This work 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))
{
// Create a file to write to.
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
/// <summary>
/// This interface defines a interface that will allow
/// a ViewModel to save a file
/// </summary>
public interface ISaveFileService
{
/// <summary>
/// FileName
/// </summary>
Boolean OverwritePrompt { get; set; }
/// <summary>
/// FileName
/// </summary>
String FileName { get; set; }
/// <summary>
/// Filter
/// </summary>
String Filter { get; set; }
/// <summary>
/// Filter
/// </summary>
String InitialDirectory { get; set; }
/// <summary>
/// This method should show a window that allows a file to be saved
/// </summary>
/// <param name="owner">The owner window of the dialog</param>
/// <returns>A bool from the ShowDialog call</returns>
bool? ShowDialog(Window owner);
}
I do not know about you lot, but we are in the middle of a very large WPF project at work, and although I am not fan of popup windows, we do have some none the less. 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 occassionally 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 implementatoion 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 a 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 apps main window as follows:
public MainWindow()
{
//register known windows
IUIVisualizerService popupVisualizer =
ViewModelBase.ServiceProvider.Resolve<IUIVisualizerService>();
popupVisualizer.Register("PropertyListPopup",
typeof(PropertyListPopup));
popupVisualizer.Register("ReferencedAssembliesPopup",
typeof(ReferencedAssembliesPopup));
popupVisualizer.Register("StringEntryPopup",
typeof(StringEntryPopup));
this.DataContext = new MainWindowViewModel();
this.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
{
/// <summary>
/// This class implements the IUIVisualizerService for WPF purposes.
/// This implementation HAD TO be in the Main interface project, as
/// it needs to know about Popup windows that are not known about in
/// the ViewModel or Cinch projects.
/// </summary>
public class WPFUIVisualizerService : Cinch.IUIVisualizerService
{
#region Data
private readonly Dictionary<string, Type> _registeredWindows;
#endregion
#region Ctor
/// <summary>
/// Constructor
/// </summary>
public WPFUIVisualizerService()
{
_registeredWindows = new Dictionary<string, Type>();
//register known windows
Register("AddEditOrderPopup", typeof(AddEditOrderPopup));
}
#endregion
#region Public Methods
/// <summary>
/// Registers a collection of entries
/// </summary>
/// <param name="startupData"></param>
public void Register(Dictionary<string, Type> startupData)
{
foreach (var entry in startupData)
Register(entry.Key, entry.Value);
}
/// <summary>
/// Registers a type through a key.
/// </summary>
/// <param name="key">Key for the UI dialog</param>
/// <param name="winType">Type which implements dialog</param>
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);
}
}
/// <summary>
/// This unregisters a type and removes it from the mapping
/// </summary>
/// <param name="key">Key to remove</param>
/// <returns>True/False success</returns>
public bool Unregister(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException("key");
lock (_registeredWindows)
{
return _registeredWindows.Remove(key);
}
}
/// <summary>
/// This method displays a modaless dialog associated with the given key.
/// </summary>
/// <param name="key">Key previously registered with the UI controller.</param>
/// <param name="state">Object state to associate with the dialog</param>
/// <param name="setOwner">Set the owner of the window</param>
/// <param name="completedProc">Callback used when UI closes (may be null)</param>
/// <returns>True/False if UI is displayed</returns>
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;
}
/// <summary>
/// This method displays a modal dialog associated with the given key.
/// </summary>
/// <param name="key">Key previously registered with the UI controller.</param>
/// <param name="state">Object state to associate with the dialog</param>
/// <returns>True/False if UI is displayed.</returns>
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
/// <summary>
/// This creates the WPF window from a key.
/// </summary>
/// <param name="key">Key</param>
/// <param name="dataContext">DataContext (state) object</param>
/// <param name="setOwner">True/False to set ownership to MainWindow</param>
/// <param name="completedProc">Callback</param>
/// <param name="isModal">True if this is a ShowDialog request</param>
/// <returns>Success code</returns>
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)
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 instruct 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 apps implementation of the 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 the 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 2 you need to know. These are as follows:
Following these simply 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
/// <summary>
/// This interface defines a UI controller which can be used to display dialogs
/// in either modal or modaless form from a ViewModel.
/// </summary>
public interface IUIVisualizerService
{
/// <summary>
/// Registers a type through a key.
/// </summary>
/// <param name="key">Key for the UI dialog</param>
/// <param name="winType">Type which implements dialog</param>
void Register(string key, Type winType);
/// <summary>
/// This unregisters a type and removes it from the mapping
/// </summary>
/// <param name="key">Key to remove</param>
/// <returns>True/False success</returns>
bool Unregister(string key);
/// <summary>
/// This method displays a modaless dialog associated with the given key.
/// </summary>
/// <param name="key">Key previously registered with the UI controller.</param>
/// <param name="state">Object state to associate with the dialog</param>
/// <param name="setOwner">Set the owner of the window</param>
/// <param name="completedProc">Callback used when UI closes (may be null)</param>
/// <returns>True/False if UI is displayed</returns>
bool Show(string key, object state, bool setOwner,
EventHandler<UICompletedEventArgs> completedProc);
/// <summary>
/// This method displays a modal dialog associated with the given key.
/// </summary>
/// <param name="key">Key previously registered with the UI controller.</param>
/// <param name="state">Object state to associate with the dialog</param>
/// <returns>True/False if UI is displayed.</returns>
bool? ShowDialog(string key, object state);
}
If your app doesn't use popups you can
Cinch.ViewModelBase class to remove all references
of IUIVisualizerService
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.
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
{
/// <summary>
/// Provides a set of commonly used Dispatcher extension methods
/// </summary>
public static class DispatcherExtensions
{
#region Dispatcher Extensions
/// <summary>
/// A simple threading extension method, to invoke a delegate
/// on the correct thread if it is not currently on the correct thread
/// which can be used with DispatcherObject types.
/// </summary>
/// <param name="dispatcher">The Dispatcher object on which to
/// perform the Invoke</param>
/// <param name="action">The delegate to run</param>
/// <param name="priority">The DispatcherPriority for the invoke.</param>
public static void InvokeIfRequired(this Dispatcher dispatcher,
Action action, DispatcherPriority priority)
{
if (!dispatcher.CheckAccess())
{
dispatcher.Invoke(priority, action);
}
else
{
action();
}
}
/// <summary>
/// A simple threading extension method, to invoke a delegate
/// on the correct thread if it is not currently on the correct thread
/// which can be used with DispatcherObject types.
/// </summary>
/// <param name="dispatcher">The Dispatcher object on which to
/// perform the Invoke</param>
/// <param name="action">The delegate to run</param>
public static void InvokeIfRequired(this Dispatcher dispatcher, Action action)
{
if (!dispatcher.CheckAccess())
{
dispatcher.Invoke(DispatcherPriority.Normal, action);
}
else
{
action();
}
}
/// <summary>
/// A simple threading extension method, to invoke a delegate
/// on the correct thread if it is not currently on the correct thread
/// which can be used with DispatcherObject types.
/// </summary>
/// <param name="dispatcher">The Dispatcher object on which to
/// perform the Invoke</param>
/// <param name="action">The delegate to run</param>
public static void InvokeInBackgroundIfRequired(
this Dispatcher dispatcher,
Action action)
{
if (!dispatcher.CheckAccess())
{
dispatcher.Invoke(DispatcherPriority.Background, action);
}
else
{
action();
}
}
/// <summary>
/// A simple threading extension method, to invoke a delegate
/// on the correct thread asynchronously if it is not currently
/// on the correct thread which can be used with DispatcherObject types.
/// </summary>
/// <param name="dispatcher">The Dispatcher object on which to
/// perform the Invoke</param>
/// <param name="action">The delegate to run</param>
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(() =>
{
//run some code on the correct UI Dispatcher thread
//at the DispatcherPriority stated
},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.
Within WPF there is no App.DoEvents() that some WinForms
convertees might be
expecting. For those that have not used the the old WinForms App.DoEvents().
What that method used to do was forced the message pump to process all queued
messages. This would sometimes help with things like selection changes, pending
events etc 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 2 flavours of App.DoEvents:
DispatcherPriority for the
lower limit that all pending Dispatcher messages should be
effectively pumped. 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
/// <summary>
/// Forces the WPF message pump to process all enqueued messages
/// that are above the input parameter DispatcherPriority.
/// </summary>
/// <param name="priority">The DispatcherPriority to use
/// as the lowest level of messages to get processed</param>
[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();
}
}
/// <summary>
/// Forces the WPF message pump to process all enqueued messages
/// that are DispatcherPriority.Background or above
/// </summary>
[SecurityPermissionAttribute(SecurityAction.Demand,
Flags = SecurityPermissionFlag.UnmanagedCode)]
public static void DoEvents()
{
DoEvents(DispatcherPriority.Background);
}
/// <summary>
/// Stops the dispatcher from continuing
/// </summary>
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, using call like the following:
ApplicationHelper.DoEvents();
Or to process all message of a particular DispatcherPriority of above, use the following
ApplicationHelper.DoEvents(DispatcherPriority.Background);
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 wire 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
/// <summary>
/// Constructs a new BackgroundTaskManager with
/// the function to run, and the action to call when the function to run
/// completes
/// </summary>
/// <param name="taskFunc">The function to run in the background</param>
/// <param name="completionAction">The completed action to call
/// when the background function completes</param>
public BackgroundTaskManager(Func<T> taskFunc, Action<T> completionAction)
{
this.TaskFunc = taskFunc;
this.CompletionAction = completionAction;
}
#endregion
#region Public Properties
/// <summary>
/// Event invoked when a background task is started.
/// </summary>
[SuppressMessage("Microsoft.Usage", "CA2211:NonConstantFieldsShouldNotBeVisible",
Justification = "Add/remove is thread-safe for events in .NET.")]
public EventHandler<EventArgs> BackgroundTaskStarted;
/// <summary>
/// Event invoked when a background task completes.
/// </summary>
[SuppressMessage("Microsoft.Usage", "CA2211:NonConstantFieldsShouldNotBeVisible",
Justification = "Add/remove is thread-safe for events in .NET.")]
public EventHandler<EventArgs> BackgroundTaskCompleted;
/// <summary>
/// Allows the Unit test to be notified on Task completion
/// </summary>
public AutoResetEvent CompletionWaitHandle { get; set; }
#endregion
#region Public Methods
/// <summary>
/// Runs a task function on a background thread;
/// invokes a completion action on the main thread.
/// </summary>
public void RunBackgroundTask()
{
// Create a BackgroundWorker instance
var backgroundWorker = new BackgroundWorker();
// Attach to its DoWork event to run the task function and capture the result
backgroundWorker.DoWork += delegate(object sender, DoWorkEventArgs e)
{
e.Result = TaskFunc();
};
// Attach to its RunWorkerCompleted event to run the completion action
backgroundWorker.RunWorkerCompleted +=
delegate(object sender, RunWorkerCompletedEventArgs e)
{
// Call the completion action
CompletionAction((T)e.Result);
// Invoke the BackgroundTaskCompleted event
var backgroundTaskFinishedHandler = BackgroundTaskCompleted;
if (null != backgroundTaskFinishedHandler)
{
backgroundTaskFinishedHandler.Invoke(null, EventArgs.Empty);
}
};
// Invoke the BackgroundTaskStarted event
var backgroundTaskStartedHandler = BackgroundTaskStarted;
if (null != backgroundTaskStartedHandler)
{
backgroundTaskStartedHandler.Invoke(null, EventArgs.Empty);
}
// Run the BackgroundWorker asynchronously
backgroundWorker.RunWorkerAsync();
}
#endregion
}
}
So how do you use one of these here 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:
BackgroundTaskManager<T>BackgroundTaskManager<T> Func<T> taskFunc, Action<T> completionAction
within the constructor of the BackgroundTaskManager<T>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
{
//background workers
private BackgroundTaskManager<DispatcherNotifiedObservableCollection<OrderModel>>
bgWorker = null;
public AddEditCustomerViewModel()
{
//setup background worker
SetUpBackgroundWorker();
}
/// <summary>
/// Background worker which lazy fetches
/// Customer Orders
/// </summary>
public BackgroundTaskManager<DispatcherNotifiedObservableCollection<OrderModel>> BgWorker
{
get { return bgWorker; }
set
{
bgWorker = value;
OnPropertyChanged(() => BgWorker);
}
}
/// <summary>
/// Setup backgrounder worker Task/Completion action
/// to fetch Orders for Customers
/// </summary>
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;
});
}
/// <summary>
/// Fetches all Orders for customer using BackgroundTaskManager<T>
/// </summary>
private void LazyFetchOrdersForCustomer()
{
bgWorker.RunBackgroundTask();
}
}
}
Another thing that occurs occasionally is that you may have an
ObservableCollection that has items added to it that need to be marshalled back
to a UI thread to make use of the new items. Cinch uses the
following code to achieve this.
/// <summary>
/// This class provides an ObservableCollection which supports the
/// Dispatcher thread marshalling for added items.
///
/// This class does not take support any thread sycnhronization of
/// adding items using multiple threads, that level of thread synchronization
/// is left to the user. This class simply marshalls the CollectionChanged
/// call to the correct Dispatcher thread
/// </summary>
/// <typeparam name="T">Type this collection holds</typeparam>
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
/// <summary>
/// Occurs when an item is added, removed, changed, moved,
/// or the entire list is refreshed.
/// </summary>
public override event NotifyCollectionChangedEventHandler CollectionChanged;
/// <summary>
/// Raises the <see cref="E:System.Collections.ObjectModel.
/// ObservableCollection`1.CollectionChanged"/>
/// event with the provided arguments.
/// </summary>
/// <param name="e">Arguments of the event being raised.</param>
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
}
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 simple a hierarchical structure (think tree) that
allows some code to be run either using a Click or can use a 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, its just how we create the collection of
Menus and Style them in the View that represents any difficulty at all.
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
{
/// <summary>
/// Provides a mechanism for constructing MenuItems
/// within a ViewModel
/// </summary>
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.
Well this too is actually quite simple. We just expose a property of our ViewModel for the MenuItems we want to expose. Here is an example property within a ViewModel.
/// <summary>
/// Returns the bindbable Menu options
/// </summary>
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 MenuItems to expose from the ViewModel. Here is an example of what this might look like:
/// <summary>
/// Creates and returns the menu items
/// </summary>
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;
}
Can you see that in this method we are not only creating the structure of the
MenuItems but we are also wiring up the MenuItems to the correct ViewModel
available ICommands. It is that easy.
The last part of the puzzle is how to render the ViewModel exposed MenuItems 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 MenuItems:
<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>
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 1 thing. When WPF came out it did not come with a way to do MDI interfaces out of the box. I 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 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 1 Window app.
Most WPF apps you see out there are also 1 Window apps. When I do a new WPF app I try and make it a 1 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 1 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
{
/// <summary>
/// This ViewModelBase subclass requests to be removed
/// from the UI when its CloseWorkSpace executes.
/// This class is abstract.
/// </summary>
public abstract class WorkspaceViewModel : ViewModelBase
{
#region Data
private SimpleCommand closeWorkSpaceCommand;
private Boolean isCloseable = true;
#endregion
#region Constructor
protected WorkspaceViewModel()
{
//This is used for popup control only
closeWorkSpaceCommand = new SimpleCommand
{
CanExecuteDelegate = x => true,
ExecuteDelegate = x => ExecuteCloseWorkSpaceCommand()
};
}
#endregion // Constructor
#region Public Properties
/// <summary>
/// Returns the command that, when invoked, attempts
/// to remove this workspace from the user interface.
/// </summary>
public SimpleCommand CloseWorkSpaceCommand
{
get
{
return closeWorkSpaceCommand;
}
}
public Boolean IsCloseable
{
get { return isCloseable; }
set
{
isCloseable = value;
OnPropertyChanged(() => IsCloseable);
}
}
#endregion // CloseCommand
#region Private Methods
/// <summary>
/// Executes the CloseWorkSpace Command
/// </summary>
private void ExecuteCloseWorkSpaceCommand()
{
CloseWorkSpaceCommand.CommandSucceeded = false;
EventHandler<EventArgs> handlers = CloseWorkSpace;
// Invoke the event handlers
if (handlers != null)
{
try
{
handlers(this, EventArgs.Empty);
CloseWorkSpaceCommand.CommandSucceeded = true;
}
catch
{
Logger.Log(LogType.Error, "Error firing CloseWorkSpace event");
}
}
}
#endregion
#region CloseWorkSpace Event
/// <summary>
/// Raised when this workspace should be removed from the UI.
/// </summary>
public event EventHandler<EventArgs> CloseWorkSpace;
#endregion
}
}
So to make use of this simple 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 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);
}
/// <summary>
/// If we get a request to add a new Workspace, add a new WorkSpace to the
/// collection and hook up the CloseWorkSpace event in a weak manner
/// </summary>
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);
}
/// <summary>
/// If we get a request to close a new Workspace, remove the WorkSpace from the
/// collection
/// </summary>
private void OnCloseWorkSpace(object sender, EventArgs e)
{
WorkspaceViewModel workspace = sender as WorkspaceViewModel;
workspace.Dispose();
this.Workspaces.Remove(workspace);
}
/// <summary>
/// Sets a ViewModel to be active, which for the View equates
/// to selected Tab
/// </summary>
/// <param name="workspace">workspace to activate</param>
private void SetActiveWorkspace(WorkspaceViewModel workspace)
{
ICollectionView collectionView =
CollectionViewSource.GetDefaultView(this.Workspaces);
if (collectionView != null)
collectionView.MoveCurrentTo(workspace);
}
/// <summary>
/// The active workspace ViewModels
/// </summary>
public ObservableCollection<WorkspaceViewModel> Workspaces
{
get { return workspaces; }
set
{
if (workspaces == null)
{
workspaces = value;
OnPropertyChanged("Workspaces");
}
}
}
}
}
So 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
<!-- MainTabControlTemplateEx -->
<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>
<!-- no content presenter -->
<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 just a case of making sure that the
correct View DataTemplates are available within a 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>
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.
/// <summary>
/// This class forces focus to set on the specified UIElement
/// </summary>
public class FocusHelper
{
/// <summary>
/// Set focus to UIElement
/// </summary>
/// <param name="element">The element to set focus on</param>
public static void Focus(UIElement element)
{
//Focus in a callback to run on another thread, ensuring the main
//UI thread is initialized by the time focus is set
ThreadPool.QueueUserWorkItem(delegate(Object theElement)
{
UIElement elem = (UIElement)theElement;
elem.Dispatcher.Invoke(DispatcherPriority.Normal,
(Action)delegate()
{
elem.Focus();
Keyboard.Focus(elem);
});
}, element);
}
}
In the subsequent articles I will be showcasing it roughly like this
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 show you how to develop an app with Cinch.
As always votes / comments are welcome.
General
News
Question
Answer
Joke
Rant
Admin
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 17 Oct 2009 Editor: |
Copyright 2009 by Sacha Barber Everything else Copyright © CodeProject, 1999-2009 Web22 | Advertise on the Code Project |