Table of Contents
Introduction
Last time, we talked about what's brand new in Cinch V2. In this article, we will compare Cinch V2 with
Cinch V1 and talk about what has changed and what has stayed the same, and where appropriate, I shall show links to both
Cinch V1 code and Cinch V2 code, so you can see for yourself what has changed.
As promised, within each article, I shall be showing the Cinch V2 compatibility matrix.
The compatibility matrix shows a list of classes along with their general work area, and whether they are compatible with WPF or SL or both.
Work Area |
Class Name |
WPF |
Silverlight (4 or above) |
Both |
Business objects |
EditableValidatingObject.cs |
|
|
Yes |
Business objects |
ValidatingObject.cs |
|
|
Yes |
Business objects |
DataWrapper.cs |
|
|
Yes |
Commands |
EventToCommandArgs.cs |
|
|
Yes |
Commands |
SimpleCommand.cs |
|
|
Yes |
Commands |
WeakEventHandlerManager.cs |
|
|
Yes |
Events |
CloseRequestEventArgs.cs |
|
|
Yes |
Events |
UICompletedEventArgs.cs |
|
|
Yes |
WeakEvents |
WeakEvent.cs |
|
|
Yes |
WeakEvents |
WeakEventHelper.cs |
|
|
Yes |
WeakEvents |
WeakEventProxy.cs |
|
|
Yes |
Extension Methods |
DispatcherExtensions.cs |
Yes |
|
|
Extension Methods |
GenericListExtensions.cs |
|
Yes |
|
Interactivity Actions |
CommandDrivenGoToStateAction.cs |
|
|
Yes |
Interactivity Behaviours |
FocusBehaviourBase.cs |
Yes |
|
|
Interactivity Behaviours |
NumericTextBoxBehaviour.cs |
Yes |
|
|
Interactivity Behaviours |
SelectorDoubleClickCommandBehavior.cs |
Yes |
|
|
Interactivity Behaviours |
TextBoxFocusBehavior.cs |
Yes |
|
|
Interactivity Triggers |
CompletedAwareCommandTrigger.cs |
|
|
Yes |
Interactivity Triggers |
CompletedAwareGotoStateCommandTrigger.cs |
|
|
Yes |
Interactivity Triggers |
EventToCommandTrigger.cs |
|
|
Yes |
Messager Mediator |
MediatorMessageSinkAttribute.cs |
|
|
Yes |
Messager Mediator |
MediatorSingleton.cs |
|
|
Yes |
Services Implementation |
ChildWindowService.cs |
|
Yes |
|
Services Implementation |
SLMessageBoxService.cs |
|
Yes |
|
Services Implementation |
ViewAwareStatus.cs |
|
|
Yes |
Services Implementation |
ViewAwareStatusWindow.cs |
Yes |
|
|
Services Implementation |
VSMService.cs |
|
|
Yes |
Services Implementation |
WPFMessageBoxService.cs |
Yes |
|
|
Services Implementation |
WPFOpenFileService.cs |
Yes |
|
|
Services Implementation |
WPFSaveFileService.cs |
Yes |
|
|
Services Implementation |
WPFUIVisualizerService.cs |
Yes |
|
|
Services Interfaces |
IChildWindowService.cs |
|
Yes |
|
Services Interfaces |
IMessageBoxService.cs |
|
Yes |
|
Services Interfaces |
IViewAwareStatus.cs |
|
|
Yes |
Services Interfaces |
IViewAwareStatusWindow.cs |
Yes |
|
|
Services Interfaces |
IVSM.cs |
|
|
Yes |
Services Interfaces |
IMessageBoxService.cs |
Yes |
|
|
Services Interfaces |
IOpenFileService.cs |
Yes |
|
|
Services Interfaces |
ISaveFileService.cs |
Yes |
|
|
Services Interfaces |
IUIVisualizerService.cs |
Yes |
|
|
Services Test Implementations |
TestChildWindowService.cs |
|
Yes |
|
Services Test Implementations |
TestMessageBoxService.cs |
|
Yes |
|
Services Test Implementations |
TestViewAwareStatus.cs |
|
|
Yes |
Services Test Implementations |
TestViewAwareStatusWindow.cs |
Yes |
|
|
Services Test Implementations |
TestVSMService.cs |
|
|
Yes |
Services Test Implementations |
TestMessageBoxService.cs |
Yes |
|
|
Services Test Implementations |
TestOpenFileService.cs |
Yes |
|
|
Services Test Implementations |
TestSaveFileService.cs |
Yes |
|
|
Services Test Implementations |
TestUIVisualizerService.cs |
Yes |
|
|
Threading |
AddRangeObservableCollection.cs (this is specific SL implementation) |
|
Yes |
|
Threading |
AddRangeObservableCollection.cs (this is specific WPF implementation) |
Yes |
|
|
Threading |
BackgroundTaskManager.cs |
|
|
Yes |
Threading |
ISynchronizationContext.cs |
|
|
Yes |
Threading |
UISynchronizationContext.cs |
|
|
Yes |
Threading |
ApplicationHelper.cs |
Yes |
|
|
Threading |
DispatcherNotifiedObservableCollection.cs |
Yes |
|
|
Menus |
CinchMenuItem.cs |
|
|
Yes |
Utilities |
ArgumentValidator.cs |
|
|
Yes |
Utilities |
IWeakEventListener.cs (this is a System class missing from SL, so I created it) |
|
Yes |
|
Utilities |
ObservableHelper.cs |
|
|
Yes |
Utilities |
PropertyChangedEventManager.cs (this is a System class missing from SL, so I created it) |
|
Yes |
|
Utilities |
PropertyObserver.cs |
|
|
Yes |
Utilities |
BindingEvaluator.cs |
Yes |
|
|
Utilities |
ObservableDictionary.cs |
Yes |
|
|
Utilities |
TreeHelper.cs |
Yes |
|
|
Validation |
RegexRule.cs |
|
|
Yes |
Validation |
Rule.cs |
|
|
Yes |
Validation |
SimpleRule.cs |
|
|
Yes |
ViewModels |
EditableValidatingViewModelBase.cs |
|
|
Yes |
ViewModels |
IViewStatusAwareInjectionAware.cs |
|
|
Yes |
ViewModels |
ValidatingViewModelBase.cs |
|
|
Yes |
ViewModels |
ViewMode.cs |
|
|
Yes |
ViewModels |
ViewModelBase.cs |
|
|
Yes |
ViewModels |
ViewModelBaseSLSpecific.cs |
|
Yes |
|
ViewModels |
ViewModelBaseWPFSpecific.cs |
Yes |
|
|
Workspaces |
ChildWindowResolver.cs |
|
Yes |
|
Workspaces |
CinchBootStrapper.cs (SL Version) |
|
Yes |
|
Workspaces |
CinchBootStrapper.cs (WPF version) |
Yes |
|
|
Workspaces |
PopupNameToViewLookupKeyMetadataAttribute.cs |
|
|
Yes |
Workspaces |
IWorkspaceAware.cs |
Yes |
|
|
Workspaces |
MockView.cs |
Yes |
|
|
Workspaces |
NavProps.cs |
Yes |
|
|
Workspaces |
PopupResolver.cs |
Yes |
|
|
Workspaces |
ViewnameToViewLookupKeyMetadataAttribute.cs |
Yes |
|
|
Workspaces |
ViewResolver.cs |
Yes |
|
|
Workspaces |
WorkspaceData.cs |
Yes |
|
|
Now that I have shown you what classes will work with WPF/SL, let's get on with the rest of this article, shall we? But first, here are the links
to the old Cinch V1 articles.
In case you missed Cinch V1, and have an interest in MVVM, I would strongly recommend that you read
all the Cinch V1 articles first, as it will give you a much deeper understanding of the content that will be presented
in these Cinch V2 articles.
CinchV1 Article Links
Some of you may never have seen the old Cinch V1 articles, so I will also include a list of these here,
as where the Cinch V2 still uses the same functionality as Cinch V1, I will be redirecting
people to these articles.
CinchV2 Article Links
That is what the article roadmap looks like. I guess it is now time to dive into the guts of this article, so let's go:
What Has Changed / What Has Stayed the Same?
Now we can get into the guts of this article which is really all about showing new/old Cinch users what has changed
and what has stayed the same.
The Changes From V1 to V2
This sub section simply discusses what aspects of Cinch V1 have changed from V1 to V2.
SimpleCommand
Within Cinch V1, I did have a basic delegate style command, but I did not really make it that easy to use so
in Cinch V2, I took it a step further and created a better SimpleCommand
which allows you to pass
in a Func<T,TResult>
for the CanExecute
and an Action<T>
for the Execute
. I also
provide a CommandCompleted
event which is useful for all sorts of things. Have a look at this link to find out
more: CinchV2_3.aspx#SimpleCommand.
Attached Properties / Event To Command
Within Cinch V1, I offered a number of attached DPs. These have largely been replaced by Blend Interactivity
Actions/Triggers/Behaviours. Let's have a look at each of the Cinch V1 offerings and I will show you what they have been replaced with in
Cinch V2.
View LifeCyle Events
These were simple attached DPs that you could use to run commands in your ViewModel. There were ones for Loaded/Unloaded/Activated/DeActivated etc.
These have now been replaced by the Cinch V2 IViewAwareStatus
service.
You can read more about the way Cinch V1 did this using this
link: CinchII.aspx#Lifecycle, and how the Cinch
V2 IViewAwareStatus
service works using this link: CinchV2_2.aspx#CoreServices.
Numeric TextBox Attached Behaviour
This was a simple attached DP that allowed the user to specify that a TextBox should only accept numeric characters. This has simply been turned into a Blend Behaviour.
You can read more about the way Cinch V1 did this using
this link: CinchII.aspx#NumericAtt, and how the Cinch
V2 Blend Behaviour works using this link: CinchV2_3.aspx#Interactivity.
Attached Command Behaviour
This was a collection of attached DPs that allowed the user to wire up an event from a certain FrameworkElement
to a bound ICommand
in their ViewModel. This has
now been replaced by a single Blend Trigger called EventToCommandTrigger
.
You can read more about the way Cinch V1 did this using this
link: CinchII.aspx#CommandAtt, and how the Cinch
V2 EventToCommandTrigger
works using this link: CinchV2_3.aspx#Interactivity.
Workspaces
Within Cinch V1, I offered a ViewModel first type of approach, and suggested View-ViewModel resolution using a
DataTemplate style approach. This is still supported in Cinch V2, but the design time support offered is far less than the new method
of managing workspaces in Cinch V2.
You can read more about both these approaches using these links:
Threading Helpers
Within Cinch V1, I already included a number of threading related helpers, such as Application.DoEvents
and Dispatcher
extension methods. For Cinch V2, I have included far more threading helper classes, and also included the
original Cinch V1 classes.
Utilities
Within Cinch V1, I already included a number of utilities related helpers, such as ObservableHelper
,
PropertyObserver
. For Cinch V2, I have included far more utility helper classes, and also included the original
Cinch V1 classes.
The Same From V1 to V2
This sub section simply discusses what aspects of Cinch V1 remains the same for V1 and V2.
ViewModel Modes
One of the things I have always struggled with when working with MVVM and using it to produce LOB apps is View mode. For example, it would be nice to have
a View that is read only and then the user clicks edit and then all the fields on the View are editable. Now, this could be achieved by having a command in the ViewModel
that changes from ReadOnly
mode to EditMode
say, and all the UIElement
s on the View could bind to some CurrentMode
property
on the ViewModel. Sounds do'able, but as we all know, things are never as clean cut as that. In my workplace, we have complicated requirements around data entry and there is no way
a single mode can be applied to all data entry fields on a single View, no way. We need very granular data entry rights, down to the individual field level.
So this got me thinking. What we need is an editable state for each data item in a UI ViewModel. I thought about this some more and came up with a generic wrapper
class which wraps a single property but also exposes an IsEditable
property. Now the View can access these wrappers as they are public properties in the ViewModel,
so it can bind its data to the wrapper's data property and can disable data entry based on the wrapper's IsEditable
property.
To this end, I came up with a simple class that looks like this:
using System;
using System.Reflection;
using System.Diagnostics;
using System.Linq;
using System.ComponentModel;
using System.Collections.Generic;
namespace Cinch
{
public abstract class DataWrapperDirtySupportingBase : EditableValidatingObject
{
#region Public Properties
public bool HasPropertyChanged(string propertyName)
{
if (_savedState == null)
return false;
object saveValue;
object currentValue;
if (!_savedState.TryGetValue(propertyName, out saveValue) ||
!this.GetFieldValues().TryGetValue(propertyName, out currentValue))
return false;
if (saveValue == null || currentValue == null)
return saveValue != currentValue;
return !saveValue.Equals(currentValue);
}
#endregion
}
public abstract class DataWrapperBase : DataWrapperDirtySupportingBase
{
#region Data
private Boolean isEditable = false;
private IParentablePropertyExposer parent = null;
private PropertyChangedEventArgs parentPropertyChangeArgs = null;
#endregion
#region Ctors
public DataWrapperBase()
{
}
public DataWrapperBase(IParentablePropertyExposer parent,
PropertyChangedEventArgs parentPropertyChangeArgs)
{
this.parent = parent;
this.parentPropertyChangeArgs = parentPropertyChangeArgs;
}
#endregion
#region Protected Methods
protected internal void NotifyParentPropertyChanged()
{
if (parent == null || parentPropertyChangeArgs == null)
return;
Delegate[] subscribers = parent.GetINPCSubscribers();
if (subscribers != null)
{
foreach (PropertyChangedEventHandler d in subscribers)
{
d(parent, parentPropertyChangeArgs);
}
}
}
#endregion
#region Public Properties
static PropertyChangedEventArgs isEditableChangeArgs =
ObservableHelper.CreateArgs<DataWrapperBase>(x => x.IsEditable);
public Boolean IsEditable
{
get { return isEditable; }
set
{
if (isEditable != value)
{
isEditable = value;
NotifyPropertyChanged(isEditableChangeArgs);
NotifyParentPropertyChanged();
}
}
}
#endregion
}
public interface IDataWrapper<T>
{
T DataValue { get; set; }
}
public interface IChangeIndicator
{
bool IsDirty { get; }
}
public interface IParentablePropertyExposer
{
Delegate[] GetINPCSubscribers();
}
public class DataWrapper<T> : DataWrapperBase,
IDataWrapper<T>, IChangeIndicator
{
#region Data
private T dataValue = default(T);
private bool isDirty = false;
#endregion
#region Ctors
public DataWrapper()
{
}
public DataWrapper(T initialValue)
{
dataValue = initialValue;
}
public DataWrapper(IParentablePropertyExposer parent,
PropertyChangedEventArgs parentPropertyChangeArgs)
: base(parent, parentPropertyChangeArgs)
{
}
#endregion
#region Public Properties
static PropertyChangedEventArgs dataValueChangeArgs =
ObservableHelper.CreateArgs<DataWrapper<T>>(x => x.DataValue);
public T DataValue
{
get { return dataValue; }
set
{
dataValue = value;
NotifyPropertyChanged(dataValueChangeArgs);
NotifyParentPropertyChanged();
IsDirty = this.HasPropertyChanged("dataValue");
}
}
static PropertyChangedEventArgs isDirtyChangeArgs =
ObservableHelper.CreateArgs<DataWrapper<T>>(x => x.IsDirty);
public bool IsDirty
{
get { return isDirty; }
set
{
isDirty = value;
NotifyPropertyChanged(isDirtyChangeArgs);
NotifyParentPropertyChanged();
}
}
#endregion
}
public class DataWrapperHelper
{
#region Public Methods
ViewMode currentViewMode)
{
bool isEditable = currentViewMode ==
ViewMode.EditMode || currentViewMode == ViewMode.AddMode;
foreach (var wrapperProperty in wrapperProperties)
{
try
{
wrapperProperty.IsEditable = isEditable;
}
catch (Exception)
{
Debug.WriteLine("There was a problem setting the currentViewMode");
}
}
}
public static void SetBeginEdit(IEnumerable<DataWrapperBase> wrapperProperties)
{
foreach (var wrapperProperty in wrapperProperties)
{
try
{
wrapperProperty.BeginEdit();
wrapperProperty.NotifyParentPropertyChanged();
}
catch (Exception)
{
Debug.WriteLine("There was a problem calling " +
"the BeginEdit method for the current DataWrapper");
}
}
}
public static void SetCancelEdit(IEnumerable<DataWrapperBase> wrapperProperties)
{
foreach (var wrapperProperty in wrapperProperties)
{
try
{
wrapperProperty.CancelEdit();
wrapperProperty.NotifyParentPropertyChanged();
}
catch (Exception)
{
Debug.WriteLine("There was a problem calling " +
"the CancelEdit method for the current DataWrapper");
}
}
}
public static void SetEndEdit(IEnumerable<DataWrapperBase> wrapperProperties)
{
foreach (var wrapperProperty in wrapperProperties)
{
try
{
wrapperProperty.EndEdit();
wrapperProperty.NotifyParentPropertyChanged();
}
catch (Exception)
{
Debug.WriteLine("There was a problem calling " +
"the EndEdit method for the current DataWrapper");
}
}
}
public static Boolean AllValid(IEnumerable<DataWrapperBase> wrapperProperties)
{
Boolean allValid = true;
foreach (var wrapperProperty in wrapperProperties)
{
try
{
allValid &= wrapperProperty.IsValid;
if (!allValid)
break;
}
catch (Exception)
{
allValid = false;
Debug.WriteLine("There was a problem calling " +
"the IsValid method for the current DataWrapper");
}
}
return allValid;
}
public static IEnumerable<DataWrapperBase> GetWrapperProperties<T>(T parentObject)
{
var properties = parentObject.GetType().GetProperties(
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
List<DataWrapperBase> wrapperProperties = new List<DataWrapperBase>();
foreach (var propItem in parentObject.GetType().GetProperties(
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
{
if (propItem.CanRead && propItem.GetIndexParameters().Count() == 0)
{
if (typeof(DataWrapperBase).IsAssignableFrom(propItem.PropertyType) == false)
continue;
var propertyValue = propItem.GetValue(parentObject, null);
if (propertyValue != null && propertyValue is DataWrapperBase)
{
wrapperProperties.Add((DataWrapperBase)propertyValue);
}
}
}
return wrapperProperties;
}
#endregion
}
}
Which can then be used as properties on your ViewModel, like this (don't worry about the inheriting from Cinch.EditableValidatingViewModelBase
, we'll get to that soon):
public class OrderViewModel : Cinch.EditableValidatingViewModelBase
{
private Cinch.DataWrapper<Int32> quantity;
public OrderViewModel()
{
Quantity = new DataWrapper<int32>(this, quantityChangeArgs);
....
....
}
static PropertyChangedEventArgs quantityChangeArgs =
ObservableHelper.CreateArgs<OrderViewModel>(x => x.Quantity);
public Cinch.DataWrapper<Int32> Quantity
{
get { return quantity; }
private set
{
quantity = value;
NotifyPropertyChanged(quantityChangeArgs);
}
}
}
Notice the setter is private
, this is due to the fact that these objects are immutable, and are only allowed to be set in the constructor.
The IsEditable
and DataValue
can be changed whenever you like though. The other thing to note is that the ViewModel actually
uses some Reflection on construction to obtain an IEnumerable<DataWrapperBase>
which is then used as a cache, so setting any of the cached
DataWrapper<T>
properties from that point on is very quick. This is achieved as follows:
In the constructor, we have something like this:
using System;
using Cinch;
using MVVM.DataAccess;
using System.ComponentModel;
using System.Collections.Generic;
namespace MVVM.ViewModels
{
public class OrderViewModel : Cinch.EditableValidatingViewModelBase
{
#region Data
private Cinch.DataWrapper<Int32> orderId;
private Cinch.DataWrapper<Int32> customerId;
private Cinch.DataWrapper<Int32> productId;
private Cinch.DataWrapper<Int32> quantity;
private Cinch.DataWrapper<DateTime> deliveryDate;
private IEnumerable<DataWrapperBase> cachedListOfDataWrappers;
#endregion
#region Ctor
public OrderViewModel()
{
#region Create DataWrappers
OrderId = new DataWrapper<Int32>(this, orderIdChangeArgs);
CustomerId = new DataWrapper<Int32>(this, customerIdChangeArgs);
ProductId = new DataWrapper<Int32>(this, productIdChangeArgs);
Quantity = new DataWrapper<Int32>(this, quantityChangeArgs);
DeliveryDate = new DataWrapper<DateTime>(this, deliveryDateChangeArgs);
cachedListOfDataWrappers =
DataWrapperHelper.GetWrapperProperties<OrderViewModel>(this);
#endregion
}
#endregion
}
}
And then from then on, whenever we deal with the DataWrapper<T>
properties, we can use the cached list.
So getting back to how we might use these in our Views, I simply bind to these DataWrapper<T>
properties as follows:
<TextBox FontWeight="Normal" FontSize="11" Width="200"
Cinch:NumericTextBoxBehavior.IsEnabled="True"
Text="{Binding Path=CurrentCustomerOrder.Quantity.DataValue,
UpdateSourceTrigger=LostFocus, ValidatesOnDataErrors=True,
ValidatesOnExceptions=True}"
Style="{StaticResource ValidatingTextBox}"
IsEnabled="{Binding Path=CurrentCustomerOrder.Quantity.IsEditable}"/>
That's all cool, but how do these DataWrapper<T>
objects respond to a change in mode state? Well, that is quite simple actually.
We do have a Cinch.ViewMode
in the ViewModel and whenever that changes state, we need to update the state of all the nested DataWrapper<T>
objects
in whatever object it is we are trying to change the state for (which could be the ViewModel itself).
Here is an example AddEditOrderViewModel
, which for me holds a single UI Model of type OrderModel
. As I say, others will not like this and would have the ViewModel
expose all the properties available within a UI Model of type OrderModel
. The thing with MVVM is that you do it your own way, and this is my way. I don't care
if InValid
data gets to the Model just so long as that Model can not be saved to the database.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Data;
using System.Linq;
using Cinch;
using MVVM.Models;
using MVVM.DataAccess;
namespace MVVM.ViewModels
{
public class AddEditOrderViewModel : Cinch.EditableValidatingViewModelBase
{
private ViewMode currentViewMode = ViewMode.AddMode;
private Cinch.DataWrapper<Int32> quantity;
#region Ctor
public AddEditOrderViewModel()
{
#region Create DataWrappers
Quantity= new DataWrapper<Int32>(this, quantityChangeArgs );
cachedListOfDataWrappers =
DataWrapperHelper.GetWrapperProperties<AddEditOrderViewModel>(this);
#endregion
}
#endregion
static PropertyChangedEventArgs quantityChangeArgs =
ObservableHelper.CreateArgs<OrderViewModel>(x => x.Quantity);
public Cinch.DataWrapper<Int32> Quantity
{
get { return quantity; }
private set
{
quantity = value;
NotifyPropertyChanged(quantityChangeArgs);
}
}
static PropertyChangedEventArgs currentViewModeChangeArgs =
ObservableHelper.CreateArgs<AddEditOrderViewModel>(x => x.CurrentViewMode);
public ViewMode CurrentViewMode
{
get { return currentViewMode; }
set
{
currentViewMode = value;
switch (currentViewMode)
{
case ViewMode.AddMode:
Quantity.DataValue= 0;
this.DisplayName = "Add Order"
break;
case ViewMode.EditMode:
this.DisplayName = "Edit Order";
break;
case ViewMode.ViewOnlyMode:
this.DisplayName = "View Order";
break;
}
DataWrapperHelper.SetMode(
CachedListOfDataWrappers,
currentViewMode);
NotifyPropertyChanged(currentViewModeChangeArgs);
}
}
....
....
}
}
One thing worth mentioning here is that when the CurrentViewMode
property changes, a DataWrapperHelper
class is used to set all
the cached DataWrapper<T>
objects for a particular object into the same state as that just requested. Here is the code that does that:
public static void SetMode(IEnumerable<DataWrapperBase> wrapperProperties,
ViewMode currentViewMode)
{
bool isEditable = currentViewMode ==
ViewMode.EditMode || currentViewMode == ViewMode.AddMode;
foreach (var wrapperProperty in wrapperProperties)
{
try
{
wrapperProperty.IsEditable = isEditable;
}
catch (Exception)
{
Debug.WriteLine("There was a problem setting the currentViewMode");
}
}
}
Validation Rules/IDataErrorInfo Integration
I recall some time ago Paul Stovell published a great article Delegates and Business Objects
which I simply loved, as it seemed to make so much sense to me. To this end, Cinch makes use of Paul's great idea to use delegates
to provide validation for business objects.
The idea is simply the business objects have the AddRule(Rule newRule)
method which is used to add rules, the business object also implements IDataErrorInfo
,
which is the preferred WPF validation technique. Then, what basically happens is that when the IDataErrorInfo.IsValid
property is called against a particular
business object, all the validation rules (delegates) are checked and a list of broken rules (as dictated by the delegate rules added to the object) are presented
as the IDataErrorInfo.Error
string.
I urge you all to read Paul Stovell's excellent Delegates and Business Objects article
first, but basically, Cinch makes use of this.
What Cinch provides is:
- A
ValidatingObject
base class that can be used which accepts any Rule
based class to be added.
SimpleRule
, a simple delegate rule.
RegexRule
, a regular expression rule.
- Quite nicely only declares the rules once per Type (as they are static fields) which saves on the amount of memory that is required for business object validation.
Here is an example of how to use these with Cinch where the property is a simple type such as String
/Int32
etc.
public class OrderViewModel : Cinch.EditableValidatingViewModelBase
{
private Int32 quantity;
private static SimpleRule quantityRule;
public OrderViewModel()
{
#region Create Validation Rules
quantity.AddRule(quantityRule);
#endregion
}
static OrderViewModel()
{
quantityRule = new SimpleRule("Quantity",
"Quantity can not be < 0",
(Object domainObject)=>
{
OrderModel obj = (OrderModel)domainObject;
return obj.Quantity <= 0;
});
}
}
However, recall I mentioned a special Cinch class to allow the ViewModel to place a single View field into edit mode using
the Cinch.DataWrapper<T>
? Well, we need to do something ever so slightly different for those, we need to do the following:
public class OrderViewModel : Cinch.EditableValidatingViewModelBase
{
#region Data
private Cinch.DataWrapper<Int32> customerId;
private static SimpleRule quantityRule;
#endregion
#region Ctor
public OrderViewModel()
{
....
....
#region Create Validation Rules
quantity.AddRule(quantityRule);
#endregion
}
static OrderModel()
{
quantityRule = new SimpleRule("DataValue",
"Quantity can not be < 0",
(Object domainObject)=>
{
DataWrapper<Int32> obj = (DataWrapper<Int32>)domainObject;
return obj.DataValue <= 0;
});
}
#endregion
#region Public Properties
static PropertyChangedEventArgs quantityChangeArgs =
ObservableHelper.CreateArgs<OrderViewModel>(x => x.Quantity);
public Cinch.DataWrapper<Int32> Quantity
{
get { return quantity; }
private set
{
quantity = value;
NotifyPropertyChanged(quantityChangeArgs);
}
}
#endregion
}
We need to declare the validation rule like this for Cinch.DataWrapper<T>
objects as they are not simply properties but are actual classes,
so we need to specify the DataValue
property of the individual Cinch.DataWrapper<T>
object to validate for the rule.
This also comes into play within the IsValid
method you get when you inherit from a Cinch.EditableValidatingViewModelBase
object.
Let's say you have something like this for a UI ViewModel object:
using System;
using Cinch;
using MVVM.DataAccess;
using System.ComponentModel;
using System.Collections.Generic;
namespace MVVM.ViewModels
{
public class OrderViewModel : Cinch.EditableValidatingViewModelBase
{
#region Data
private Cinch.DataWrapper<Int32> orderId;
private Cinch.DataWrapper<Int32> customerId;
private Cinch.DataWrapper<Int32> productId;
private Cinch.DataWrapper<Int32> quantity;
private Cinch.DataWrapper<DateTime> deliveryDate;
private IEnumerable<DataWrapperBase> cachedListOfDataWrappers;
private static SimpleRule quantityRule;
#endregion
#region Ctor
public OrderViewModel()
{
#region Create DataWrappers
OrderId = new DataWrapper<Int32>(this, orderIdChangeArgs);
CustomerId = new DataWrapper<Int32>(this, customerIdChangeArgs);
ProductId = new DataWrapper<Int32>(this, productIdChangeArgs);
Quantity = new DataWrapper<Int32>(this, quantityChangeArgs);
DeliveryDate = new DataWrapper<DateTime>(this, deliveryDateChangeArgs);
cachedListOfDataWrappers =
DataWrapperHelper.GetWrapperProperties<OrderViewModel>(this);
#endregion
#region Create Validation Rules
quantity.AddRule(quantityRule);
#endregion
DeliveryDate.DataValue = DateTime.Now;
}
static OrderModel()
{
quantityRule = new SimpleRule("DataValue", "Quantity can not be < 0",
(Object domainObject)=>
{
DataWrapper<Int32> obj = (DataWrapper<Int32>)domainObject;
return obj.DataValue <= 0;
});
}
#endregion
#region Public Properties
static PropertyChangedEventArgs orderIdChangeArgs =
ObservableHelper.CreateArgs<OrderViewModel>(x => x.OrderId);
public Cinch.DataWrapper<Int32> OrderId
{
get { return orderId; }
private set
{
orderId = value;
NotifyPropertyChanged(orderIdChangeArgs);
}
}
static PropertyChangedEventArgs customerIdChangeArgs =
ObservableHelper.CreateArgs<OrderViewModel>(x => x.CustomerId);
public Cinch.DataWrapper<Int32> CustomerId
{
get { return customerId; }
private set
{
customerId = value;
NotifyPropertyChanged(customerIdChangeArgs);
}
}
static PropertyChangedEventArgs productIdChangeArgs =
ObservableHelper.CreateArgs<OrderViewModel>(x => x.ProductId);
public Cinch.DataWrapper<Int32> ProductId
{
get { return productId; }
private set
{
productId = value;
NotifyPropertyChanged(productIdChangeArgs);
}
}
static PropertyChangedEventArgs quantityChangeArgs =
ObservableHelper.CreateArgs<OrderViewModel>(x => x.Quantity);
public Cinch.DataWrapper<Int32> Quantity
{
get { return quantity; }
private set
{
quantity = value;
NotifyPropertyChanged(quantityChangeArgs);
}
}
static PropertyChangedEventArgs deliveryDateChangeArgs =
ObservableHelper.CreateArgs<OrderViewModel>(x => x.DeliveryDate);
public Cinch.DataWrapper<DateTime> DeliveryDate
{
get { return deliveryDate; }
private set
{
deliveryDate = value;
NotifyPropertyChanged(deliveryDateChangeArgs);
}
}
public IEnumerable<DataWrapperBase> CachedListOfDataWrappers
{
get { return cachedListOfDataWrappers; }
}
#endregion
#region Overrides
public override bool IsValid
{
get
{
return base.IsValid &&
DataWrapperHelper.AllValid(cachedListOfDataWrappers);
}
}
#endregion
}
}
You would then need to override the IsValid
property to look like this, where we come up with a combined IsValid
for the entire object based
not only on its IsValid
but also the IsValid
state of any nested Cinch.DataWrapper<T>
object, which is very easy as they
also inherit from Cinch.EditableValidatingViewModelBase
which in turn inherits from Cinch.ValidatingViewModelBase
, so they already have
the IDataErrorInfo
implementation, so it is not that hard to cope with.
I know this seems a lot of extra work, but the added benefit of the ViewModel being able to set an individual field's editability state, and have the View reflect
this seamlessly via bindings, simply can not be ignored.
public override bool IsValid
{
get
{
return base.IsValid &&
DataWrapperHelper.AllValid(cachedListOfDataWrappers);
}
}
Typically, the WPF style we would use for a TextBox
that needed to supply validation support for IDataErrorInfo
would look something
like the following, where we use the Validation.HasError
property to change the border color of the TextBox
when there is a validation error present.
<Style x:Key="ValidatingTextBox" TargetType="{x:Type TextBoxBase}">
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="KeyboardNavigation.TabNavigation" Value="None"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="MinWidth" Value="120"/>
<Setter Property="MinHeight" Value="20"/>
<Setter Property="AllowDrop" Value="true"/>
<Setter Property="Validation.ErrorTemplate" Value="{x:Null}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TextBoxBase}">
<Border
Name="Border"
CornerRadius="5"
Padding="2"
Background="White"
BorderBrush="Black"
BorderThickness="2" >
<ScrollViewer Margin="0" x:Name="PART_ContentHost"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Border"
Property="Background" Value="LightGray"/>
<Setter TargetName="Border"
Property="BorderBrush" Value="Black"/>
<Setter Property="Foreground" Value="Gray"/>
</Trigger>
<Trigger Property="Validation.HasError" Value="true">
<Setter TargetName="Border" Property="BorderBrush"
Value="Red"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors).CurrentItem.ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
IEditableObject Support
I have in the past used a pattern called Memento which basically is a cool pattern for support of Undo on business objects. Basically, what it allows
for is the storage of an object's state to a Memento backing object which has the exact same properties as the business object it was storing state for.
So when you start an edit on a business object, you store the current state in a Memento and do your edit. If you cancell the edit, the business object's state
would be restored from the Memento. This does work very well, but Microsoft also supports this via an interface called IEditableObject
which looks like this:
BeginEdit()
CancelEdit()
EndEdit()
Using this interface, we can actually make our business objects store their own state. Now, I can take no credit for this next piece of code, it comes from Mark Smith's excellent work.
Actually, a fair amount of Cinch is down to Mark Smith's work; again, I did ask Mark if I could poach his code,
he said yes, great cheers Mark.
What Cinch does is provide a base class that can be used for you to inherit from for your business objects.
This base class also supports validation via the IDataErrorInfo
interface we just discussed above. Here is how it works. On BeginEdit()
,
a little bit of Reflection/LINQ is used to store the current object's state in an internal Dictionary. On Canceldit()
, the internal
Dictionary values are restored to the current object's properties using the property name as a key into the stored Dictionary state.
Here is the Cinch base class that does all this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Reflection;
using System.Diagnostics;
namespace Cinch
{
public abstract partial class EditableValidatingViewModelBase :
ValidatingViewModelBase, IEditableObject
{
#region Data
private Dictionary<string, object> _savedState;
#endregion
#region Public/Protected Methods
public void BeginEdit()
{
OnBeginEdit();
_savedState = GetFieldValues();
}
protected virtual void OnBeginEdit()
{
}
public void CancelEdit()
{
OnCancelEdit();
RestoreFieldValues(_savedState);
_savedState = null;
}
protected virtual void OnCancelEdit()
{
}
public void EndEdit()
{
OnEndEdit();
_savedState = null;
}
protected virtual void OnEndEdit()
{
}
protected virtual Dictionary<string, object> GetFieldValues()
{
return GetType().GetProperties(BindingFlags.Public |
BindingFlags.NonPublic | BindingFlags.Instance)
.Where(pi => pi.CanRead && pi.GetIndexParameters().Length == 0)
.Select(pi => new { Key = pi.Name, Value = pi.GetValue(this, null) })
.ToDictionary(k => k.Key, k => k.Value);
}
protected virtual void RestoreFieldValues(Dictionary<string, object> fieldValues)
{
foreach (PropertyInfo pi in GetType().GetProperties(
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
.Where(pi => pi.CanWrite && pi.GetIndexParameters().Length == 0) )
{
object value;
if (fieldValues.TryGetValue(pi.Name, out value))
pi.SetValue(this, value, null);
else
{
Debug.WriteLine("Failed to restore property " +
pi.Name + " from cloned values, property not found in Dictionary.");
}
}
}
#endregion
}
}
So all you have to do to get editability support is inherit your UI model objects from Cinch.EditableValidatingViewModelBase
. Job done.
Let's see how you might put an object that inherits from Cinch.EditableValidatingViewModelBase
into Edit mode.
From the ViewModel, we can simply do this.BeginEdit()
, it's that easy. However, what you must also do if you have nested Cinch.DataWrapper<T>
objects
is make sure they too are put into the correct state. You would do this in your UI ViewModel class as follows, where we simply override the protected virtual void OnBeginEdit()
we get from inheriting from Cinch.EditableValidatingViewModelBase
.
Where we may have a UI ViewModel object that looks like:
using System;
using Cinch;
using MVVM.DataAccess;
using System.ComponentModel;
using System.Collections.Generic;
namespace MVVM.ViewModels
{
public class OrderViewModel : Cinch.EditableValidatingViewModelBase
{
#region Data
private Cinch.DataWrapper<Int32> orderId;
private Cinch.DataWrapper<Int32> customerId;
private Cinch.DataWrapper<Int32> productId;
private Cinch.DataWrapper<Int32> quantity;
private Cinch.DataWrapper<DateTime> deliveryDate;
private IEnumerable<DataWrapperBase> cachedListOfDataWrappers;
private static SimpleRule quantityRule;
#endregion
#region Ctor
public OrderViewModel()
{
#region Create DataWrappers
OrderId = new DataWrapper<Int32>(this, orderIdChangeArgs);
CustomerId = new DataWrapper<Int32>(this, customerIdChangeArgs);
ProductId = new DataWrapper<Int32>(this, productIdChangeArgs);
Quantity = new DataWrapper<Int32>(this, quantityChangeArgs);
DeliveryDate = new DataWrapper<DateTime>(this, deliveryDateChangeArgs);
cachedListOfDataWrappers =
DataWrapperHelper.GetWrapperProperties<OrderViewModel>(this);
#endregion
#region Create Validation Rules
quantity.AddRule(quantityRule);
#endregion
DeliveryDate.DataValue = DateTime.Now;
}
static OrderModel()
{
quantityRule = new SimpleRule("DataValue", "Quantity can not be < 0",
(Object domainObject)=>
{
DataWrapper<Int32> obj = (DataWrapper<Int32>)domainObject;
return obj.DataValue <= 0;
});
}
#endregion
#region Public Properties
static PropertyChangedEventArgs orderIdChangeArgs =
ObservableHelper.CreateArgs<OrderViewModel>(x => x.OrderId);
public Cinch.DataWrapper<Int32> OrderId
{
get { return orderId; }
private set
{
orderId = value;
NotifyPropertyChanged(orderIdChangeArgs);
}
}
static PropertyChangedEventArgs customerIdChangeArgs =
ObservableHelper.CreateArgs<OrderViewModel>(x => x.CustomerId);
public Cinch.DataWrapper<Int32> CustomerId
{
get { return customerId; }
private set
{
customerId = value;
NotifyPropertyChanged(customerIdChangeArgs);
}
}
static PropertyChangedEventArgs productIdChangeArgs =
ObservableHelper.CreateArgs<OrderViewModel>(x => x.ProductId);
public Cinch.DataWrapper<Int32> ProductId
{
get { return productId; }
private set
{
productId = value;
NotifyPropertyChanged(productIdChangeArgs);
}
}
static PropertyChangedEventArgs quantityChangeArgs =
ObservableHelper.CreateArgs<OrderViewModel>(x => x.Quantity);
public Cinch.DataWrapper<Int32> Quantity
{
get { return quantity; }
private set
{
quantity = value;
NotifyPropertyChanged(quantityChangeArgs);
}
}
static PropertyChangedEventArgs deliveryDateChangeArgs =
ObservableHelper.CreateArgs<OrderViewModel>(x => x.DeliveryDate);
public Cinch.DataWrapper<DateTime> DeliveryDate
{
get { return deliveryDate; }
private set
{
deliveryDate = value;
NotifyPropertyChanged(deliveryDateChangeArgs);
}
}
public IEnumerable<DataWrapperBase> CachedListOfDataWrappers
{
get { return cachedListOfDataWrappers; }
}
#endregion
#region Overrides
public override bool IsValid
{
get
{
return base.IsValid &&
DataWrapperHelper.AllValid(cachedListOfDataWrappers);
}
}
#endregion
#region EditableValidatingViewModelBase overrides
protected override void OnBeginEdit()
{
base.OnBeginEdit();
DataWrapperHelper.SetBeginEdit(cachedListOfDataWrappers);
}
protected override void OnEndEdit()
{
base.OnEndEdit();
DataWrapperHelper.SetEndEdit(cachedListOfDataWrappers);
}
protected override void OnCancelEdit()
{
base.OnCancelEdit();
DataWrapperHelper.SetCancelEdit(cachedListOfDataWrappers);
}
#endregion
}
}
What we need to do is override the Cinch.EditableValidatingViewModelBase
virtual method as follows:
#region EditableValidatingViewModelBase overrides
protected override void OnBeginEdit()
{
base.OnBeginEdit();
DataWrapperHelper.SetBeginEdit(cachedListOfDataWrappers);
}
protected override void OnEndEdit()
{
base.OnEndEdit();
DataWrapperHelper.SetEndEdit(cachedListOfDataWrappers);
}
protected override void OnCancelEdit()
{
base.OnCancelEdit();
DataWrapperHelper.SetCancelEdit(cachedListOfDataWrappers);
}
#endregion
Where the Cinch framework provides a static helper called DataWrapperHelper
, which you must use to set the correct
edit state of the nested DataWrapper<T>
objects; you can use these helper methods:
DataWrapperHelper.SetBeginEdit(IEnumerable<DataWrapperBase> wrapperProperties)
DataWrapperHelper.SetEndEdit(IEnumerable<DataWrapperBase> wrapperProperties)
DataWrapperHelper.SetCancelEdit(IEnumerable<DataWrapperBase> wrapperProperties)
Where IEnumerable<DataWrapperBase> wrapperProperties
is actually the cachedListOfDataWrappers
which was obtained during the object construction.
You do not have to worry about this. Cinch will do this for you, providing you do the right thing in the UI ViewModel class. If you are feeling a little lost by all this,
do not worry, help is at hand. You can read all about creating ViewModels using Cinch using this
article link: CinchIV.aspx#DevelopingVMs.
WeakEvent Creation
Before I start talking about how to create WeakEvents, I think this may be a good place to start a small discussion. I imagine there are loads of readers/.NET developers
that think Events in .NET are cool. Well, me too, I love events. The thing is, how many of you think you need to worry much about Garbage Collection and when dealing with events,
.NET manages its own memory via the GC right? Well yeah, it does, but Events are one area that are, shall we say, a little gray in .NET.
In the diagram above, there is an object ("eventExposer
") that declares an event ("SpecialEvent
"). Then, a form
is created ("myForm
") that adds a handler to the event. The form is closed and the expectation is that the form will be released to garbage collection,
but it isn't. Unfortunately, the underlying delegate of the event still maintains a strong reference to the form because the form's handler wasn't removed.
The image and text are borrowed from
http://diditwith.net/2007/03/23/SolvingTheProblemWithEventsWeakEventHandlers.aspx.
In typical applications, it is possible that handlers that are attached to event sources will not be destroyed in coordination with the listener object that attached
the handler to the source. This situation can lead to memory leaks. Windows Presentation Foundation (WPF) introduces a particular design pattern that can be used to address this issue,
by providing a dedicated manager class for particular events and implementing an interface on listeners for that event. This design pattern is known as the WeakEvent pattern.
MSDN: http://msdn.microsoft.com/en-us/library/aa970850.aspx
Now if you have ever looked into the Weak Event manager / interface implementation, you will realise it is quite a lot of work and you must have
a new WeakEventManager
per Event type. This to me sounds like too much work, so I would prefer some other mechanisms, such as have a WeakEvent
in the beginning.
Better still, maybe have a weak listener that only reacts to the source event if the source of the event is still alive and has not been GC'd.
Raise a WeakEvent<T>
So without further ado, let me show you some handy little helpers that are available within Cinch when dealing with Events, and possibly making them Weak.
It is still obviously better to Add/Remove the delegates for an event manually where possible, but sometimes you just do not know the lifecyles of the objects involved,
so it is preferable to opt for a WeakEvent strategy.
Firstly, let's use the absolutely brilliant WeakEvent<T>
from the very, very talented Daniel Grunwald, who published a
superb article on WeakEvents some time ago. Daniel's WeakEvent<T>
shows how you can raise an event in a weak manner.
I am not going to bore you with all the code for WeakEvent<T>
but one thing that you should probably get familiar with, if you are not already,
is the WeakReference
class. This is a standard .NET class which references an object while still allowing that object to be reclaimed by garbage collection.
Pretty much any WeakEvent subscription/raising of event will use an internal WeakReference
class to allow the source of the event or the subscriber to be GC'd.
Anyway, to use Daniel Grunwald's WeakEvent<T>
, we can do the following:
Declaring the WeakEvent<T>
private readonly WeakEvent<EventHandler<EventArgs>>
dependencyChangedEvent =
new WeakEvent<EventHandler<EventArgs>>();
public event EventHandler<EventArgs> DependencyChanged
{
add { dependencyChangedEvent.Add(value); }
remove { dependencyChangedEvent.Remove(value); }
}
Raising the WeakEvent<T>
dependencyChangedEvent.Raise(this, new EventArgs());
Listening to WeakEvent<T>
SourceDependency.DependencyChanged += OnSourceChanged;
...
private void OnSourceChanged(object sender, EventArgs e)
{
}
That is how you could make a WeakEvent<T>
, but sometimes it is not your own code and you are not in charge of the Events contained in the code.
Perhaps you are using a third party control set. In that case, you may need to use a WeakEvent subscription. Cinch provides two methods of doing this.
WeakEvent Subscription
Above we saw how to raise a WeakEvent using Daniel Grunwald's WeakEvent<T>
. How about in the case where we want to subscribe to an existing event?
Again, this is typically achieved using a WeakReference
class to check the WeakReference.Target
for null. If the value is null, the source of the event
has been garbage collected so do not fire the invocation list delegate; if it is not null, the source of the event is alive so call the invocation list delegate which subscribed to the event.
Cinch provides two methods to do this.
WeakEventProxy
Which is a neat little class which Paul Stovell wrote some time ago. The entire class looks like this:
using System;
namespace Cinch
{
public class WeakEventProxy<TEventArgs> : IDisposable
where TEventArgs : EventArgs
{
#region Data
private WeakReference callbackReference;
private readonly object syncRoot = new object();
#endregion
#region Ctor
public WeakEventProxy(EventHandler<TEventArgs> callback)
{
callbackReference = new WeakReference(callback, true);
}
#endregion
#region Public Methods
public void Handler(object sender, TEventArgs e)
{
EventHandler<TEventArgs> callback;
lock (syncRoot)
{
callback = callbackReference == null ? null :
callbackReference.Target as EventHandler<TEventArgs>;
}
if (callback != null)
{
callback(sender, e);
}
}
public void Dispose()
{
lock (syncRoot)
{
GC.SuppressFinalize(this);
if (callbackReference != null)
{
callbackReference.Target = null;
}
callbackReference = null;
}
}
#endregion
}
}
And to use this, we can simply do the following:
Declare Event Handlers like
private EventHandler<NotifyCollectionChangedEventArgs>
collectionChangeHandler;
private WeakEventProxy<NotifyCollectionChangedEventArgs>
weakCollectionChangeListener;
And wire up the event subscription delegate like
if (weakCollectionChangeListener == null)
{
collectionChangeHandler = OnCollectionChanged;
weakCollectionChangeListener =
new WeakEventProxy<NotifyCollectionChangedEventArgs>(
collectionChangeHandler);
}
ncc.CollectionChanged += weakCollectionChangeListener.Handler;
private void OnCollectionChanged(object sender,
NotifyCollectionChangedEventArgs e)
{
}
WeakEvent Subscriber With Auto Unsubscription
I was trawling the internet one day and found this superb article on WeakEvents:
http://diditwith.net/PermaLink,guid,aacdb8ae-7baa-4423-a953-c18c1c7940ab.aspx.
This link contained some cool code that I have used within Cinch, which not only allows users to create WeakEvent subscriptions, but allows the user to specify
an auto unsubscribe callback delegate. In addition, using a small variation to this code, it is possible to make all subscribed event handlers weak. Let's have a quick look
at the syntax for both these operations.
Specifying a WeakEvent Subscription With Unhook
We simply do this:
workspace.CloseWorkSpace +=
new EventHandler<EventArgs>(OnCloseWorkSpace).
MakeWeak(eh => workspace.CloseWorkSpace -= eh);
private void OnCloseWorkSpace(object sender, EventArgs e)
{
}
That one line creates a Weak listener with an auto unsubscribe. Neat huh?
I mentioned that you can also use this code to create a WeakEvent, such that all subscribers to a particular event would be Weak. This is how you could do that using this code:
public class EventProvider
{
private EventHandler<EventArgs> closeWorkSpace;
public event EventHandler<EventArgs> CloseWorkSpace
{
add
{
closeWorkSpace += value.MakeWeak(eh => closeWorkSpace -= eh);
}
remove
{
}
}
}
As I say, I can not take much credit for this code, it came from the link specified, but I do think it's very handy. We actually use it in production code without too many issues.
The only thing I have noticed is that it doesn't play well with the CollectionChanged
of ObservableCollection<T>
, but then I just use
the WeakEventProxy
that I also mentioned above that is part of Cinch, and that works just fine.
Mediator Messaging
Now I do not know about you, but generally when I work with the MVVM framework, I do not have a single ViewModel managing the whole shooting match. I actually have a number
of them (in fact, we have loads). One thing that is an issue using the standard MVVM pattern is cross ViewModel communication. After all, the ViewModels that form
an application may all be disparate unconnected objects that know nothing about each other. However, they need to know about certain actions that a user performs. Here is a concrete example.
Say you have two Views, one with customers and one with orders for a customer. Let's say the Orders view was using a OrdersViewModel
and that the Customers view was using
a CustomersViewModel
, and when a Customers order is updated, deleted, or added to that, the Customer view should show some sort of visual trigger to alert the user that some
order detail of the customer has changed.
Sounds simple enough, right? However, we have two independent Views run by two independent ViewModels, with no link, but clearly, there needs to be some sort of connection
from the OrdersViewModel
to the CustomersViewModel
, some sort of messaging perhaps.
This is exactly what the Mediator pattern is all about, it is a simple light weight messaging system. I wrote about this some time ago on
my blog, which in turn got made a ton better by Josh Smith /
Marlon Grech (as an atomic pair) who came up with the Mediator implementation you will see in Cinch.
So how does the Mediator work?
This diagram may help:
The idea is a simple one, the Mediator listens for incoming messages, sees who is interested in a particular message, and calls each of those that are subscribed
against a given message. The messages are usually strings.
Basically, what happens is that there is a single instance of the Mediator
sitting somewhere (usually exposed as a static property on the ViewModelBase
class)
that is waiting for objects to subscribe to it either using:
- An entire object reference. Then any
Mediator
message methods that have been marked up with the MediatorMessageSinkAttribute
attribute
will be located on the registered object (using Reflection) and will have a callback delegate automatically created.
- An actual Lambda callback delegate.
In either case, the Mediator
maintains a list of WeakAction
s callback delegates. Where each WeakAction
is a delegate which uses
an internal WeakReference
class to check the WeakReference.Target
for null, before calling back the delegate. This caters for the fact that
the target of the callback delegate may no longer be alive as it may have been Garbage Collected. Any instance of WeakAction
s callback delegates that point
to objects that are no longer alive are removed from the list of Mediator
WeakAction
callback delegates.
When a callback delegate is obtained, either the original callback delegate is called or the Mediator
message methods that have been marked up with
the MediatorMessageSinkAttribute
attribute will be called.
Here is an example of how to use the Mediator in all the different possible manners.
Registering for Messages
Using an explicit callback delegate (this is not my proffered option though).
We simply create the correct type of delegate and Register a callback for a message notification with the Mediator
.
public delegate void DummyDelegate(Boolean dummy);
...
Mediator.Instance.Register("AddCustomerMessage", new DummyDelegate((x) =>
{
AddCustomerCommand.Execute(null);
}));
Register an entire object, and use the MediatorMessageSinkAttribute attribute
This is my favourite approach and is the simplest approach in my opinion. You just need to register an entire object with the Mediator
and attribute
up some message hook methods.
Within Cinch V1, I would automatically register any ViewModel that inherited from ViewModelBase
for you. Now for
Cinch V2, I decided against this, as you may have many ViewModels that do not ever require the Mediator
at all, so you
have to manually register with the Mediator yourself in your ViewModel, which is done as follows:
Mediator.Instance.Register(this);
Any method that is marked up with the MediatorMessageSinkAttribute
attribute will be located on the registered object (using Reflection)
and will have a callback delegate automatically created. Here is an example:
[MediatorMessageSink("AddCustomerMessage"))]
private void AddCustomerMessageSink(Boolean dummy)
{
AddCustomerCommand.Execute(null);
}
So how about notification of messages?
Message Notification
That is very easy to do. We simply use the Mediator.Instance.NotifyCollegues()
method as follows:
Mediator.Instance.NotifyColleagues<Boolean>("AddCustomerMessage", true);
You can also use the Mediator asynchronously as follows:
Mediator.Instance.NotifyColleaguesAsync<Boolean>("AddCustomerMessage", true);
Choice of Model Bases
In Cinch V1, I started off telling people to expose a CurrentXXXModel
off their ViewModel
and bind to that. Several people started to complain that they could not edit the model. Now for me, the UI stack was always something like the one directly below:
DB -> LINQ to SQL/EF -> Server Side Model -> WCF -> UI Model -> UI ViewModel -> View
where the UI was always in charge of its own Model. In fact, that is what the Cinch
V1 demo app shows people, using LINQ to EF. Where there is no sharing of Model objects between the server and client, each side has its own types. So one is
able to expose a current Model (where the Model is a Cinch based Model).
Cinch provides several Model base classes that you can use, these are as follows:
ValidatingObject
: Provides support for DataWrapper
s and INotifyPropertyChanged
implementation, and support for Validation rules
via an IDataErrorInfo
implementation.
EditableValidatingObject
: Provides editable object support via an IEditableObject
implementation.
If this describes your situation, you may want to read this section of an older Cinch V1
article: CinchIV.aspx#DevelopingModels.
Important note: As I effectively changed my mind to match what most people wanted (below), the Cinch
code generator will output code where the code produced is expecting to inherit from a Cinch
ViewModel, so be warned, if you go down the exposed CurrentXXXModel
off your ViewModel, the code generator will not help you at all.
Complaints Complaints Complaints
That said, a lot of people stated that their stack looked more like this:
DB -> LINQ to SQL/EF (shared) -> Server Side Model -> WCF -> LINQ to SQL/EF (shared) -> UI ViewModel -> View
So they could not modify their model to have it inherit from a Cinch
V1 Model class. This is fair enough; in fact, I have done a 360 degree turn around on my thinking, and no longer advocate exposing
a CurrentXXXModel
off a ViewModel, but rather pass things from the View into the ViewModel and then into the Model. As such, all Validation Rules (IDataErrorInfo
)
/ ViewMode
changes / IEditableObject
operations should be made against a ViewModel that supports these operations. I discuss these options in the next section.
In fact, there is as entire Cinch V1 article dedicated to discussing the whole exposed Model/ViewModel debate,
which you can find at: CinchIV.aspx.
Choice of ViewModel Bases
As I just stated, Cinch does actually allow you to either expose a CurrentXXXModel
off your ViewModel
which supports DataWrapper
s/Validation Rules (IDataErrorInfo
) / ViewMode changes / IEditableObject
operations,
by use of the two Cinch Model classes mentioned above.
But as I also stated above, I no longer recommend that approach, and think that the Model should be left alone, and that you should do
all your DataWrapper
s/Validation Ruless (IDataErrorInfo
) / ViewMode changes / IEditableObject
operations in your ViewModel.
As such, Cinch provides several ViewModel base classes that you can use; these are as follows:
ViewModelBase
: Provides support for DataWrapper
s and an INotifyPropertyChanged
implementation
ValidatingViewModelBase
: Provides support for Validation rules via an IDataErrorInfo
implementation
EditableValidatingViewModelBase
: Provides editable object support via an IEditableObject
implementation
As before, please see the Cinch V1 article dedicated to discussing the whole exposed Model/ViewModel debate,
which you can find at: CinchIV.aspx.
And this section will be of particular interest: CinchIV.aspx#DevelopingVMs; you will
of course have to ignore how the services are retrieved, as these are now provided by meffedMVVM.
Unit Testing
Although a few of the services have changed and a couple more have been added, Unit Testing of Cinch
remains largely unchanged from Cinch V1 to Cinch V2.
As such, the initial Cinch V1 Unit Testing article is still totally accurate, and you can read more about this using the following
link: CinchV.aspx.
As for the additional IStatusAware
service that was not part of Cinch
V1, I have provided a test double called TestViewAwareStatus
within Cinch
V2 which should be easy enough to figure out how to use. Just examine the names of the various test methods such as SimulateViewIsLoadedEvent()
, which will
simulate a Loaded
event from an actual View.
Code Generator
Cinch V1 provides a code generator that aids in the development of Cinch
ViewModels. It produces two partial parts to a partial class. More details can be found on the Cinch code generator using
this link: CinchCodeGen.aspx.
The code produced by the generator is perfectly valid for Cinch V1 and Cinch V2.
The article for the code generator is quite interesting I think, as it uses Compiler Services/CodeDOM to make sure the generated code actually works.
It even won two awards, so it is worth a gander I think.
That's It ....For Now
Could I just ask if you have enjoyed this article, and feel it is going to help you out, could you please show your support by leaving a vote/comment?
As before, if you have any deep MEF related questions, you should direct this to Marlon Grech either by using his blog
C# Disciples, or by using the MefedMVVM CodePlex site. Any other
Cinch V2 questions will be answered by the next Cinch V2 articles.