SharpCrafters Forum – Thoughts on INotifyPropertyChanged Implementation

SharpCrafters Support Forum

        


Thoughts on INotifyPropertyChanged... Expand / Collapse
Author
Message
Posted 8/18/2009 9:07:54 PM


Community Member
Looking at the INotifyPropertyChanged Implementation, two thoughts came up.
1. No way for the original class to implement INotifyPropertyChanged and fire the event. Common in WPF Model-View-ViewModel. This way the original class can respond to changes of its own properties.
2. The ability to check if the value of the property has changed before setting and firing the on change.

Solutions
1. Added a property to specify the method to call on the object for OnPropertyChanged.
2. Added a property to indicate whether to check equality first (defaults to true).

Code:

[Serializable]
class OnPropertySetSubAspect : OnMethodBoundaryAspect
{
string propertyName;
string onPropertyChanged;
bool checkEqualBeforeSet;

public OnPropertySetSubAspect(string propertyName, NotifyPropertyChangedAttribute parent)
{
this.AspectPriority = parent.AspectPriority;
this.onPropertyChanged = parent.OnPropertyChanged;
this.checkEqualBeforeSet = parent.CheckEqualBeforeSet;
this.propertyName = propertyName;
}

[NonSerialized]
MethodInfo getMethod;
[NonSerialized]
MethodInfo onPropertyChangedMethod;

public override void RuntimeInitialize(MethodBase method)
{
if (!string.IsNullOrEmpty(onPropertyChanged))
onPropertyChangedMethod = method.DeclaringType.GetMethod(onPropertyChanged, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);

if (checkEqualBeforeSet)
getMethod = method.DeclaringType.GetProperty(propertyName).GetGetMethod();
}

public override void OnEntry(MethodExecutionEventArgs eventArgs)
{
if (checkEqualBeforeSet)
{
object oldValue = getMethod.Invoke(eventArgs.Instance, null);
object newValue = eventArgs.GetReadOnlyArgumentArray()[0];
if ((oldValue == null && newValue == null) || (oldValue != null && oldValue.Equals(newValue))) eventArgs.FlowBehavior = FlowBehavior.Return;
}
}

public override void OnSuccess(MethodExecutionEventArgs eventArgs)
{
if (onPropertyChangedMethod == null)
{
NotifyPropertyChangedImplementation implementation = (NotifyPropertyChangedImplementation)((IComposed<INotifyPropertyChanged>)eventArgs.Instance).GetImplementation(eventArgs.InstanceCredentials);
implementation.OnPropertyChanged(propertyName);
}
else
onPropertyChangedMethod.Invoke(eventArgs.Instance, new object[] { propertyName });
}
}

[MulticastAttributeUsage(MulticastTargets.Class | MulticastTargets.Struct)]
[Serializable]
public sealed class NotifyPropertyChangedAttribute : CompoundAspect
{
[NonSerialized]
private int aspectPriority = 0;
[NonSerialized]
private string onPropertyChanged = string.Empty;
[NonSerialized]
private bool checkEqualBeforeSet = true;

public override void ProvideAspects(object targetElement, LaosReflectionAspectCollection collection)
{
Type targetType = (Type)targetElement;
// Add a OnMethodBoundaryAspect on each writable non-static property.
foreach (PropertyInfo property in targetType.GetProperties())
{
if (property.DeclaringType == targetType && property.CanWrite)
{
MethodInfo method = property.GetSetMethod();
if (!method.IsStatic)
{
collection.AddAspect(method, new OnPropertySetSubAspect(property.Name, this));
}
}
}

if (string.IsNullOrEmpty(onPropertyChanged))
collection.AddAspect(targetType, new AddNotifyPropertyChangedInterfaceSubAspect()); //On the type, add a Composition aspect to implement the INotifyPropertyChanged interface
else
{
//validate onPropertyChanged has correct signature
MethodInfo mi = targetType.GetMethod(onPropertyChanged, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
if (mi == null)
MessageSource.MessageSink.Write(new Message(SeverityType.Error, "CU0001", string.Format("OnPropertyChanged ({0}) method not found on type {1}", onPropertyChanged, targetType.Name), "CoreAspect"));
else
{
ParameterInfo[] p = mi.GetParameters();
if (!(p.Length == 1 && p[0].ParameterType == typeof(string)))
{
MessageSource.MessageSink.Write(new Message(SeverityType.Error, "CU0001", string.Format("OnPropertyChanged method must take only 1 string parameter: {0}.{1}", targetType.Name, mi.Name), "CoreAspect"));
}
}

}
}

public int AspectPriority
{
get { return aspectPriority; }
set { aspectPriority = value; }
}

public string OnPropertyChanged
{
get { return onPropertyChanged; }
set { onPropertyChanged = value; }
}

public bool CheckEqualBeforeSet
{
get { return checkEqualBeforeSet; }
set { checkEqualBeforeSet = value; }
}
}


OnPropertyChanged Usage:

[NotifyPropertyChanged(OnPropertyChanged="OnPropertyChanged")]
public class Borrower : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;

protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;

if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}

public string FirstName { get; set; }
public string LastName { get; set; }

}

Brian Chance
Post #3626
Posted 8/19/2009 3:47:19 AM


Gael Fraiteur

SharpCrafters
Brian,

Thank you for your feedback.

I am aware of the limitations of PostSharp 1.5 regarding the implementation of INotifyPropertyChanged.

PostSharp 2.0 will come with a fully new design that elegantly solves this problem.

I'll be out in a couple of months.

-gael
Post #3627
Posted 8/19/2009 1:45:57 PM


Community Member
Great, cannot wait for it. Is there a roadmap for 2.0 anywhere?

Forgot to say earlier, great, great tool.

Brian
Post #3628
Posted 8/19/2009 1:55:14 PM


Gael Fraiteur

SharpCrafters
The roadmap currently is that I am targetting a first CTP before 15th October (it's a deadline because I will have a demo in Germany), hopefully before. I'm not sure yet for the stable release.
Post #3629
Posted 8/26/2009 8:05:05 AM


Community Member
Great implementation !

below, one mix of
_implementation of the post above
_of the Binding sample
_of implementation presents on <!-- m --><a class="postlink" href="http://codepaste.net/3h4uqp">http://codepaste.net/3h4uqp</a><!-- m -->

It takes again the advantages of the 3:
_if no implementation of INotifyPropertyChanged, auto interface implementation by aspect
_if present and implement IFirePropertyChanged, automatic set of OnPropertyChanged variable
_Can be set only one Property.

usage :

[NotifyPropertyChanged]
   public class Borrower2
   {
       public string FirstName { get; set; }
       public string LastName { get; set; }
   }

or

public class Borrower3 : IFirePropertyChanged, INotifyPropertyChanged
   {
       [RaisePropertyChanged]
       public string FirstName { get; set; }
       public string LastName { get; set; }

       public event PropertyChangedEventHandler PropertyChanged;

       public virtual void OnPropertyChanged(string propertyName)
       {
           PropertyChangedEventHandler handler = PropertyChanged;

           if (handler != null)
           {
               handler(this, new PropertyChangedEventArgs(propertyName));
           }
       }
   }

or

   [NotifyPropertyChanged(OnPropertyChanged = "OnPropertyChanged")]
   public class Borrower : INotifyPropertyChanged
   {
       public event PropertyChangedEventHandler PropertyChanged;

       protected virtual void OnPropertyChanged(string propertyName)
       {
           PropertyChangedEventHandler handler = PropertyChanged;

           if (handler != null)
           {
               handler(this, new PropertyChangedEventArgs(propertyName));
           }
       }

       public string FirstName { get; set; }
       public string LastName { get; set; }
   }

or

[NotifyPropertyChanged]
   public class Borrower4 : IFirePropertyChanged, INotifyPropertyChanged
   {
       public string FirstName { get; set; }
       public string LastName { get; set; }

       public event PropertyChangedEventHandler PropertyChanged;

       public virtual void OnPropertyChanged(string propertyName)
       {
           PropertyChangedEventHandler handler = PropertyChanged;

           if (handler != null)
           {
               handler(this, new PropertyChangedEventArgs(propertyName));
           }
       }
   }

[size=150]implementation :[/size]

NotifyPropertyChangedAttribute.cs

   
   /// <summary>
   /// Implementation of <see cref="CompositionAspect"/> that adds the <see cref="INotifyPropertyChanged"/>
   /// interface to the type to which it is applied.
   /// </summary>
   [Serializable]
   internal class AddNotifyPropertyChangedInterfaceSubAspect : CompositionAspect
   {
       /// <summary>
       /// Called at runtime, creates the implementation of the <see cref="INotifyPropertyChanged"/> interface.
       /// </summary>
       /// <param name="eventArgs">Execution context.</param>
       /// <returns>A new instance of <see cref="NotifyPropertyChangedImplementation"/>, which implements
       /// <see cref="INotifyPropertyChanged"/>.</returns>
       public override object CreateImplementationObject(InstanceBoundLaosEventArgs eventArgs)
       {
           return new NotifyPropertyChangedImplementation(eventArgs.Instance);
       }

       /// <summary>
       /// Called at compile-time, gets the interface that should be publicly exposed.
       /// </summary>
       /// <param name="containerType">Type on which the interface will be implemented.</param>
       /// <returns></returns>
       public override Type GetPublicInterface(Type containerType)
       {
           return typeof(INotifyPropertyChanged);
       }

       public override Type[] GetProtectedInterfaces(Type containerType)
       {
           return new Type[] { typeof(IFirePropertyChanged) };
       }

       /// <summary>
       /// Gets weaving options.
       /// </summary>
       /// <returns>Weaving options specifying that the implementation accessor interface (<see cref="IComposed{T}"/>)
       /// should be exposed, and that the implementation of interfaces should be silently ignored if they are
       /// already implemented in the parent types.</returns>
       public override CompositionAspectOptions GetOptions()
       {
           return
               CompositionAspectOptions.GenerateImplementationAccessor |
               CompositionAspectOptions.IgnoreIfAlreadyImplemented;
       }
   }

   /// <summary>
   /// Implementation of the <see cref="INotifyPropertyChanged"/> interface.
   /// </summary>
   internal class NotifyPropertyChangedImplementation : INotifyPropertyChanged, IFirePropertyChanged
   {
       // Instance that exposes the current implementation.
       private readonly object instance;

       /// <summary>
       /// Initializes a new <see cref="NotifyPropertyChangedImplementation"/> instance.
       /// </summary>
       /// <param name="instance">Instance that exposes the current implementation.</param>
       public NotifyPropertyChangedImplementation(object instance)
       {
           this.instance = instance;
       }

       /// <summary>
       /// Event raised when a property is changed on the instance that
       /// exposes the current implementation.
       /// </summary>
       public event PropertyChangedEventHandler PropertyChanged;

       /// <summary>
       /// Raises the <see cref="PropertyChanged"/> event. Called by the
       /// property-level aspect (<see cref="AddNotifyPropertyChangedInterfaceSubAspect"/>)
       /// at the end of property set accessors.
       /// </summary>
       /// <param name="propertyName">Name of the changed property.</param>
       public void OnPropertyChanged(string propertyName)
       {
           if (this.PropertyChanged != null)
           {
               this.PropertyChanged(this.instance, new PropertyChangedEventArgs(propertyName));
           }
       }
   }




   /// <summary>
   /// Custom attribute that, when applied on a type (designated <i>target type</i>), implements the interface
   /// <see cref="INotifyPropertyChanged"/> and raises the <see cref="INotifyPropertyChanged.PropertyChanged"/>
   /// event when any property of the target type is modified.
   /// </summary>
   /// <remarks>
   /// Event raising is implemented by appending logic to the <b>set</b> accessor of properties. The
   /// <see cref="INotifyPropertyChanged.PropertyChanged"/> is raised only when accessors successfully complete.
   /// <para>
   /// Auto implement INotifyPropertyChanged for properties
   /// If class implement INotifyPropertyChanged set parameter OnPropertyChanged to changed method,
   /// if implement IFirePropertyChanged and INotifyPropertyChanged add nothing :)
   /// if implement nothing, it will be done, but not accessible without Post.Cast
   /// </para>
   /// </remarks>
   [MulticastAttributeUsage(MulticastTargets.Class | MulticastTargets.Struct)]
   [Serializable]
   public sealed class NotifyPropertyChangedAttribute : CompoundAspect
   {
       [NonSerialized]
       private int aspectPriority = 0;
       [NonSerialized]
       private string onPropertyChanged = string.Empty;
       [NonSerialized]
       private bool checkEqualBeforeSet = true;

       /// <summary>
       /// Method called at compile time to get individual aspects required by the current compound
       /// aspect.
       /// </summary>
       /// <param name="targetElement">Metadata element (<see cref="Type"/> in our case) to which
       /// the current custom attribute instance is applied.</param>
       /// <param name="collection">Collection of aspects to which individual aspects should be
       /// added.</param>
       public override void ProvideAspects(object targetElement, LaosReflectionAspectCollection collection)
       {
           Type targetType = (Type)targetElement;
           
           if (string.IsNullOrEmpty(onPropertyChanged))
           {
               if (targetType.GetInterface(typeof(INotifyPropertyChanged).FullName) != null)
                   if (targetType.GetInterface(typeof(IFirePropertyChanged).FullName) != null)
                       onPropertyChanged = "OnPropertyChanged";
                   else
                       MessageSource.MessageSink.Write(new Message(SeverityType.Error, "CU0001", string.Format("OnPropertyChanged ({0}) method not found on type {1}, if type implement INotifyPropertyChanged precise OnPropertyChanged or implement IFirePropertyChanged", onPropertyChanged, targetType.Name), "CoreAspect"));

           }
           if (string.IsNullOrEmpty(onPropertyChanged))
               collection.AddAspect(targetType, new AddNotifyPropertyChangedInterfaceSubAspect()); //On the type, add a Composition aspect to implement the INotifyPropertyChanged interface
           else
           {
               //validate onPropertyChanged has correct signature
               MethodInfo mi = targetType.GetMethod(onPropertyChanged, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
               if (mi == null)
                   MessageSource.MessageSink.Write(new Message(SeverityType.Error, "CU0001", string.Format("OnPropertyChanged ({0}) method not found on type {1}", onPropertyChanged, targetType.Name), "CoreAspect"));
               else
               {
                   ParameterInfo[] p = mi.GetParameters();
                   if (!(p.Length == 1 && p[0].ParameterType == typeof(string)))
                   {
                       MessageSource.MessageSink.Write(new Message(SeverityType.Error, "CU0001", string.Format("OnPropertyChanged method must take only 1 string parameter: {0}.{1}", targetType.Name, mi.Name), "CoreAspect"));
                   }
               }
           }

           // Add a OnMethodBoundaryAspect on each writable non-static property.
           foreach (PropertyInfo property in targetType.GetProperties())
           {
               if (property.DeclaringType == targetType && property.CanWrite)
               {
                   MethodInfo method = property.GetSetMethod();
                   if (!method.IsStatic)
                   {
                       collection.AddAspect(method, new RaisePropertyChangedAttribute(property.Name, this));
                   }
               }
           }
       }

       /// <summary>
       /// Gets or sets the priority of the property-level aspect.
       /// </summary>
       /// <remarks>
       /// Give a large number to have the event raisen after any other
       /// on-success aspect on the properties of this type. The default value
       /// is 0.
       /// </remarks>
       public int AspectPriority
       {
           get { return aspectPriority; }
           set { aspectPriority = value; }
       }

       public string OnPropertyChanged
       {
           get { return onPropertyChanged; }
           set { onPropertyChanged = value; }
       }

       public bool CheckEqualBeforeSet
       {
           get { return checkEqualBeforeSet; }
           set { checkEqualBeforeSet = value; }
       }
   }


RaisePropertyChangedAttribute.cs

/// <summary>
   /// Implementation of <see cref="OnMethodBoundaryAspect"/> that raises the
   /// <see cref="INotifyPropertyChanged.PropertyChanged"/> event when a property set
   /// accessor completes successfully.
   /// </summary>
   [MulticastAttributeUsage(MulticastTargets.Method)]
   [Serializable]
   public class RaisePropertyChangedAttribute : OnMethodBoundaryAspect
   {
       string propertyName;
       string onPropertyChanged;
       bool checkEqualBeforeSet;

       public RaisePropertyChangedAttribute()
       {
       }

       /// <summary>
       /// Initializes a new <see cref="RaisePropertyChangedAttribute"/>.
       /// </summary>
       /// <param name="propertyName">Name of the property to which this set accessor belong.</param>
       /// <param name="parent">Parent <see cref="NotifyPropertyChangedAttribute"/>.</param>
       public RaisePropertyChangedAttribute(string propertyName, NotifyPropertyChangedAttribute parent)
       {
           this.AspectPriority = parent.AspectPriority;
           this.OnPropertyChanged = parent.OnPropertyChanged;
           this.CheckEqualBeforeSet = parent.CheckEqualBeforeSet;
           this.propertyName = propertyName;
       }

       [NonSerialized]
       MethodInfo getMethod;


       [NonSerialized]
       MethodInfo onPropertyChangedMethod;


       public string OnPropertyChanged
       {
           get { return onPropertyChanged; }
           set { onPropertyChanged = value; }
       }

       public bool CheckEqualBeforeSet
       {
           get { return checkEqualBeforeSet; }
           set { checkEqualBeforeSet = value; }
       }

       //public override void CompileTimeInitialize(MethodBase method)
       //{
       //    Type T = method.DeclaringType;

       //    if (T.GetInterface(typeof(INotifyPropertyChanged).FullName) == null)
       //    {
       //        string a = string.Empty;
       //        foreach (var type in T.GetInterfaces())
       //        {
       //            a += type.Name + ", ";
       //        }

       //        throw new Exception(string.Format("Class {0} has an attribute RaisePropertyChanged but doesn't implements INotifyPropertyChanged!", T.Name + " " + a));
       //    }
       //    base.CompileTimeInitialize(method);

       //}


       public override bool CompileTimeValidate(MethodBase method)
       {
           if (!string.IsNullOrEmpty(propertyName)) return true;

           // Set propertyName
           if (IsPropertySetter(method))
           {
               propertyName = GetPropertyName(method);
           }
           else
               return false;


           // Set onPropertyChanged

           Type targetType = method.DeclaringType;
           if (string.IsNullOrEmpty(onPropertyChanged))
           {
               if (targetType.GetInterface(typeof(INotifyPropertyChanged).FullName) != null)
                   if (targetType.GetInterface(typeof(IFirePropertyChanged).FullName) != null)
                       onPropertyChanged = "OnPropertyChanged";
                   else
                   {
                       MessageSource.MessageSink.Write(new Message(SeverityType.Error, "CU0001", string.Format("OnPropertyChanged ({0}) method not found on type {1}, if type implement INotifyPropertyChanged precise OnPropertyChanged or implement IFirePropertyChanged", onPropertyChanged, targetType.Name), "CoreAspect"));
                       return false;
                   }

           }
           if (string.IsNullOrEmpty(onPropertyChanged))
           {
               MessageSource.MessageSink.Write(new Message(SeverityType.Error, "CU0001", string.Format("OnPropertyChanged ({0}) method not found on type {1}", onPropertyChanged, targetType.Name), "CoreAspect"));
               return false;
           }
           //validate onPropertyChanged has correct signature
           MethodInfo mi = targetType.GetMethod(onPropertyChanged, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
           if (mi == null)
           {
               MessageSource.MessageSink.Write(new Message(SeverityType.Error, "CU0001", string.Format("OnPropertyChanged ({0}) method not found on type {1}", onPropertyChanged, targetType.Name), "CoreAspect"));
               return false;
           }
           else
           {
               ParameterInfo[] p = mi.GetParameters();
               if (!(p.Length == 1 && p[0].ParameterType == typeof(string)))
               {
                   MessageSource.MessageSink.Write(new Message(SeverityType.Error, "CU0001", string.Format("OnPropertyChanged method must take only 1 string parameter: {0}.{1}", targetType.Name, mi.Name), "CoreAspect"));
                   return false;
               }
           }
           return true;
       }

       private static string GetPropertyName(MethodBase method)
       {
           return method.Name.Replace("set_", "");
       }

       private static bool IsPropertySetter(MethodBase method)
       {
           return method.Name.StartsWith("set_");
       }

       public virtual PostSharp.Laos.OnMethodBoundaryAspectOptions GetOptions()
       {
           return OnMethodBoundaryAspectOptions.None;
       }

       public override void RuntimeInitialize(MethodBase method)
       {
           if (!string.IsNullOrEmpty(onPropertyChanged))
               onPropertyChangedMethod = method.DeclaringType.GetMethod(onPropertyChanged, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);

           if (checkEqualBeforeSet)
               getMethod = method.DeclaringType.GetProperty(propertyName).GetGetMethod();
       }

       /// <summary>
       /// Executed when the set accessor is entered.
       /// </summary>
       /// <param name="eventArgs">Event arguments with information about the
       /// current execution context.</param>
       public override void OnEntry(MethodExecutionEventArgs eventArgs)
       {
           if (checkEqualBeforeSet)
           {
               object oldValue = getMethod.Invoke(eventArgs.Instance, null);
               object newValue = eventArgs.GetReadOnlyArgumentArray()[0];
               if ((oldValue == null && newValue == null) || (oldValue != null && oldValue.Equals(newValue)))
                   eventArgs.FlowBehavior = FlowBehavior.Return;
           }
       }

       /// <summary>
       /// Executed when the set accessor successfully completes. Raises the
       /// <see cref="INotifyPropertyChanged.PropertyChanged"/> event.
       /// </summary>
       /// <param name="eventArgs">Event arguments with information about the
       /// current execution context.</param>
       public override void OnSuccess(MethodExecutionEventArgs eventArgs)
       {
           if (onPropertyChangedMethod == null)
           {
               NotifyPropertyChangedImplementation implementation = (NotifyPropertyChangedImplementation)((IComposed<INotifyPropertyChanged>)eventArgs.Instance).GetImplementation(eventArgs.InstanceCredentials);
               implementation.OnPropertyChanged(propertyName);
           }
           else
               onPropertyChangedMethod.Invoke(eventArgs.Instance, new object[] { propertyName });
       }
   }


Best regards,

Alexandre Richonnier
Post #3630
Posted 8/26/2009 8:09:35 AM


Gael Fraiteur

SharpCrafters
Thanks!
Post #3631
« Prev Topic | Next Topic »


All times are GMT +1:00, Time now is 11:32am

Powered By InstantForum.NET v4.1.4 © 2010
Execution: 0.106. 8 queries. Compression Disabled.