#region File and License Information
/*
<File>
<Copyright>Copyright © 2009, Daniel Vaughan. All rights reserved.</Copyright>
<License>
This file is part of DanielVaughan's base library
DanielVaughan's base library is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
DanielVaughan's base library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with DanielVaughan's base library. If not, see http://www.gnu.org/licenses/.
</License>
<Owner Name="Daniel Vaughan" Email="dbvaughan@gmail.com"/>
<CreationDate>2010-04-22 17:26:58Z</CreationDate>
</File>
*/
#endregion
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Reflection;
using DanielVaughan.ComponentModel;
using DanielVaughan.Concurrency;
namespace DanielVaughan.Mtvt.ComponentModel
{
/// <summary>
/// Indicates the result of a property assignment.
/// </summary>
public enum PropertyAssignmentResult
{
Success, Cancelled, AlreadyAssigned, OwnerDisposed
}
/// <summary>
/// This class provides an implementation of the <see cref="INotifyPropertyChanged"/>
/// and <see cref="INotifyPropertyChanging"/> interfaces.
/// Extended <see cref="PropertyChangedEventArgs"/> and <see cref="PropertyChangingEventArgs"/>
/// are used to provides the old value and new value for the property.
/// <seealso cref="PropertyChangedEventArgs{TProperty}"/>
/// <seealso cref="PropertyChangingEventArgs{TProperty}"/>
/// </summary>
[Serializable]
public sealed class PropertyChangeNotifier : INotifyPropertyChanged, INotifyPropertyChanging
{
readonly WeakReference ownerWeakReference;
readonly DelegateManager changedEventManager;
readonly DelegateManager changingEventManager;
/// <summary>
/// Gets the owner for testing purposes.
/// </summary>
/// <value>The owner.</value>
internal object Owner
{
get
{
if (ownerWeakReference.Target != null)
{
return ownerWeakReference.Target;
}
return null;
}
}
/// <summary>
/// Initializes a new instance
/// of the <see cref="PropertyChangeNotifier"/> class.
/// </summary>
/// <param name="owner">The intended sender
/// of the <code>PropertyChanged</code> event.</param>
public PropertyChangeNotifier(object owner)
: this(owner, true)
{
/* Intentionally left blank. */
}
/// <summary>
/// Initializes a new instance
/// of the <see cref="PropertyChangeNotifier"/> class.
/// </summary>
/// <param name="owner">The intended sender
/// <param name="useExtendedEventArgs">If <c>true</c> the
/// generic <see cref="PropertyChangedEventArgs{TProperty}"/>
/// and <see cref="PropertyChangingEventArgs{TProperty}"/>
/// are used when raising events.
/// Otherwise, the non-generic types are used, and they are cached
/// to decrease heap fragmentation.</param>
/// of the <code>PropertyChanged</code> event.</param>
public PropertyChangeNotifier(object owner, bool useExtendedEventArgs)
: this(owner, useExtendedEventArgs, true)
{
/* Intentionally left blank. */
}
/// <summary>
/// Initializes a new instance
/// of the <see cref="PropertyChangeNotifier"/> class.
/// </summary>
/// <param name="owner">The intended sender
/// <param name="useExtendedEventArgs">If <c>true</c> the
/// generic <see cref="PropertyChangedEventArgs{TProperty}"/>
/// and <see cref="PropertyChangingEventArgs{TProperty}"/>
/// are used when raising events.
/// Otherwise, the non-generic types are used, and they are cached
/// to decrease heap fragmentation.</param>
/// of the <code>PropertyChanged</code> event.</param>
/// <param name="useExtendedEventArgs">If <c>true</c> the
/// generic <see cref="PropertyChangedEventArgs{TProperty}"/>
/// and <see cref="PropertyChangingEventArgs{TProperty}"/>
/// are used when raising events. Otherwise, the non-generic types
/// are used, and they are cached
/// to decrease heap fragmentation.</param>
/// <param name="preserveThreadAffinity">Indicates whether to invoke handlers
/// on the thread that the subscription took place.</param>
public PropertyChangeNotifier(object owner, bool useExtendedEventArgs, bool preserveThreadAffinity)
{
ArgumentValidator.AssertNotNull(owner, "owner");
ownerWeakReference = new WeakReference(owner);
this.useExtendedEventArgs = useExtendedEventArgs;
changedEventManager = new DelegateManager(preserveThreadAffinity);
changingEventManager = new DelegateManager(preserveThreadAffinity);
}
#region event PropertyChanged
/// <summary>
/// Occurs when a property value changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged
{
add
{
if (OwnerDisposed)
{
return;
}
changedEventManager.Add(value);
}
remove
{
if (OwnerDisposed)
{
return;
}
changedEventManager.Remove(value);
}
}
#region Experimental Thread Affinity
public bool MaintainThreadAffinity { get; set; }
#endregion
/// <summary>
/// Raises the <see cref="E:PropertyChanged"/> event.
/// If the owner has been GC'd then the event will not be raised.
/// </summary>
/// <param name="e">The <see cref="System.ComponentModel.PropertyChangedEventArgs"/>
/// instance containing the event data.</param>
void OnPropertyChanged(PropertyChangedEventArgs e)
{
changedEventManager.InvokeDelegates(Owner, e);
}
#endregion
/// <summary>
/// Assigns the specified newValue to the specified property
/// and then notifies listeners that the property has changed.
/// </summary>
/// <typeparam name="TProperty">The type of the property.</typeparam>
/// <param name="propertyName">Name of the property. Can not be null.</param>
/// <param name="property">A reference to the property that is to be assigned.</param>
/// <param name="newValue">The value to assign the property.</param>
/// <exception cref="ArgumentNullException">
/// Occurs if the specified propertyName is <code>null</code>.</exception>
/// <exception cref="ArgumentException">
/// Occurs if the specified propertyName is an empty string.</exception>
public PropertyAssignmentResult Assign<TProperty>(
string propertyName, ref TProperty property, TProperty newValue)
{
if (OwnerDisposed)
{
return PropertyAssignmentResult.OwnerDisposed;
}
ArgumentValidator.AssertNotNullOrEmpty(propertyName, "propertyName");
ValidatePropertyName(propertyName);
return AssignWithNotification(propertyName, ref property, newValue);
}
/// <summary>
/// Slow. Not recommended.
/// Assigns the specified newValue to the specified property
/// and then notifies listeners that the property has changed.
/// Assignment nor notification will occur if the specified
/// property and newValue are equal.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TProperty">The type of the property.</typeparam>
/// <param name="expression">The expression that is used to derive the property name.
/// Should not be <code>null</code>.</param>
/// <param name="property">A reference to the property that is to be assigned.</param>
/// <param name="newValue">The value to assign the property.</param>
/// <exception cref="ArgumentNullException">
/// Occurs if the specified propertyName is <code>null</code>.</exception>
/// <exception cref="ArgumentException">
/// Occurs if the specified propertyName is an empty string.</exception>
public PropertyAssignmentResult Assign<T, TProperty>(
Expression<Func<T, TProperty>> expression, ref TProperty property, TProperty newValue)
{
if (OwnerDisposed)
{
return PropertyAssignmentResult.OwnerDisposed;
}
string propertyName = GetPropertyName(expression);
return AssignWithNotification(propertyName, ref property, newValue);
}
PropertyAssignmentResult AssignWithNotification<TProperty>(
string propertyName, ref TProperty property, TProperty newValue)
{
/* Boxing may occur here. We should consider
* providing some overloads for primitives. */
if (Equals(property, newValue))
{
return PropertyAssignmentResult.AlreadyAssigned;
}
if (useExtendedEventArgs)
{
var args = new PropertyChangingEventArgs<TProperty>(propertyName, property, newValue);
OnPropertyChanging(args);
if (args.Cancelled)
{
return PropertyAssignmentResult.Cancelled;
}
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);
}
return PropertyAssignmentResult.Success;
}
readonly Dictionary<string, string> expressions = new Dictionary<string, string>();
/// <summary>
/// Notifies listeners that the specified property has changed.
/// </summary>
/// <typeparam name="TProperty">The type of the property.</typeparam>
/// <param name="propertyName">Name of the property. Can not be null.</param>
/// <param name="oldValue">The old value before the change occured.</param>
/// <param name="newValue">The new value after the change occured.</param>
/// <exception cref="ArgumentNullException">
/// Occurs if the specified propertyName is <code>null</code>.</exception>
/// <exception cref="ArgumentException">
/// Occurs if the specified propertyName is an empty string.</exception>
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);
}
/// <summary>
/// Slow. Not recommended.
/// Notifies listeners that the property has changed.
/// Notification will occur if the specified
/// property and newValue are equal.
/// </summary>
/// <param name="expression">The expression that is used to derive the property name.
/// Should not be <code>null</code>.</param>
/// <param name="oldValue">The old value of the property before it was changed.</param>
/// <param name="newValue">The new value of the property after it was changed.</param>
/// <exception cref="ArgumentNullException">
/// Occurs if the specified propertyName is <code>null</code>.</exception>
/// <exception cref="ArgumentException">
/// Occurs if the specified propertyName is an empty string.</exception>
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;
}
/* TODO: Make localizable resource. */
throw new ArgumentException("MemberExpression expected.", "expression");
}
#region INotifyPropertyChanging Implementation
public event PropertyChangingEventHandler PropertyChanging
{
add
{
if (OwnerDisposed)
{
return;
}
changingEventManager.Add(value);
}
remove
{
if (OwnerDisposed)
{
return;
}
changingEventManager.Remove(value);
}
}
/// <summary>
/// Raises the <see cref="E:PropertyChanging"/> event.
/// If the owner has been disposed then the event will not be raised.
/// </summary>
/// <param name="e">The <see cref="PropertyChangingEventArgs"/>
/// instance containing the event data.</param>
void OnPropertyChanging(PropertyChangingEventArgs e)
{
changingEventManager.InvokeDelegates(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)
{
/* TODO: Make localizable resource. */
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)
{
/* TODO: Make localizable resource. */
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
{
/* Improve performance here
* by avoiding multiple Owner property calls
* after the Owner has been disposed. */
if (cleanupOccured)
{
return true;
}
var owner = Owner;
if (owner != null)
{
return false;
}
cleanupOccured = true;
return true;
}
}
[Conditional("DEBUG")]
void ValidatePropertyName(string propertyName)
{
#if !SILVERLIGHT
var propertyDescriptor = TypeDescriptor.GetProperties(Owner)[propertyName];
if (propertyDescriptor == null)
{
/* TODO: Make localizable resource. */
throw new Exception(string.Format(
"The property '{0}' does not exist.", propertyName));
}
#endif
}
readonly 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
}
}