The PostSharp.Samples.Binding
project demonstrates how to combine many sub-aspects in a single custom
attribute using the CompoundAttribute
mechanism.
As an example, we tried to implement automatically interfaces used in data binding:
INotifyPropertyChanged
contains a
single event named PropertyChanged
,
which should be raised, obviously, whenever a property is changed. This
interface enables objects to be observed, i.e. to make a view of the
object without having to continuously polling for changes.IEditableObject
(from the
namespace System.ComponentModel
), provides
the functionality to commit or rollback changes to an object. It
defines the methods BeginEdit
, EndEdit
and CancelEdit
.public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler PropertyChanged;
}
The solution is in three steps:
INotifyPropertyChanged
interface.PropertyChanged
event
of INotifyPropertyChanged
when the value changes.We will implement the INotifyPropertyChanged
interface by composition. That is, we will develop
a class (NotifyPropertyChangedImplementation
) that will
implement INotifyPropertyChanged
. This class will
be composed into the aspected class (say
that we the Customer
class to be observable; we
call this class the aspected class). The
INotifyPropertyChanged
interface would be exposed by the aspected class, but the
implementation would be delegated to the class NotifyPropertyChangedImplementation
.
Note that this class is purely runtime. It is never instantiated at
compile time.
private class NotifyPropertyChangedImplementation : INotifyPropertyChanged
{
private object instance;
public NotifyPropertyChangedImplementation(object instance)
{
this.instance = instance;
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this.instance, new PropertyChangedEventArgs(propertyName));
}
}
}
The code is annoyingly classic.
Class composition is supported in PostSharp Laos by the CompositionAspect
class (or the ICompositionAspect
interface, if you prefer). We derive from it the new class AddNotifyPropertyChangedInterfaceSubAspect
:
[Serializable]
private class AddNotifyPropertyChangedInterfaceSubAspect : CompositionAspect
{
public override object CreateImplementationObject(InstanceBoundLaosEventArgs eventArgs)
{
return new NotifyPropertyChangedImplementation(eventArgs.Instance);
}
public override Type GetPublicInterface(Type containerType)
{
return typeof(INotifyPropertyChanged);
}
public override CompositionAspectOptions GetOptions()
{
return CompositionAspectOptions.GenerateImplementationAccessor | CompositionAspectOptions.IgnoreIfAlreadyImplemented;
}
}
The GetPublicInterface
method is
called at compile time. It returns which interface should be exposed on
the aspected type. The CreateImplementationObject
is called at runtime to create the object that will actually implement
the interface. We create an instance of our
NotifyPropertyChangedImplementation
.
In order to modify the write accessor of properties, we derive
a new OnPropertySetSubAspect
class from OnMethodBoundaryAspect
and we simply override the OnSuccess
method
so that it calls the OnPropertyChanged
method
of our NotifyPropertyChangedImplementation
. Seems pretty
easy.
OnPropertyChanged
method? One solution would be to add this method in a public
interface derived from INotifyPropertyChanged
, and expose this interface. We would just have to
cast the property instance and call the OnPropertyChanged
method. But it is not very safe: everyone could
raise this event. IComposed
interface (in our
case IComposed<INotifyPropertyChanged>
) on
the aspected type. The way to request it is to return the GenerateImplementationAccessor
flag from the GetOptions
method of the CompositionAspect
(see above). This interface defines the method GetImplementation
,
which retrieve obviously the object implementing the composed
interface, in our case NotifyPropertyChangedImplementation
.[Serializable]
private class OnPropertySetSubAspect : OnMethodBoundaryAspect
{
string propertyName;
public OnPropertySetSubAspect( string propertyName, NotifyPropertyChangedAttribute parent )
{
this.AspectPriority = parent.AspectPriority;
this.propertyName = propertyName;
}
public override void OnSuccess(MethodExecutionEventArgs eventArgs)
{
NotifyPropertyChangedImplementation implementation =
(NotifyPropertyChangedImplementation) ((IComposed<INotifyPropertyChanged>)eventArgs.Instance). GetImplementation(eventArgs.InstanceCredentials);
implementation.OnPropertyChanged(this.propertyName);
}
}
We now have all code transformations we need to realize the
'notify property changed' functionality, but we need to assemble them in a single
custom attribute that could be applied to any class. This is the role
of CompoundAspect
,
from which we derive our NotifyPropertyChangedAttribute
.
[MulticastAttributeUsage(MulticastTargets.Class | MulticastTargets.Struct)]
[Serializable]
public sealed class NotifyPropertyChangedAttribute : CompoundAspect
{
[NonSerialized]
private int aspectPriority = 0;
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());
// 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));
}
}
}
}
public int AspectPriority
{
get { return aspectPriority; }
set { aspectPriority = value; }
}
}
The principal method of this aspect is of course ProvideAspects
.
It should add sub-aspects into a collection. Here we add AddNotifyPropertyChangedInterfaceSubAspect
to the aspected class. Then we select all writable properties and we
apply OnPropertySetSubAspect
to their
write accessor, unless if the property is static.
This aspect is structurally similar to the Notify Property Changed Aspect, but it works on field-level and not on accessor-level.
Although there exists other strategies, we chose here to virtualize the field storage, that is, we will replace the instance fields by something else. Yes, we will remove the fields at compile-time.
Instead of the instance fields, we will have two
dictionaries: one with the working copy of fields, the second with the
backup copy. Instead of reading from and writing to instance fields, we
will read from and write to the dictionary of working values. The BeginEdit
,
EndEdit
and CancelEdit
methods will simply copy the dictionary of working values into the
dictionary of backup values or conversely.
Both dictionaries will be instance fields of the EditableImplementation
class, which is the composed object implementing IEditableObject
.
Additionally to this interface, this class will have the semantics SetValue
and GetValue
.
Please refer to the code for more details. If you have understood the Notify Propetrty Changed sample, EditableObject should not be problematic.