Features
- Desktop and Silverlight CLR compatibility
- Capability to perform assignment and raise appropriate events before and after assignment
- Weak referenced
- Provides for both expression tree and loosely typed strings
- Uses extended
EventArgs
to supply before and after values - Extended
PropertyChangingEventArgs
for cancellable changes - Configurable to use caching of
EventArgs
to decrease heap fragmentation - Comes with unit tests for Desktop and Silverlight CLRs
Introduction
INotifyPropertyChanged
is a ubiquitous part of Silverlight and WPF programming. It is used extensively in WPF and Silverlight to enable non-DependencyObjects
to signal that a bound value is out of date. I’ve seen many approaches that have been used in order to remove the property name string
requirement. Some have employed lambda expressions, or walking the stack, while others have used generated proxies or AOP point cuts. This post and the accompanying code are not so much about ridding us from the loosely typed string
, although I do provide the means if you don’t mind a performance hit. Today, there is another code smell that I would like to address, and it is the use of base classes for property change notification.
In this post, I will demonstrate how we are able to encapsulate two property change interface implementations (INotifyPropertyChanged
and INotifyPropertyChanging
) in a reusable class, and in a weak referencing manner to avoid any possible leakage.
Using a base class implementation for INotifyPropertyChanged
has never sat easy with me. It probably goes back to early 2003 after reading Juval Lowy’s landmark book Programming .NET Components. The principal of favouring aggregation over inheritance is a driver for creating shallow class hierarchies and maintainable components. It is a principal that offers many advantages, and one that I strongly recommend.
Using the Library
DanielVaughan.ComponentModel .PropertyChangeNotifier
is the main class. You use it by creating a field in your owner class, and instantiating it within the owner’s constructor. We apply the boilerplate code, which consists of a ‘flow-through’ interface implementation for either or both INotifyPropertyChanged
and INotifyPropertyChanging
.
An example is shown in the following excerpt.
class MockNotifyPropertyChanged : INotifyPropertyChanged, INotifyPropertyChanging
{
readonly PropertyChangeNotifier notifier;
#region INotifyPropertyChanged Implementation
public event PropertyChangedEventHandler PropertyChanged
{
add
{
notifier.PropertyChanged += value;
}
remove
{
notifier.PropertyChanged -= value;
}
}
#endregion
#region INotifyPropertyChanging Implementation
public event PropertyChangingEventHandler PropertyChanging
{
add
{
notifier.PropertyChanging += value;
}
remove
{
notifier.PropertyChanging -= value;
}
}
#endregion
public MockNotifyPropertyChanged()
{
notifier = new PropertyChangeNotifier(this);
}
int int1;
public int TestPropertyAssigned
{
get
{
return int1;
}
set
{
notifier.Assign(
"TestPropertyAssigned", ref int1, value);
}
}
string string1;
public string TestPropertyAssignedLambda
{
get
{
return string1;
}
set
{
notifier.Assign(
(MockNotifyPropertyChanged mock) => mock.TestPropertyAssignedLambda,
ref string1, value);
}
}
}
The two property examples shown, delegate the task of assigning the property to the PropertyChangeNotifier
. This provides the PropertyChangeNotifier
with the opportunity to raise the PropertyChanging
event of the INotifyPropertyChanging
interface, perform the assignment (or return if a handler called Cancel()
on the EventArgs
, then raise the PropertyChangedEvent
from the INotifyPropertyChanged
interface.
Implementation
The following excerpt is taken from the PropertyChangeNotifier
class. It contains both Silverlight and Desktop CLR specific sections.
#if !SILVERLIGHT
[Serializable]
#endif
public sealed class PropertyChangeNotifier : INotifyPropertyChanged, INotifyPropertyChanging
{
readonly WeakReference ownerWeakReference;
internal object Owner
{
get
{
if (ownerWeakReference.Target != null)
{
return ownerWeakReference.Target;
}
return null;
}
}
public PropertyChangeNotifier(object owner) : this(owner, true)
{
}
public PropertyChangeNotifier(object owner, bool useExtendedEventArgs)
{
ArgumentValidator.AssertNotNull(owner, "owner");
ownerWeakReference = new WeakReference(owner);
this.useExtendedEventArgs = useExtendedEventArgs;
}
#region event PropertyChanged
#if !SILVERLIGHT
[field: NonSerialized]
#endif
event PropertyChangedEventHandler propertyChanged;
public event PropertyChangedEventHandler PropertyChanged
{
add
{
if (OwnerDisposed)
{
return;
}
propertyChanged += value;
}
remove
{
if (OwnerDisposed)
{
return;
}
propertyChanged -= value;
}
}
void OnPropertyChanged(PropertyChangedEventArgs e)
{
var owner = ownerWeakReference.Target;
if (owner != null && propertyChanged != null)
{
propertyChanged(owner, e);
}
}
#endregion
public void Assign<TProperty>(
string propertyName, ref TProperty property, TProperty newValue)
{
if (OwnerDisposed)
{
return;
}
ArgumentValidator.AssertNotNullOrEmpty(propertyName, "propertyName");
ValidatePropertyName(propertyName);
AssignWithNotificationAux(propertyName, ref property, newValue);
}
public void Assign<T, TProperty>(
Expression<Func<T, TProperty>> expression, ref TProperty property, TProperty newValue)
{
if (OwnerDisposed)
{
return;
}
string propertyName = GetPropertyName(expression);
AssignWithNotificationAux(propertyName, ref property, newValue);
}
void AssignWithNotificationAux<TProperty>(
string propertyName, ref TProperty property, TProperty newValue)
{
if (Equals(property, newValue))
{
return;
}
if (useExtendedEventArgs)
{
var args = new PropertyChangingEventArgs<TProperty>(propertyName, property, newValue);
OnPropertyChanging(args);
if (args.Cancelled)
{
return;
}
var oldValue = property;
property = newValue;
OnPropertyChanged(new PropertyChangedEventArgs<TProperty>(
propertyName, oldValue, newValue));
}
else
{
var args = RetrieveOrCreatePropertyChangingEventArgs(propertyName);
OnPropertyChanging(args);
var changedArgs = RetrieveOrCreatePropertyChangedEventArgs(propertyName);
OnPropertyChanged(changedArgs);
}
}
readonly Dictionary<string, string> expressions = new Dictionary<string, string>();
public void NotifyChanged<TProperty>(
string propertyName, TProperty oldValue, TProperty newValue)
{
if (OwnerDisposed)
{
return;
}
ArgumentValidator.AssertNotNullOrEmpty(propertyName, "propertyName");
ValidatePropertyName(propertyName);
if (ReferenceEquals(oldValue, newValue))
{
return;
}
var args = useExtendedEventArgs
? new PropertyChangedEventArgs<TProperty>(propertyName, oldValue, newValue)
: RetrieveOrCreatePropertyChangedEventArgs(propertyName);
OnPropertyChanged(args);
}
public void NotifyChanged<T, TResult>(
Expression<Func<T, TResult>> expression, TResult oldValue, TResult newValue)
{
if (OwnerDisposed)
{
return;
}
ArgumentValidator.AssertNotNull(expression, "expression");
string name = GetPropertyName(expression);
NotifyChanged(name, oldValue, newValue);
}
static MemberInfo GetMemberInfo<T, TResult>(Expression<Func<T, TResult>> expression)
{
var member = expression.Body as MemberExpression;
if (member != null)
{
return member.Member;
}
throw new ArgumentException("MemberExpression expected.", "expression");
}
#region INotifyPropertyChanging Implementation
#if !SILVERLIGHT
[field: NonSerialized]
#endif
event PropertyChangingEventHandler propertyChanging;
public event PropertyChangingEventHandler PropertyChanging
{
add
{
if (OwnerDisposed)
{
return;
}
propertyChanging += value;
}
remove
{
if (OwnerDisposed)
{
return;
}
propertyChanging -= value;
}
}
void OnPropertyChanging(PropertyChangingEventArgs e)
{
var owner = ownerWeakReference.Target;
if (owner != null && propertyChanging != null)
{
propertyChanging(owner, e);
}
}
#endregion
#if SILVERLIGHT
readonly object expressionsLock = new object();
string GetPropertyName<T, TResult>(Expression<Func<T, TResult>> expression)
{
string name;
lock (expressionsLock)
{
if (!expressions.TryGetValue(expression.ToString(), out name))
{
if (!expressions.TryGetValue(expression.ToString(), out name))
{
var memberInfo = GetMemberInfo(expression);
if (memberInfo == null)
{
throw new InvalidOperationException("MemberInfo not found.");
}
name = memberInfo.Name;
expressions.Add(expression.ToString(), name);
}
}
}
return name;
}
#else
readonly ReaderWriterLockSlim expressionsLock = new ReaderWriterLockSlim();
string GetPropertyName<T, TResult>(Expression<Func<T, TResult>> expression)
{
string name;
expressionsLock.EnterUpgradeableReadLock();
try
{
if (!expressions.TryGetValue(expression.ToString(), out name))
{
expressionsLock.EnterWriteLock();
try
{
if (!expressions.TryGetValue(expression.ToString(), out name))
{
var memberInfo = GetMemberInfo(expression);
if (memberInfo == null)
{
throw new InvalidOperationException("MemberInfo not found.");
}
name = memberInfo.Name;
expressions.Add(expression.ToString(), name);
}
}
finally
{
expressionsLock.ExitWriteLock();
}
}
}
finally
{
expressionsLock.ExitUpgradeableReadLock();
}
return name;
}
#endif
bool cleanupOccured;
bool OwnerDisposed
{
get
{
if (cleanupOccured)
{
return true;
}
var owner = Owner;
if (owner != null)
{
return false;
}
cleanupOccured = true;
var changedSubscribers = propertyChanged.GetInvocationList();
foreach (var subscriber in changedSubscribers)
{
propertyChanged -= (PropertyChangedEventHandler)subscriber;
}
var changingSubscribers = propertyChanging.GetInvocationList();
foreach (var subscriber in changingSubscribers)
{
propertyChanging -= (PropertyChangingEventHandler)subscriber;
}
propertyChanged = null;
propertyChanging = null;
propertyChangedEventArgsCache.Clear();
propertyChangingEventArgsCache.Clear();
return true;
}
}
[Conditional("DEBUG")]
void ValidatePropertyName(string propertyName)
{
#if !SILVERLIGHT
var propertyDescriptor = TypeDescriptor.GetProperties(Owner)[propertyName];
if (propertyDescriptor == null)
{
throw new Exception(string.Format(
"The property '{0}' does not exist.", propertyName));
}
#endif
}
bool useExtendedEventArgs;
readonly Dictionary<string, PropertyChangedEventArgs>
propertyChangedEventArgsCache = new Dictionary<string, PropertyChangedEventArgs>();
readonly Dictionary<string, PropertyChangingEventArgs>
propertyChangingEventArgsCache = new Dictionary<string, PropertyChangingEventArgs>();
#if SILVERLIGHT
readonly object propertyChangingEventArgsCacheLock = new object();
PropertyChangingEventArgs RetrieveOrCreatePropertyChangingEventArgs(string propertyName)
{
var result = RetrieveOrCreateEventArgs(
propertyName,
propertyChangingEventArgsCacheLock,
propertyChangingEventArgsCache,
x => new PropertyChangingEventArgs(x));
return result;
}
readonly object propertyChangedEventArgsCacheLock = new object();
PropertyChangedEventArgs RetrieveOrCreatePropertyChangedEventArgs(string propertyName)
{
var result = RetrieveOrCreateEventArgs(
propertyName,
propertyChangedEventArgsCacheLock,
propertyChangedEventArgsCache,
x => new PropertyChangedEventArgs(x));
return result;
}
static TArgs RetrieveOrCreateEventArgs<TArgs>(
string propertyName, object cacheLock, Dictionary<string, TArgs> argsCache,
Func<string, TArgs> createFunc)
{
ArgumentValidator.AssertNotNull(propertyName, "propertyName");
TArgs result;
lock (cacheLock)
{
if (argsCache.TryGetValue(propertyName, out result))
{
return result;
}
result = createFunc(propertyName);
argsCache[propertyName] = result;
}
return result;
}
#else
readonly ReaderWriterLockSlim propertyChangedEventArgsCacheLock = new ReaderWriterLockSlim();
PropertyChangedEventArgs RetrieveOrCreatePropertyChangedEventArgs(string propertyName)
{
ArgumentValidator.AssertNotNull(propertyName, "propertyName");
var result = RetrieveOrCreateArgs(
propertyName,
propertyChangedEventArgsCache,
propertyChangedEventArgsCacheLock,
x => new PropertyChangedEventArgs(x));
return result;
}
readonly ReaderWriterLockSlim propertyChangingEventArgsCacheLock = new ReaderWriterLockSlim();
static TArgs RetrieveOrCreateArgs<TArgs>(string propertyName, Dictionary<string, TArgs> argsCache,
ReaderWriterLockSlim lockSlim, Func<string, TArgs> createFunc)
{
ArgumentValidator.AssertNotNull(propertyName, "propertyName");
TArgs result;
lockSlim.EnterUpgradeableReadLock();
try
{
if (argsCache.TryGetValue(propertyName, out result))
{
return result;
}
lockSlim.EnterWriteLock();
try
{
if (argsCache.TryGetValue(propertyName, out result))
{
return result;
}
result = createFunc(propertyName);
argsCache[propertyName] = result;
return result;
}
finally
{
lockSlim.ExitWriteLock();
}
}
finally
{
lockSlim.ExitUpgradeableReadLock();
}
}
PropertyChangingEventArgs RetrieveOrCreatePropertyChangingEventArgs(string propertyName)
{
ArgumentValidator.AssertNotNull(propertyName, "propertyName");
var result = RetrieveOrCreateArgs(
propertyName,
propertyChangingEventArgsCache,
propertyChangingEventArgsCacheLock,
x => new PropertyChangingEventArgs(x));
return result;
}
#endif
}
I’ve extended the PropertyChangedEventArgs
and the PropertyChangingEventArgs
to include before and after values. The following excerpt shows the PropertyChangedEventArgs
.
public sealed class PropertyChangedEventArgs<TProperty> : PropertyChangedEventArgs
{
public TProperty OldValue { get; private set; }
public TProperty NewValue { get; private set; }
internal PropertyChangedEventArgs(
string propertyName, TProperty oldValue, TProperty newValue)
: base(propertyName)
{
OldValue = oldValue;
NewValue = newValue;
}
}
INotifyPropertyChanging
doesn’t exist in Silverlight, so I’ve implemented it.
In order to turn of the extended EventArgs
, pass an extra argument to the constructor. By turning of the extended EventArgs
, we enable to arg caching feature. I implemented this after reading Josh Smith’s nice articles on the subject.
The PropertyChangeNotifier
retains a link to the Owner
using a WeakReference
. Each time a change is handled, the PropertyChangeNotifier
checks to see if the Owner
is still alive. If it isn’t, the PropertyChangeNotifier
will remove all event handlers.
Unit Tests
The download includes various unit tests for both the Desktop and Silverlight environments. I recommend examining them, to further your understanding about how it all works.
Figure: Desktop CLR test results.
Figure: Silverlight CLR test results.
Future Enhancements
- Batch support
- Event Suppression