Contents
Cinch Article Series Links
Introduction
Last time I simply introduced the Cinch framework, and this time, we would do a walkthrough of Cinch and its internals. This is going to be a large article, so it has actually been split over two articles. In this article, I will be covering the following:
- Allows view to communicate LifeCycle events to a ViewModel without any hard reference links being maintained, and no
IView
interface requirements. There is no link at all between the View and the ViewModel. - Has several attached behaviours for common tasks such as:
- Numeric text entry
- Run an
ICommand
in a ViewModel based on a RoutedEvent
from a XAML FrameworkElement
- Have a group of such
ICommand
/RoutedEvent
Events for a single XAML FrameworkElement
- Allows the ViewModel to determine if a Model's data should be editable; the UI simply updates via bindings, based on the ViewModel driven editability state. This is available at individual Model field level, so it's very flexible.
- Delegate validation rules which allow validation rules to be as granular as necessary.
- Native
IDataErrorInfo
support using the Delegate rules approach. IEditableObject
usage to store/restore object state on edit / cancel edit.- Weak event creation, to allow the creation of
WeakEvent
s. - Weak event subscription, which also allows auto unsubscriptions.
- Mediator messaging with WeakReference support out of the box.
So I guess the only way to do this is to just start, so let's get going, shall we? But before we do that, I just need to repeat the special thanks section, with one addition, Paul Stovell who I forgot to include last time.
Prerequisites
The demo app makes use of:
- VS2008 SP1
- .NET 3.5 SP1
- SQL Server (see the README.txt in the MVVM.DataAccess project to learn what you have to setup for the demo app database)
Special Thanks
Before I start, I would specifically like to say a massive thanks to the following people, without whom this article and the subsequent series of articles would never have been possible. Basically, what I have done with Cinch is studied most of these guys, seen what's hot, what's not, and come up with Cinch, which I hope addresses some new ground not covered in other frameworks.
- Mark Smith (Julmar Technology), for his excellent MVVM Helper Library, which has helped me enormously. Mark, I know, I asked your permission to use some of your code, which you most kindly gave, but I just wanted to say a massive thanks for your cool ideas, some of which I genuinely had not thought of. I take my hat off to you mate.
- Josh Smith / Marlon Grech (as an atomic pair) for their excellent Mediator implementation. You boys rock, always a pleasure.
- Karl Shifflett / Jaime Rodriguez (Microsoft boys) for their excellent MVVM Lob tour, which I attended, well done lads.
- Bill Kempf, for just being Bill and being a crazy wizard like programmer, who also has a great MVVM framework called Onyx, which I wrote an article about some time ago. Bill always has the answers to tough questions, cheers Bill.
- Paul Stovell for his excellent delegate validation idea, which Cinch uses for validation of business objects.
All of the WPF Disciples, for being the best online group to belong to, IMHO.
Thanks guys/girl, you know who you are.
Cinch Internals I
This section will start the dive into the internals of Cinch. As I say, there is far too much for one article, so I am splitting the internals of Cinch over two articles. Hopefully there will be something of use to you here. Well, I hope so anyway.
View Lifecycle Events
When developing with the MVVM pattern, the holy grail is for there to be no coupling at all between the View and the ViewModel. This clean separation between the View and ViewModel is achieved in Cinch, there is no link between them at all, which is ace. However, occasionally it is advantageous for the ViewModel to know something about certain View events, such as:
Loaded
Activated
Deactivated
Close
But we just said that there was no referencing between View/ViewModel and we would like to know about these View events in the ViewModel, so how do we achieve that? The answer lies in Attached Behaviours. What we do is we have a ICommand
based property in the ViewModel and we use an attached View lifecycle behaviour and attach the correct View event to the correct ViewModel ICommand
. So when the View event is raised, it will actually call a ICommand
implementation in the ViewModel.
Cinch provides for this using two things:
- A Cinch ViewModel base class called
ViewModelBase
, which already contains all the View lifecycle ICommand
implementation you will need, which, I should point out, can be overridden in the inheritors of the Cinch ViewModelBase
class - Some View lifecycle attached behaviours
Let us examine one View lifecycle event, Activated
, to see how it works; the others are the same.
Starting with the ViewModelBase
classm we can see from the code below (extra code removed for clarity) that there is an ICommand
called Activated
where the trimmed Cinch ViewModelBase
looks like below.
Cinch also provides public bindable (INPC) properties for each of the View lifecycle events (be careful, UserControl
does not have Activated
/Deactivated
, these are only available on Window
. These properties are set in the Cinch ViewModelBase
, so overriders be aware, if you fail to call the Cinch ViewModelBase
in your overrides of the lifecycle methods, these properties will not work. Anyway, here is a full example for the Activated
command, the rest work the same.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Configuration;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Linq.Expressions;
namespace Cinch
{
public abstract class ViewModelBase : INotifyPropertyChanged,
IDisposable, IParentablePropertyExposer
{
private SimpleCommand activatedCommand;
private Boolean isActivated=false;
public ViewModelBase() : this(new UnityProvider())
{
}
public ViewModelBase(IIOCProvider iocProvider)
{
activatedCommand= new SimpleCommand
{
CanExecuteDelegate = x => true,
ExecuteDelegate = x => OnWindowActivated()
};
}
protected virtual void OnWindowActivated()
{
IsActivated= true;
}
public SimpleCommand ActivatedCommand
{
get { return activatedCommand ; }
}
static PropertyChangedEventArgs isactivatedChangeArgs =
ObservableHelper.CreateArgs<ViewModelBase>(x => x.IsActivated);
public Boolean IsActivated
{
get { return isActivated; }
private set
{
isActivated = value;
NotifyPropertyChanged(isactivatedChangeArgs );
}
}
}
}
The eagle eyed amongst you may notice that there is a property called ActivatedCommand
within the code above, which actually returns a SimpleCommand
and not a ICommand
that you may have been expecting and maybe even seen in various other posts. This is all cool, don't worry, the XAML parser and binding system are clever enough to know that any class that implements ICommand
can be used in place of an ICommand
in a binding. The reason to expose the ICommand
as a SimpleCommand
is that the SimpleCommand
has an extra property that can be used from inside Unit tests. Namely, the CommandSucceeded
which is set to true
on Command execution completion, so can be used in Unit test Assert
statements.
Here is the SimpleCommand
code:
using System;
using System.Windows.Input;
namespace Cinch
{
public class SimpleCommand : ICommand
{
#region Public Properties
public Boolean CommandSucceeded { get; set; }
public Predicate<object> CanExecuteDelegate { get; set; }
public Action<object> ExecuteDelegate { get; set; }
#endregion
#region ICommand Members
public bool CanExecute(object parameter)
{
if (CanExecuteDelegate != null)
return CanExecuteDelegate(parameter);
return true;
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
if (ExecuteDelegate != null)
{
ExecuteDelegate(parameter);
CommandSucceeded = true;
}
}
#endregion
}
}
OK, so we now have a ViewModel that has a SimpleCommand
for Activated
that will call a protected virtual void OnWindowActivated()
method, but how does this Activated
SimpleCommand
get executed? Well, recall I said there was some attached behaviour magic, so let's look at that next.
First, this is how the View would wire up the View Activated
attached behavior:
<Window x:Class="MVVM.Demo.Views.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Cinch="clr-namespace:Cinch;assembly=Cinch"
Title="Window1" Height="300" Width="300">
Cinch:LifetimeEvent.Activated="{Binding ActivatedCommand}"
....
....
</Window>
So this will then use the attached View lifecycle event, which is shown below.
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
namespace Cinch
{
public static class LifetimeEvent
{
#region Activated
public static readonly DependencyProperty ActivatedProperty =
DependencyProperty.RegisterAttached("Activated",
typeof(ICommand), typeof(LifetimeEvent),
new UIPropertyMetadata(null, OnActivatedEventInfoChanged));
public static ICommand GetActivated(DependencyObject source)
{
return (ICommand)source.GetValue(ActivatedProperty);
}
public static void SetActivated(DependencyObject source, ICommand command)
{
source.SetValue(ActivatedProperty, command);
}
private static void OnActivatedEventInfoChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
var win = sender as Window;
if (win != null)
{
win.Activated -= OnWindowActivated;
if (e.NewValue != null)
win.Activated += OnWindowActivated;
}
}
private static void OnWindowActivated(object sender, EventArgs e)
{
var dpo = (DependencyObject)sender;
ICommand activatedCommand = GetActivated(dpo);
if (activatedCommand != null)
activatedCommand.Execute(GetCommandParameter(dpo));
}
#endregion
}
}
The same mechanism is used for:
- View
Deactivated
event, where the SimpleCommand
in the Cinch ViewModelBase
is called DeactivatedCommand
, and the virtual method in the Cinch ViewModelBase
is called OnWindowDeactivated()
- View
Closing/Closed
event, where the SimpleCommand
in the Cinch ViewModelBase
is called Closeommand
, and the virtual method in the Cinch ViewModelBase
is called OnWindowClose()
- View
Loaded
event, where the SimpleCommand
in the Cinch ViewModelBase
is called LoadedCommand
, and the virtual method in the Cinch ViewModelBase
is called OnWindowLoaded()
Again, don't forget there is no standard implementation provided by the Cinch ViewModelBase
as this functionality should be performed by overriding these methods in a ViewModel which inherits from the Cinch ViewModelBase
.
So all you have to do is inherit from the Cinch ViewModelBase
, and you can use these View lifecycle events/properties, just remember to call the base methods.
Numeric Textbox Attached Behaviour
One very common requirement when developing LOB applications is to have numeric only data entry on textboxes. Whilst this is possible to achieve using Regular Expressions and some code-behind and possibly a ValidationRule
, wouldn't it just be easier to not let the textbox accept anything apart from numbers in the first place? To this end, Cinch contains a NumericTextBoxBehavior
attached behaviour which looks like below. You should also note that this behaviour caters for pasted text using the DataObject
pasting event.
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System;
using System.Linq;
using System.Windows.Controls;
using System.Text.RegularExpressions;
namespace Cinch
{
public static class NumericTextBoxBehavior
{
#region IsEnabled DP
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached("IsEnabled",
typeof(bool), typeof(NumericTextBoxBehavior),
new UIPropertyMetadata(false, OnEnabledStateChanged));
public static bool GetIsEnabled(DependencyObject source)
{
return (bool)source.GetValue(IsEnabledProperty);
}
public static void SetIsEnabled(DependencyObject source, bool value)
{
source.SetValue(IsEnabledProperty, value);
}
private static void OnEnabledStateChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
TextBox tb = sender as TextBox;
if (tb == null)
return;
tb.PreviewTextInput -= tbb_PreviewTextInput;
DataObject.RemovePastingHandler(tb, OnClipboardPaste);
bool b = ((e.NewValue != null && e.NewValue.GetType() == typeof(bool))) ?
(bool)e.NewValue : false;
if (b)
{
tb.PreviewTextInput += tbb_PreviewTextInput;
DataObject.AddPastingHandler(tb, OnClipboardPaste);
}
}
#endregion
#region Private Methods
private static void OnClipboardPaste(object sender, DataObjectPastingEventArgs e)
{
TextBox tb = sender as TextBox;
string text = e.SourceDataObject.GetData(e.FormatToApply) as string;
if (tb != null && !string.IsNullOrEmpty(text) && !Validate(tb, text))
e.CancelCommand();
}
static void tbb_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
TextBox tb = sender as TextBox;
if (tb != null && !Validate(tb, e.Text))
e.Handled = true;
}
#endregion
private static bool Validate(TextBox tb, string newContent)
{
string testString = string.Empty;
if (!string.IsNullOrEmpty(tb.SelectedText))
{
string pre = tb.Text.Substring(0, tb.SelectionStart);
string after = tb.Text.Substring(tb.SelectionStart + tb.SelectionLength,
tb.Text.Length - (tb.SelectionStart + tb.SelectionLength));
testString = pre + newContent + after;
}
else
{
string pre = tb.Text.Substring(0, tb.CaretIndex);
string after = tb.Text.Substring(tb.CaretIndex,
tb.Text.Length - tb.CaretIndex);
testString = pre + newContent + after;
}
Regex regExpr = new Regex(@"^([-+]?)(\d*)([,.]?)(\d*)$");
if (regExpr.IsMatch(testString))
return true;
return false;
}
}
}
Which can easily be applied to a textbox in your application as follows:
<Window x:Class="MVVM.Demo.Views.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Cinch="clr-namespace:Cinch;assembly=Cinch"
Title="Window1" Height="300" Width="300">
<Grid>
<TextBox Cinch:NumericTextBoxBehavior.IsEnabled="True" />
</Grid>
</Window>
With this attached behaviour enabled, only numeric data entry will be allowed for the textbox that has the NumericTextBoxBehavior
attached behaviour applied.
Attached Command Behaviour
Continuing with the attached behaviors, it is also sometimes very convenient to be able to fire a ViewModel ICommand
from a FrameworkElement RoutedEvent
. This is something that the WPF framework does not do out of the box, though Blend 3 does allow this by the use of the Blend Interactivity DLL. If you want an example of that, have a look at my blog post WPF: Blend 3 Interactions / Behaviours.
But we are where we are, and I want people to know how to do this without using a non released DLL from a different product that will more than likely find its way into WPF proper at some stage. So let's continue with how to do it without the Blend Interactivity DLL.
So here is how, Cinch actually provides two alternatives here, attaching a single ICommand
to a single FrameworkElement RoutedEvent
, or attaching a collection of ICommand
s to a FrameworkElement
using different RoutedEvent
s.
Let's start simple, and build up from there.
Attaching a single ICommand to a single FrameworkElement RoutedEvent
This is easily done via yet another attached property or two, which you can set on a View FrameworkElement
like this:
<Window x:Class="MVVM.Demo.Views.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Cinch="clr-namespace:Cinch;assembly=Cinch"
Title="Window1" Height="300" Width="300">
<Grid Background="WhiteSmoke"
Cinch:SingleEventCommand.RoutedEventName="MouseDown"
Cinch:SingleEventCommand.TheCommandToRun=
"{Binding Path=ShowWindowCommand}"/>
</Window>
So we set the RoutedEvent
to fire the ICommand
and we set the ViewModel ICommand
to execute via a Binding.
So all that is left is to look at how this is achieved, which is done with a standard DP or two and a touch of Reflection to get the RoutedEvent
from the DependencyObject
that declares the RoutedEventName
attached DP, and from there, we just create a Delegate
which is called when the event is raised, which in turn fires the ICommand
that was specified using the TheCommandToRun
DP.
Here is the code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Input;
using System.Reflection;
using System.Windows.Media;
namespace Cinch
{
#region SCommandArgs Class
public class SCommandArgs
{
#region Data
public object Sender { get; set; }
public object EventArgs { get; set; }
public object CommandParameter { get; set; }
#endregion
#region Ctor
public SCommandArgs()
{
}
public SCommandArgs(object sender, object eventArgs,
object commandParameter)
{
Sender = sender;
EventArgs = eventArgs;
CommandParameter = commandParameter;
}
#endregion
}
#endregion
#region SingleEventCommand Class
public static class SingleEventCommand
{
#region TheCommandToRun
public static readonly DependencyProperty TheCommandToRunProperty =
DependencyProperty.RegisterAttached("TheCommandToRun",
typeof(ICommand),
typeof(SingleEventCommand),
new FrameworkPropertyMetadata((ICommand)null));
public static ICommand GetTheCommandToRun(DependencyObject d)
{
return (ICommand)d.GetValue(TheCommandToRunProperty);
}
public static void SetTheCommandToRun(DependencyObject d, ICommand value)
{
d.SetValue(TheCommandToRunProperty, value);
}
#endregion
#region RoutedEventName
public static readonly DependencyProperty RoutedEventNameProperty =
DependencyProperty.RegisterAttached("RoutedEventName", typeof(String),
typeof(SingleEventCommand),
new FrameworkPropertyMetadata((String)String.Empty,
new PropertyChangedCallback(OnRoutedEventNameChanged)));
public static String GetRoutedEventName(DependencyObject d)
{
return (String)d.GetValue(RoutedEventNameProperty);
}
public static void SetRoutedEventName(DependencyObject d, String value)
{
d.SetValue(RoutedEventNameProperty, value);
}
private static void OnRoutedEventNameChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
String routedEvent = (String)e.NewValue;
if (d == null || String.IsNullOrEmpty(routedEvent))
return;
EventHooker eventHooker = new EventHooker();
eventHooker.ObjectWithAttachedCommand = d;
EventInfo eventInfo = d.GetType().GetEvent(routedEvent,
BindingFlags.Public | BindingFlags.Instance);
if (eventInfo != null)
{
eventInfo.RemoveEventHandler(d,
eventHooker.GetNewEventHandlerToRunCommand(eventInfo));
eventInfo.AddEventHandler(d,
eventHooker.GetNewEventHandlerToRunCommand(eventInfo));
}
}
#endregion
#region CommandParameter
public static readonly DependencyProperty CommandParameterProperty =
DependencyProperty.RegisterAttached("CommandParameter", typeof(object),
typeof(SingleEventCommand), new UIPropertyMetadata(null));
public static object GetCommandParameter(DependencyObject obj)
{
return (object)obj.GetValue(CommandParameterProperty);
}
public static void SetCommandParameter(DependencyObject obj, object value)
{
obj.SetValue(CommandParameterProperty, value);
}
#endregion
}
#endregion
#region EventHooker Class
sealed class EventHooker
{
#region Public Methods/Properties
public DependencyObject ObjectWithAttachedCommand { get; set; }
public Delegate GetNewEventHandlerToRunCommand(EventInfo eventInfo)
{
Delegate del = null;
if (eventInfo == null)
throw new ArgumentNullException("eventInfo");
if (eventInfo.EventHandlerType == null)
throw new ArgumentException("EventHandlerType is null");
if (del == null)
del = Delegate.CreateDelegate(eventInfo.EventHandlerType, this,
GetType().GetMethod("OnEventRaised",
BindingFlags.NonPublic |
BindingFlags.Instance));
return del;
}
#endregion
#region Private Methods
private void OnEventRaised(object sender, EventArgs e)
{
ICommand command = (ICommand)(sender as DependencyObject).
GetValue(SingleEventCommand.TheCommandToRunProperty);
object commandParameter = (sender as DependencyObject).
GetValue(SingleEventCommand.CommandParameterProperty);
SCommandArgs commandArgs = new SCommandArgs(sender, e, commandParameter);
if (command != null)
command.Execute(commandArgs);
}
#endregion
}
#endregion
}
This allows a single FrameworkElement RoutedEvent
to fire a single ViewModel ICommand
and pass down the original EventArgs
/Sender
and CommandParamtere
down to the ViewModel all within the SCommandArgs
object. So that means, all you have to do within your ViewModel to use the original EventArgs
is something like this:
someCommand= new SimpleCommand
{
CanExecuteDelegate = x => true,
ExecuteDelegate = x => ExecuteSomeCommand(x)
};
private void ExecuteSomeCommand(Object o)
{
Cinch.SCommandArgs data =(Cinch.SCommandArgs)o;
}
But Cinch does more than that. So let's look at the more advanced scenario.
Attaching a collection of ICommand(s) to a FrameworkElement using different RoutedEvent(s)
As before, let us see how you would use the attached collection of ICommand
s/RoutedEvent
s from a View. Here is how you might do that:
<Window x:Class="MVVM.Demo.Views.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Cinch="clr-namespace:Cinch;assembly=Cinch"
Title="Window1" Height="300" Width="300">
<Grid Background="WhiteSmoke">
<Behaviors:EventCommander.Mappings>
<Behaviors:CommandEvent
Command="{Binding MouseEnterCommand}"
Event="MouseEnter" />
<Behaviors:CommandEvent
Command="{Binding MouseLeaveCommand}"
Event="MouseLeave" />
</Behaviors:EventCommander.Mappings>
</Grid>
</Window>
So you can see that this time, there is an attached property (EventCommander.Mappings
) that is expecting a collection of CommandEvent
objects. The concept behind this is largely the same, except this time, there is a collection of these CommandEvent
objects, but they largely work the same as just described above, so I will concentrate on how the EventCommander.Mappings
collection works, and assume the discussion above was enough to explain a single CommandEvent
object.
The EventCommander.Mappings
collection looks like this:
public static class EventCommander
{
#region InternalMappings DP
internal static readonly DependencyProperty MappingsProperty =
DependencyProperty.RegisterAttached("InternalMappings",
typeof(CommandEventCollection), typeof(EventCommander),
new UIPropertyMetadata(null, OnMappingsChanged));
internal static CommandEventCollection InternalGetMappingCollection(
DependencyObject obj)
{
var map = obj.GetValue(MappingsProperty) as CommandEventCollection;
if (map == null)
{
map = new CommandEventCollection();
SetMappings(obj, map);
}
return map;
}
public static IList GetMappings(DependencyObject obj)
{
return InternalGetMappingCollection(obj);
}
public static void SetMappings(DependencyObject obj,
CommandEventCollection value)
{
obj.SetValue(MappingsProperty, value);
}
private static void OnMappingsChanged(DependencyObject target,
DependencyPropertyChangedEventArgs e)
{
if (e.OldValue != null)
{
CommandEventCollection cec = e.OldValue as CommandEventCollection;
if (cec != null)
cec.Unsubscribe(target);
}
if (e.NewValue != null)
{
CommandEventCollection cec = e.NewValue as CommandEventCollection;
if (cec != null)
cec.Subscribe(target);
}
}
#endregion
}
Where it can be seen that behind the scenes, the EventCommander.Mappings
collection is populating a CommandEventCollection
. The CommandEventCollection
inherits from Freezable
, as this is a trick you can use to get DataContext
inheritance. Basically, what happens is that by inheriting from Freezable
, a non-UI element will also get the current UI element's DataContext
(which is more than likely the ViewModel). If you did not inherit from Freezable
, the CommandEventCollection
would not be able to pick up the ViewModels bound ICommand
; as it would know nothing about a current DataContext
object, it would be null. It's a trick, but it works. If you want to know more, read Mike Hillberg's blog entry: Mike Hillberg's Freezable blog post, and maybe have a look at Josh Smith's DataContextSpy post, which is also very useful.
Anyway, the CommandEventCollection
looks like this, where its main job is to maintain the list of current CommandEvent
s:
public class CommandEventCollection : FreezableCollection<CommandEvent>
{
#region Data
private object _target;
private readonly List<CommandEvent> _currentList = new List<CommandEvent>();
#endregion
#region Ctor
public CommandEventCollection()
{
((INotifyCollectionChanged)this).CollectionChanged +=
OnCollectionChanged;
}
#endregion
#region Private/Internal Methods
internal void Subscribe(object target)
{
_target = target;
foreach(var item in this)
item.Subscribe(target);
}
internal void Unsubscribe(object target)
{
foreach (var item in this)
item.Unsubscribe(target);
_target = null;
}
private void OnCollectionChanged(object sender,
NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var item in e.NewItems)
OnItemAdded((CommandEvent)item);
break;
case NotifyCollectionChangedAction.Remove:
foreach (var item in e.OldItems)
OnItemRemoved((CommandEvent)item);
break;
case NotifyCollectionChangedAction.Replace:
foreach (var item in e.OldItems)
OnItemRemoved((CommandEvent)item);
foreach (var item in e.NewItems)
OnItemAdded((CommandEvent)item);
break;
case NotifyCollectionChangedAction.Move:
break;
case NotifyCollectionChangedAction.Reset:
_currentList.ForEach(i => i.Unsubscribe(_target));
_currentList.Clear();
foreach (var item in this)
OnItemAdded(item);
break;
default:
return;
}
}
private void OnItemAdded(CommandEvent item)
{
if (item != null && _target != null)
{
_currentList.Add(item);
item.Subscribe(_target);
}
}
private void OnItemRemoved(CommandEvent item)
{
if (item != null && _target != null)
{
_currentList.Remove(item);
item.Unsubscribe(_target);
}
}
#endregion
}
As before, to use the original EventArgs
in the ViewModel ICommand
, we just need to do something like this in the ViewModel:
someCommand= new SimpleCommand
{
CanExecuteDelegate = x => true,
ExecuteDelegate = x => ExecuteSomeCommand(x)
};
private void ExecuteSomeCommand(Object o)
{
Cinch.EventParameters data =(Cinch.EventParameters)o;
}
Better INPC, No Magic Strings
When working with WPF, it is very common to have your ViewModel/Model classes implement the System.ComponentModel.INotifyPropertyChanged
interface for binding notification of property changes. This is typically implemented like this:
using System.ComponentModel;
namespace SDKSample
{
public class Person : INotifyPropertyChanged
{
private string name;
public event PropertyChangedEventHandler PropertyChanged;
public Person()
{
}
public Person(string value)
{
this.name = value;
}
public string PersonName
{
get { return name; }
set
{
name = value;
OnPropertyChanged("PersonName");
}
}
protected void OnPropertyChanged(string name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
}
The problem with this approach is that the code has magic strings in it which are easy to get wrong. Have a look at the OnPropertyChanged("PersonName");
code shown above. I had originally been using some static Reflection by using LINQ expression trees to obtain the property name. I was using Bill Kempf's excellent Reflect code. The problem with the static reflection Expression tree thing is that it is slow to create the eventArgs
each time. One of the readers of this article actually came up with a better solution where the INPC EventArgs
are created once and then re-used. I liked this idea a lot, so have now included it in Cinch. The reader's post is available here if you want some more information about this: CinchII.aspx?msg=3141144#xx3141144xx.
Cinch now uses this approach. Here is how it works. There is a static helper class which looks like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
namespace Cinch
{
public static class ObservableHelper
{
#region Public Methods
public static PropertyChangedEventArgs CreateArgs<T>(
Expression<Func<T, Object>> propertyExpression)
{
return new PropertyChangedEventArgs(
GetPropertyName<T>(propertyExpression));
}
public static string GetPropertyName<T>(
Expression<Func<T, Object>> propertyExpression)
{
var lambda = propertyExpression as LambdaExpression;
MemberExpression memberExpression;
if (lambda.Body is UnaryExpression)
{
var unaryExpression = lambda.Body as UnaryExpression;
memberExpression = unaryExpression.Operand as MemberExpression;
}
else
{
memberExpression = lambda.Body as MemberExpression;
}
var propertyInfo = memberExpression.Member as PropertyInfo;
return propertyInfo.Name;
}
#endregion
}
}
Which we can then use like this from any property:
static PropertyChangedEventArgs currentCustomerChangeArgs =
ObservableHelper.CreateArgs<AddEditOrderViewModel>(x => x.CurrentCustomer);
public CustomerModel CurrentCustomer
{
get { return currentCustomer; }
set
{
currentCustomer = value;
NotifyPropertyChanged(currentCustomerChangeArgs);
}
}
No more magic strings, at all. Most excellent.
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 Model. 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 ViewModel can access these wrappers as they are public properties, on either UI Model classes, or actually public properties in the ViewModel (I expose a CurrentX
object of my ViewModel, but others repeat all the UI Model properties in the ViewModel; me, I have no issues with writing straight from the View to the Model, providing it doesn't get to the database if its InValid
), 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 UI Model classes (or directly on the ViewModel if you want to), like this (don't worry about the inheriting from Cinch.EditableValidatingObject
, we'll get to that soon):
public class OrderModel : Cinch.EditableValidatingObject
{
private Cinch.DataWrapper<Int32> quantity;
public OrderModel()
{
Quantity = new DataWrapper<int32>(this, quantityChangeArgs);
....
....
}
static PropertyChangedEventArgs quantityChangeArgs =
ObservableHelper.CreateArgs<OrderModel>(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 the DataValue
can be changed whenever you like though. The other thing to note is that the Model/ViewModel actually use 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.Models
{
public class OrderModel : Cinch.EditableValidatingObject
{
#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 OrderModel()
{
#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<OrderModel>(this);
#endregion
}
#endregion
}
}
And 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}"/>
So that's all cool, but how do these DataWrapper<T>
objects respond to a change in mode state? Well, that is quite simply 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 is we are trying to change the state for (for me, this is always a UI Model, for others this may 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.WorkspaceViewModel
{
private ViewMode currentViewMode = ViewMode.AddMode;
private OrderModel currentCustomerOrder;
public AddEditOrderViewModel()
{
}
static PropertyChangedEventArgs currentViewModeChangeArgs =
ObservableHelper.CreateArgs<AddEditOrderViewModel>(x => x.CurrentViewMode);
public ViewMode CurrentViewMode
{
get { return currentViewMode; }
set
{
currentViewMode = value;
switch (currentViewMode)
{
case ViewMode.AddMode:
CurrentCustomerOrder = new OrderModel();
this.DisplayName = "Add Order";
break;
case ViewMode.EditMode:
CurrentCustomerOrder.BeginEdit();
this.DisplayName = "Edit Order";
break;
case ViewMode.ViewOnlyMode:
this.DisplayName = "View Order";
break;
}
DataWrapperHelper.SetMode(
CurrentCustomer.CachedListOfDataWrappers,
currentViewMode);
NotifyPropertyChanged(currentViewModeChangeArgs);
}
}
static PropertyChangedEventArgs currentCustomerOrderChangeArgs =
ObservableHelper.CreateArgs<AddEditOrderViewModel>(x => x.CurrentCustomerOrder);
public OrderModel CurrentCustomerOrder
{
get { return currentCustomerOrder; }
set
{
currentCustomerOrder = value;
if (currentCustomerOrder != null)
{
if (currentCustomerOrder.ProductId.DataValue > 0)
{
ProductModel prod = this.Products.Where(p => p.ProductId ==
currentCustomerOrder.ProductId.DataValue).Single();
productsCV.MoveCurrentTo(prod);
}
}
NotifyPropertyChanged(currentCustomerOrderChangeArgs);
}
}
....
....
....
}
}
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 object has 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 ruleRegexRule
, 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 OrderModel : Cinch.ValidatingObject
{
private Int32 quantity;
private static SimpleRule quantityRule;
public OrderModel()
{
#region Create Validation Rules
quantity.AddRule(quantityRule);
#endregion
}
static OrderModel()
{
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 Model 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 OrderModel : Cinch.ValidatingObject
{
#region Data
private Cinch.DataWrapper<Int32> customerId;
private static SimpleRule quantityRule;
#endregion
#region Ctor
public OrderModel()
{
....
....
#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<OrderModel>(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.ValidatingObject
object. Let's say you have something like this for a UI Model object:
using System;
using Cinch;
using MVVM.DataAccess;
using System.ComponentModel;
using System.Collections.Generic;
namespace MVVM.Models
{
public class OrderModel : Cinch.ValidatingObject
{
#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 OrderModel()
{
#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<OrderModel>(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<OrderModel>(x => x.OrderId);
public Cinch.DataWrapper<Int32> OrderId
{
get { return orderId; }
private set
{
orderId = value;
NotifyPropertyChanged(orderIdChangeArgs);
}
}
static PropertyChangedEventArgs customerIdChangeArgs =
ObservableHelper.CreateArgs<OrderModel>(x => x.CustomerId);
public Cinch.DataWrapper<Int32> CustomerId
{
get { return customerId; }
private set
{
customerId = value;
NotifyPropertyChanged(customerIdChangeArgs);
}
}
static PropertyChangedEventArgs productIdChangeArgs =
ObservableHelper.CreateArgs<OrderModel>(x => x.ProductId);
public Cinch.DataWrapper<Int32> ProductId
{
get { return productId; }
private set
{
productId = value;
NotifyPropertyChanged(productIdChangeArgs);
}
}
static PropertyChangedEventArgs quantityChangeArgs =
ObservableHelper.CreateArgs<OrderModel>(x => x.Quantity);
public Cinch.DataWrapper<Int32> Quantity
{
get { return quantity; }
private set
{
quantity = value;
NotifyPropertyChanged(quantityChangeArgs);
}
}
static PropertyChangedEventArgs deliveryDateChangeArgs =
ObservableHelper.CreateArgs<OrderModel>(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.EditableValidatingObject
which in turn inherits from Cinch.ValidatingObject
, so they already have the IDataErrorInfo
implementation, so it's 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 a Model's individual field 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 allowed for was the storage of an object's state to a Memento backing object which had the exact same properties as the business object it was storing state for. So when you started an edit on a business object, you store the current state in a Memento and did your edit. If you cancelled the edit, the business object's state would be restored from the Memento. This does work very well, but Microsoft also support this via an interface called IEditableObject
which looks like this:
BeginEdit()
CancelEdit()
EndEdit()
So 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 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.
This diagram shows this:
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 EditableValidatingObject :
ValidatingObject, 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.EditableValidatingObject
. Job done.
Let's see how you might put an object that inherits from Cinch.EditableValidatingObject
into Edit mode.
Well, from the ViewModel, we can simply do this.CurrentCustomer.BeginEdit()
; it's that easy. However, what you must also do is if you have any nested Cinch.DataWrapper<T>
objects, make sure they are put into the correct state. You would do this in your UI Model class as follows, where we simply override the protected virtual void OnBeginEdit()
we get from inheriting from Cinch.EditableValidatingObject
.
Where we may have a UI Model object that looks like:
using System;
using Cinch;
using MVVM.DataAccess;
using System.ComponentModel;
using System.Collections.Generic;
namespace MVVM.Models
{
public class OrderModel : Cinch.EditableValidatingObject
{
#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 OrderModel()
{
#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<OrderModel>(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<OrderModel>(x => x.OrderId);
public Cinch.DataWrapper<Int32> OrderId
{
get { return orderId; }
private set
{
orderId = value;
NotifyPropertyChanged(orderIdChangeArgs);
}
}
static PropertyChangedEventArgs customerIdChangeArgs =
ObservableHelper.CreateArgs<OrderModel>(x => x.CustomerId);
public Cinch.DataWrapper<Int32> CustomerId
{
get { return customerId; }
private set
{
customerId = value;
NotifyPropertyChanged(customerIdChangeArgs);
}
}
static PropertyChangedEventArgs productIdChangeArgs =
ObservableHelper.CreateArgs<OrderModel>(x => x.ProductId);
public Cinch.DataWrapper<Int32> ProductId
{
get { return productId; }
private set
{
productId = value;
NotifyPropertyChanged(productIdChangeArgs);
}
}
static PropertyChangedEventArgs quantityChangeArgs =
ObservableHelper.CreateArgs<OrderModel>(x => x.Quantity);
public Cinch.DataWrapper<Int32> Quantity
{
get { return quantity; }
private set
{
quantity = value;
NotifyPropertyChanged(quantityChangeArgs);
}
}
static PropertyChangedEventArgs deliveryDateChangeArgs =
ObservableHelper.CreateArgs<OrderModel>(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 Static Methods
public static OrderModel OrderToOrderModel(Order order)
{
OrderModel orderModel = new OrderModel();
orderModel.OrderId.DataValue = order.OrderId;
orderModel.CustomerId.DataValue = order.CustomerId;
orderModel.Quantity.DataValue = order.Quantity;
orderModel.ProductId.DataValue = order.ProductId;
orderModel.DeliveryDate.DataValue = order.DeliveryDate;
return orderModel;
}
#endregion
#region EditableValidatingObject 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.EditableValidatingObject
virtual methods as follows:
#region EditableValidatingObject 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 the 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 Model class. If you are feeling a little lost by all this, do not worry: one of the subsequent articles will walk through how to create a UI Model using Cinch. This article is more about Cinch internals for those that may be interested.
WeakEvent Creation
Before I start talking about how to create WeakEvent
s, 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 attachs 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 lifecycles 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)
{
}
So 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. So 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>
, so 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 Un-subscription
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 links 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 created 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 performed. 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 customer's 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 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 the WeakAction
's callback delegates that point to objects that are no longer alive are removed from the list of the Mediator
WeakAction
's 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 preferred 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.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.
So to register, this is done for you in Cinch, so you don't have to do anything. Simply inherit from ViewModelBase
, job done. If you are wondering how this is done, the ViewModelBase
class in Cinch simply registers itself with the Mediator
like this:
Mediator.Register(this);
So 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.NotifyCollegues()
method as follows:
Mediator.NotifyColleagues<Boolean>("AddCustomerMessage", true);
You can also use the Mediator asynchronously as follows:
Mediator.NotifyColleaguesAsync<Boolean>("AddCustomerMessage", true);
At present, the Cinch ViewModelBase
class registers the ViewModel instance with the Mediator, within the ViewModel constructor, and unregisters the ViewModel instance with the Mediator within the Dispose()
method.
So any object that subscribes to this message will now be called back by the list of the Mediator
WeakAction
's callback delegates.
What's Coming Up?
In the subsequent articles, I will be showcasing it roughly like this:
- A walkthrough of Cinch, and its internals: II
- How to develop ViewModels using Cinch
- How to Unit test ViewModels using a Cinch app, including how to test background worker threads which may run within Cinch ViewModels
- A demo app using Cinch
That's It, Hope You Liked It
That is actually all I wanted to say right now, but I hope from this article you can see where Cinch is going and how it could help you with MVVM. As we continue our journey, we will be covering the remaining items within Cinch, and then we will move on to see how to develop an app with Cinch.
Thanks
As always, votes / comments are welcome.
History
- 19/07/2009: Initial release
- 25/07/2009:
- Altered
NumericTextBoxBehavior
to take into account pasted data - Changed
INotifyPropertyChanged
to use static LINQ Expression Trees
- 02/08/2009: Changed
INotifyPropertyChanged
to use Phil's (a reader's) idea, which you can read more about here: CinchII.aspx?msg=3141144#xx3141144xx - 15/08/2009: Added support for
DataWrapper
s to notify parent when an internal DataWrapper
property changes - 12/09/2009: Adding caching support to
DataWrapper
and fixed RegExRule
- 05/12/09: Added new code sections to show users how to add validation rules using the new validation methods within Cinch
- 24/12/09: Updated code snippet for
NumericTextBoxBehavior
- 07/05/09: Updated article to show new
ViewModelBase
view lifecycle properties and updated DataWrapper
code, and also Mediator usage was updated