For the last week, most WPF disciples are discussing how to get rid of hardcoded property name string inside INotifyPropertyChanged implementation and how to keep using automatic properties implementation but keep WPF binding working. The thread was started by Karl Shifflett, who proposed interesting method of using StackFrame for this task. During this thread, other methods were proposed including code snippets, R#, Observer Pattern, Cinch framework, Static Reflection, Weak References and others. I also proposed the method we’re using for our classes and promised to blog about it. So the topic today is how to use PostSharp to wire automatic implementation of INotifyPropertyChanged interface
based on automatic setters only.
So, I want my code to look like this:
public class AutoWiredSource {
public double MyProperty { get; set; }
public double MyOtherProperty { get; set; }
}
while being fully noticeable about any change in any property and enables me to bind to those properties.
<StackPanel DataContext="{Binding Source={StaticResource source}}">
<Slider Value="{Binding Path=MyProperty}" />
<Slider Value="{Binding Path=MyProperty}" />
</StackPanel>
How to achieve it? How to make compiler replace my code with the following?
private double _MyProperty;
public double MyProperty {
get { return _MyProperty; }
set {
if (value != _MyProperty) {
_MyProperty = value; OnPropertyChanged("MyProperty");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
internal void OnPropertyChanged(string propertyName) {
if (string.IsNullOrEmpty(propertyName)) throw new ArgumentNullException(
"propertyName");
var handler = PropertyChanged as PropertyChangedEventHandler;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
Simple: to use aspect oriented programming to inject set of instructions into pre-compiled source.
First of all, we have to build some attribute which will be used for marking classes that require change tracking. This attribute should be combined (compound) aspect to include all aspects used for change tracking. All we’re doing here is to get all set
methods to add composition aspect to:
[Serializable, DebuggerNonUserCode, AttributeUsage(
AttributeTargets.Assembly | AttributeTargets.Class, AllowMultiple = false,
Inherited = false),
MulticastAttributeUsage(MulticastTargets.Class, AllowMultiple = false,
Inheritance = MulticastInheritance.None, AllowExternalAssemblies = true)]
public sealed class NotifyPropertyChangedAttribute : CompoundAspect {
public int AspectPriority { get; set; }
public override void ProvideAspects(object element,
LaosReflectionAspectCollection collection) {
Type targetType = (Type)element;
collection.AddAspect(targetType, new PropertyChangedAspect {
AspectPriority = AspectPriority });
foreach (var info in targetType.GetProperties(
BindingFlags.Public | BindingFlags.Instance).Where(
pi => pi.GetSetMethod() != null)) {
collection.AddAspect(info.GetSetMethod(), new NotifyPropertyChangedAspect(
info.Name) { AspectPriority = AspectPriority });
}
}
}
Next aspect is change tracking composition aspect. Which is used for marking only:
[Serializable]
internal sealed class PropertyChangedAspect : CompositionAspect {
public override object CreateImplementationObject(
InstanceBoundLaosEventArgs eventArgs) {
return new PropertyChangedImpl(eventArgs.Instance);
}
public override Type GetPublicInterface(Type containerType) {
return typeof(INotifyPropertyChanged);
}
public override CompositionAspectOptions GetOptions() {
return CompositionAspectOptions.GenerateImplementationAccessor;
}
}
And the next which is the most interesting one, we will put onto method boundary for tracking. There are some highlights here. First, we do not want to fire PropertyChanged
event if the actual value did not changed, thus we’ll handle the method on its entry and on its exit for check.
[Serializable]
internal sealed class NotifyPropertyChangedAspect : OnMethodBoundaryAspect {
private readonly string _propertyName;
public NotifyPropertyChangedAspect(string propertyName) {
if (string.IsNullOrEmpty(propertyName)) throw new ArgumentNullException(
"propertyName");
_propertyName = propertyName;
}
public override void OnEntry(MethodExecutionEventArgs eventArgs) {
var targetType = eventArgs.Instance.GetType();
var setSetMethod = targetType.GetProperty(_propertyName);
if (setSetMethod == null) throw new AccessViolationException();
var oldValue = setSetMethod.GetValue(eventArgs.Instance,null);
var newValue = eventArgs.GetReadOnlyArgumentArray()[0];
if (oldValue == newValue) eventArgs.FlowBehavior = FlowBehavior.Return;
}
public override void OnSuccess(MethodExecutionEventArgs eventArgs) {
var instance = eventArgs.Instance as IComposed<INotifyPropertyChanged>;
var imp = instance.GetImplementation(
eventArgs.InstanceCredentials) as PropertyChangedImpl;
imp.OnPropertyChanged(_propertyName);
}
}
We're almost done, all we have to do is to create a class which implements INotifyPropertyChanged
with internal method to useful call:
[Serializable]
internal sealed class PropertyChangedImpl : INotifyPropertyChanged {
private readonly object _instance;
public PropertyChangedImpl(object instance) {
if (instance == null) throw new ArgumentNullException("instance");
_instance = instance;
}
public event PropertyChangedEventHandler PropertyChanged;
internal void OnPropertyChanged(string propertyName) {
if (string.IsNullOrEmpty(propertyName)) throw new ArgumentNullException(
"propertyName");
var handler = PropertyChanged as PropertyChangedEventHandler;
if (handler != null) handler(_instance, new PropertyChangedEventArgs(propertyName));
}
}
We're done. The last thing is to reference to PostSharp Laos and Public assemblies and mark compiler to use Postsharp targets (inside your project file (*.csproj)
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<DontImportPostSharp>True</DontImportPostSharp>
</PropertyGroup>
<Import Project="PostSharp\PostSharp-1.5.targets" />
Now we're done. We can use clear syntax like the following to make all our properties having public
setter to be traceable. The only disadvantage is that you’ll have to drag two PostSharp files with your project. But after all, it is much more convenient than manual notify change tracking all over your project.
[NotifyPropertyChanged]
public class AutoWiredSource {
public double MyProperty { get; set; }
}
Have a nice day and be good people. Also try to think what other extremely useful things can be done with PostSharp (or any other aspect oriented engine).
Related Posts
- Nifty time savers for WPF development
- Set binding, based on trigger
- Auto scroll ListBox in WPF