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.
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):
How to use this? In my business object, I have:
And a test program:
And my output:
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.
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
[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.


7 Comments:
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
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
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
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
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.
Peter
Were you ever able to get PostSharp and NHibernate working together? I seem to be having a bit of trouble with [NotifyPropertyChanged] cohabiting with NHibernate. Any hints or advice?
Dale
Hmm maybe what you are having trouble with is the fact that NHibernate can trigger propertychangedevents when creating/hydrating an object?
If so, then your mappings probably tell NHibernate to manipulate public properties. this is how it works by default. To avoid this, you can set the access="field" on your mappings, and that will instruct NHibernate to manipulate the private variables that are encapsulated by your public properties. If NHibernate has direct access to the private fields, this will subvert the propertychangedevent while still allowing the property to change.
Post a Comment
<< Home