Introduction
I spend some time looking for AOP options for .NET. All references point to PostSharp as the coolest option - even when you have to pay to use it - due to its features, starting from the way it works – in build time and static – plus a lot of build-in “advice” and extensions points.
There are also several run time and dynamic options such as Castle, Unity, etc., but I prefer the build time and static approach. Indeed I use Fody - as an alternative to PostSharp - even when I have to write down custom plugins directly in IL.
But all of these AOP-like libraries and tools for .NET do not allow you to handle exactly the all the AOP concepts such as pointcut, join-point, and advice.
But recently I found a project in CodePlex, named SheepAspect with an introductory statement that includes the following words “…was inspired in AspectJ”. So, I just installed the package from NuGet and started to write in C# a notify property changed proof of concept with a friend of mine (Leandro).
Introducing NotifyPropertyChanged aspect
The very first step is to write a NotifyPropertyChangedAspect class. It’s important to get related with SAQL in order to query the right properties from the right types, and with the usage of the SheepAspect attributes. For this particular example the pointcut includes “all public property setters of types that implement System.ComponentModel.INotifyPropertyChanged” and look like this:
[Aspect] public class NotifyPropertyChangedAspect { [SelectTypes("ImplementsType:'System.ComponentModel.INotifyPropertyChanged'")] public void NotifiedPropertyChangedTypes() { } [SelectPropertyMethods("Public & Setter & InType:AssignableToType:@NotifiedPropertyChangedTypes")] public void PublicPropertiesOfTypesThatImplementsINotifyPropertyChangedInterfacePointCut() { } }
Now, I just needed to add the notify property changed behavior as around advice just as follow:
[Around("PublicPropertiesOfTypesThatImplementsINotifyPropertyChangedInterfacePointCut")] public void AdviceForPublicPropertiesOfTypesThatImplementsINotifyPropertyChangedInterface(PropertySetJointPoint jp) { object value = jp.Property.GetValue(jp.This); if (!object.Equals(value, jp.Value)) { jp.Proceed(); jp.This.RaiseNotifyPropertyChanged(jp.Property); } }
As you should notice, to implement this, I also introduce a couple of extension methods TryGetPropertyChangedField and of course the RaiseNotifyPropertyChanged itself:
public static bool TryGetPropertyChangedField(this Type type, out FieldInfo propertyChangedEvent) { propertyChangedEvent = null; while (propertyChangedEvent == null && type != null && type != typeof(object)) { propertyChangedEvent = type.GetField("PropertyChanged", BindingFlags.Instance | BindingFlags.NonPublic); if (propertyChangedEvent == null) { type = type.BaseType; } else if (!typeof(MulticastDelegate).IsAssignableFrom(propertyChangedEvent.FieldType)) { propertyChangedEvent = null; } } return propertyChangedEvent != null; } /*...*/ public static void RaiseNotifyPropertyChanged(this object instance, PropertyInfo property) { FieldInfo propertyChangedEvent; if (instance.GetType().TryGetPropertyChangedField(out propertyChangedEvent)) { var propertyChangedEventMulticastDelegate = (MulticastDelegate)propertyChangedEvent.GetValue(instance); var invocationList = propertyChangedEventMulticastDelegate.GetInvocationList(); foreach (var handler in invocationList) { MethodInfo methodInfo = handler.GetMethodInfo(); methodInfo.Invoke(handler.Target, new[] { instance, new PropertyChangedEventArgs(property.Name) }); } } }
Now, if a class that implements or inherits from a class that implements INotifyPropertyChanged interface is added, just like this one:
public class Person : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public string FirstName { get; set; } public string LastName { get; set; } public string FullName { get { return string.Format(CultureInfo.InvariantCulture, "{0} {1}", this.FirstName, this.LastName).Trim(); } } }
and a program like this one is written:
this.person.PropertyChanged += (sender, args) => { object value = sender.GetType().GetProperty(args.PropertyName).GetValue(sender); Console.WriteLine("Property Changed => '{0}' = '{1}'", args.PropertyName, value); }; this.person.FirstName = "Igr Alexánder"; this.person.LastName = "Fernández Saúco";
then the output will be:
Property Changed => 'FirstName' = 'Igr Alexánder'
Property Changed => 'LastName' = 'Fernández Saúco'
Uhmm! But what about with the computed read-only properties like FullName?
Notifying property changes of computed read-only properties.
In order to notify changes of computed read-only properties, an inspection of the IL code is required. The .NET native reflection API is limited. From the PropertyInfo is only possible to get the IL byte array from the get method body, and nothing more. Therefore, I just switched to the Mono.Cecil reflection API to be able to complement the existing RaiseNotifyPropertyChanged extension method with the following extension methods:
public static bool ExistPropertyDependencyBetween(this Type type, PropertyInfo dependentProperty, PropertyInfo propertyInfo) { AssemblyDefinition assemblyDefinition = new DefaultAssemblyResolver().Resolve(type.Assembly.FullName); TypeDefinition typeDefinition = assemblyDefinition.MainModule.GetType(type.FullName); PropertyDefinition dependentPropertyDefinition = typeDefinition.Properties.FirstOrDefault(definition => definition.Name == dependentProperty.Name); bool found = false; if (dependentPropertyDefinition != null) { MethodDefinition definition = dependentPropertyDefinition.GetMethod; if (definition.HasBody) { ILProcessor processor = definition.Body.GetILProcessor(); int idx = 0; while (!found && idx < processor.Body.Instructions.Count) { Instruction instruction = processor.Body.Instructions[idx]; MethodDefinition methodDefinition; if (instruction.OpCode == OpCodes.Call && (methodDefinition = instruction.Operand as MethodDefinition) != null && methodDefinition.DeclaringType.IsAssignableFrom(typeDefinition) && methodDefinition.Name == string.Format(CultureInfo.InvariantCulture, "get_{0}", propertyInfo.Name)) { found = true; } else { idx++; } } } } return found; } /*...*/ public static IEnumerable<PropertyInfo> GetDependentPropertiesFrom(this Type type, PropertyInfo property) { List<PropertyInfo> dependentPropertyInfos = type.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(dependentProperty => property != dependentProperty && dependentProperty.CanRead && type.ExistPropertyDependencyBetween(dependentProperty, property)).ToList(); for (int i = 0; i < dependentPropertyInfos.Count; i++) { foreach (PropertyInfo info in type.GetDependentPropertiesFrom(dependentPropertyInfos[i])) { if (!dependentPropertyInfos.Contains(info)) { dependentPropertyInfos.Add(info); } } } return dependentPropertyInfos; }just like this:
public static void RaiseNotifyPropertyChanged(this object instance, PropertyInfo property) { FieldInfo propertyChangedEvent; if (instance.GetType().TryGetPropertyChangedField(out propertyChangedEvent)) { var propertyChangedEventMulticastDelegate = (MulticastDelegate)propertyChangedEvent.GetValue(@this); var invocationList = propertyChangedEventMulticastDelegate.GetInvocationList(); foreach (var handler in invocationList) { MethodInfo methodInfo = handler.GetMethodInfo(); methodInfo.Invoke(handler.Target, new[] { instance, new PropertyChangedEventArgs(property.Name) }); } foreach (PropertyInfo propertyInfo in instance.GetType().GetDependentPropertiesFrom(property)) { foreach (var handler in invocationList) { MethodInfo methodInfo = handler.GetMethodInfo(); methodInfo.Invoke(handler.Target, new[] { instance, new PropertyChangedEventArgs(propertyInfo.Name) }); } } } }
So, now the program output is:
Property Changed => 'FirstName' = 'Igr Alexánder'
Property Changed => 'FullName' = ' Igr Alexánder'
Property Changed => 'LastName' = 'Fernández Saúco'
Property Changed => 'FullName' = ' Igr Alexánder Fernández Saúco'
Conclusion
At this point, you should be worried about the performance and a lot of reflection API calls. I'm pretty sure that this issue could be handle with the right caching approach ;).
I didn’t know why I never heard about SheepAspect before. Probably no one trust in something called “Sheep”, but believe me SheepAspect rocks!