Sunday, January 20, 2008

PostSharp - Part 2: DataBinding support and some adjustments

Now that I have had a chance to play around with PostSharp with a few more rounds I've discovered that my old examples have some room for improvement.
Go here for my initial impressions.
  • I want my "Triggers" to occur only on property setters
  • I want the triggers to disregard any property related to reflection
  • I want to my triggers to be independent of one another (this is important)
  • I want to create a non-intrusive attribute that supports INotifyPropertyChanged
For the first two requirements, this is easy. I add some conditionals to the beginning of each MethodBoundaryAspect event. For example,


[Serializable]
public class NotNullValidator : OnMethodBoundaryAspect
{
public override void OnEntry(MethodExecutionEventArgs eventArgs)
{
//ignore calls that have no arguments, they come from reflection
if (eventArgs.GetArguments() == null)
return;
//ignore anything that isnt a setter
if (!eventArgs.Method.Name.StartsWith("set_"))
return;

if (eventArgs.GetArguments()[0] == null)
throw new ArgumentNullException(eventArgs.Method.Name);
base.OnEntry(eventArgs);
}
}

Now for the INotifyPropertyChanged functionality. I searched for an existing example, but came up empty-handed. Fortunately, there was an open source version of Data Binding support readily available at http://code.google.com/p/postsharp-user-samples/ . These examples are in VB, so I converted the code in to C#. I will be looking at adding my changes to the trunk as soon as I can get my examples to match their standards. In the meantime, this is the C# code I have been using for NotifyPropertyChanged support (based on the provided VB code):

/// <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 and the
/// underlying value is really changed.
/// </remarks>
[MulticastAttributeUsage(MulticastTargets.Class | MulticastTargets.Struct), Serializable()]
public sealed class SupportDataBindingAttribute : CompoundAspect
{

#region "Private Variables"
[NonSerialized()]
private int myAspectPriority = 0;
#endregion

/// <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)
{
// Get the target type.
Type targetType = (Type) targetElement;
// On the type, add a Composition aspect to implement the INotifyPropertyChanged interface.
collection.AddAspect(targetType, new AddNotifyPropertyChangedInterfaceSubAspect());
// On the type, add a Composition aspect to implement the IEditableObject interface
//collection.AddAspect(targetType, new AddEditableObjectInterfaceSubAspect());
// Add a OnMethodBoundaryAspect on each writable non-static property. The implementation of
// INotifyPropertyChanged.PropertyChanged needs the name of the property (not of the field), so we have to detect
// changes on the property level, not on the field level. Unfortunately, there is no rule for naming properties and
// their related fields. Even more, one property could access many fields, or gets its value out of one or
// more fields. At this point, using an enhancer to add this functionallity is only recomended, as there exixts a design
// rule, which relates one field to one and only one property and vice versa. Unfortunately, there is no compile time check available.
// Please be careful!!
// Personally, I tend to say, that implementing INotifyPropertyChanged is out of the scope of enhancers due to the
// possibility to pack logic within the properties implementation, which is never under the enhancers control.
foreach (PropertyInfo pi in targetType.UnderlyingSystemType.GetProperties())
{
if (object.ReferenceEquals(pi.DeclaringType, targetType) && pi.CanWrite)
{
MethodInfo mi = pi.GetSetMethod();
if (!mi.IsStatic)
{
collection.AddAspect(mi, new OnPropertySetSubAspect(pi.Name, this));
}
}
}
}
public int AspectPriority {
get { return myAspectPriority; }
set { myAspectPriority = value; }
}

/// <summary>
/// Implementation of <see cref="OnMethodBoundaryAspect"/> that raises the
/// <see cref="INotifyPropertyChanged.PropertyChanged"/> event when a property set
/// accessor completes successfully and the value really changes.
/// </summary>
[Serializable()]
private class OnPropertySetSubAspect : OnMethodBoundaryAspect
{

private readonly string myPropertyName;
private object myOldValue;

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

public override void OnEntry(MethodExecutionEventArgs eventArgs)
{
// Construct the name of the properties get-method and backup the value before the set-method is invoked.
myOldValue =
eventArgs.Instance.GetType().InvokeMember(eventArgs.Method.Name.Substring(4),
BindingFlags.GetProperty, null, eventArgs.Instance, null,
null, null, null);
base.OnEntry(eventArgs);
}
/// <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)
{
object newValue;
newValue =
eventArgs.Instance.GetType().InvokeMember(eventArgs.Method.Name.Substring(4),
BindingFlags.GetProperty, null, eventArgs.Instance, null,
null, null, null);
// Raises the PropertyChanged event, if necessary. We assume in this sample, that only value types were used.
if (myOldValue != newValue)
{
// Get the implementation of INotifyPropertyChanged. We have access to it through the IComposed interface,
// which is implemented at compile time.
NotifyPropertyChangedImplementation implementation =
(NotifyPropertyChangedImplementation)
((IComposed<INotifyPropertyChanged>) eventArgs.Instance).GetImplementation(
eventArgs.InstanceCredentials);
implementation.OnPropertyChanged(myPropertyName);
}
}
}

/// <summary>
/// Implementation of <see cref="CompositionAspect"/> that adds the <see cref="INotifyPropertyChanged"/>
/// interface to the type to which it is applied.
/// </summary>
[Serializable()]
private 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);
}

/// <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>
private class NotifyPropertyChangedImplementation : INotifyPropertyChanged
{
// Instance that exposes the current implementation.
private readonly object myInstance;

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

/// <summary>
/// Event raised when a property is changed on the instance that
/// exposes the current implementation.
/// </summary>
//public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
//public delegate void PropertyChangedEventHandler(object sender, System.ComponentModel.PropertyChangedEventArgs e);

/// <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 (PropertyChanged != null) {
PropertyChanged(myInstance, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
}


How to use this? In my business object, I have:

[SupportDataBinding]
public class SomeBusinessObject
{
#region members
private string m_Name;
#endregion

#region properties

[NotNullValidator]
[Logger( LogOnEntry = true, LogOnException = true, LogOnSuccess = true)]
[StringLengthValidator(MinLength = 1,MaxLength = 10)]
public string Name
{
get { return m_Name; }
set { m_Name = value; }
}
#endregion

#region methods
public SomeBusinessObject(string name)
{
m_Name = name;
}
#endregion
}

And a test program:

class Program
{
static void Main(string[] args)
{
SomeBusinessObject someBusinessObject = new SomeBusinessObject("Pete");
INotifyPropertyChanged bindableVersion = someBusinessObject as INotifyPropertyChanged;
bindableVersion.PropertyChanged += new PropertyChangedEventHandler(bindableVersion_PropertyChanged);
try
{
Console.WriteLine("Try passing in something long");
someBusinessObject.Name = "Some name that is too long";
}
catch(Exception e)
{
Console.WriteLine("Oops! that was too long");
}
try
{
Console.WriteLine("Try passing in something too short");
someBusinessObject.Name = string.Empty;
}
catch(Exception e)
{
Console.WriteLine("Oops! That was too short");
}
try
{
Console.WriteLine("Try passing in a null");
someBusinessObject.Name = null;
}
catch
{
Console.WriteLine("Oops, that was too null...");
}

Console.WriteLine("Run a successful property change.");
someBusinessObject.Name = "123";
Console.Read();
}

static void bindableVersion_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
Console.WriteLine("Handling property changed event!");
}
}

And my output:

Try passing in something long
Oops! that was too long
Try passing in something too short
Oops! That was too short
Try passing in a null
Oops, that was too null...
Run a successful property change.
OnEntry: set_Name
OnSuccess: set_Name
Handling property changed event!

Contact me if you are interested in getting my code in a downloadable solution and I will post it! Otherwise, I am going to work at getting my code on to the PostSharp user samples next week.

5 Comments:

Blogger Martin Nyborg said...

I have worked on and off with the CSLA framework for 6 years now. The CSLA objects is optimized for winforms with rich support for databinding, validation rules and n-level undo. I would like to know if you have compared the PostSharp databinding approach with a "hard coded" one. But anyway I have to try it. It can reduce the amount of code if it's works

1:58 PM  
Blogger Peter Weissbrod said...

Martin:

I might be able to help give you some more information on this.

CSLA is cool but I have no experience with it. I am currently working on a WPF/NHibernate project which has a significant level of business logic complexity.

I am interested in seeing how PostSharp interacts with the reflection-intensive DynamicProxy technology of NHibernate. I'll have some updates on this in the near future.
-p

4:59 PM  
Blogger Cristiano said...

Hi Pete, I would like to see your code, could you post the solution please?

Im trying to use the same concept for my project on c# 2.0 (I really cant use 3.X). Do you think its possible?

Thanks

5:23 AM  
Blogger Peter Weissbrod said...

Hi Cristiano!

You definitely can use this in .NET 2. The differences between .net 2 and .net 3 lie within linq, wpf, and a few new syntactical shortcuts, none of them are used in my code.

All of my validator code can be found at google code.
http://code.google.com/p/postsharp-user-samples/
Use subversion and check out yourself a copy of the trunk to get started.
Good luck with your project.
-p

5:34 AM  
Anonymous Anonymous said...

Why do you get the UnderlyingType before enumerating the properties?

Also you can specify BindingFlags.Instance and DeclaredOnly to eliminate some of the checks inside your pi loop.

8:24 AM  

Post a Comment

<< Home