Implementing INotifyPropertyChanged with PostSharp 1.5
Im new to .NET and WPF so i hope i will ask the question correctly. I am using INotifyPropertyChanged implemented using PostSharp 1.5:
[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 });
}
}
}
[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;
}
}
[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 =开发者_如何学Go 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);
}
}
[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));
}
}
}
Then i have a couple of classes (user and adress) that implement [NotifyPropertyChanged]. It works fine. But what i want would be that if the child object changes (in my example address) that the parent object gets notified (in my case user). Would it be possible to expand this code so it automaticly creates listeners on parent objects that listen for changes in its child objets?
I'm not sure if this works in v1.5, but this works in 2.0. I've done only basic testing (it fires the method correctly), so use at your own risk.
/// <summary>
/// Aspect that, when applied to a class, registers to receive notifications when any
/// child properties fire NotifyPropertyChanged. This requires that the class
/// implements a method OnChildPropertyChanged(Object sender, PropertyChangedEventArgs e).
/// </summary>
[Serializable]
[MulticastAttributeUsage(MulticastTargets.Class,
Inheritance = MulticastInheritance.Strict)]
public class OnChildPropertyChangedAttribute : InstanceLevelAspect
{
[ImportMember("OnChildPropertyChanged", IsRequired = true)]
public PropertyChangedEventHandler OnChildPropertyChangedMethod;
private IEnumerable<PropertyInfo> SelectProperties(Type type)
{
const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public;
return from property in type.GetProperties(bindingFlags)
where property.CanWrite && typeof(INotifyPropertyChanged).IsAssignableFrom(property.PropertyType)
select property;
}
/// <summary>
/// Method intercepting any call to a property setter.
/// </summary>
/// <param name="args">Aspect arguments.</param>
[OnLocationSetValueAdvice, MethodPointcut("SelectProperties")]
public void OnPropertySet(LocationInterceptionArgs args)
{
if (args.Value == args.GetCurrentValue()) return;
var current = args.GetCurrentValue() as INotifyPropertyChanged;
if (current != null)
{
current.PropertyChanged -= OnChildPropertyChangedMethod;
}
args.ProceedSetValue();
var newValue = args.Value as INotifyPropertyChanged;
if (newValue != null)
{
newValue.PropertyChanged += OnChildPropertyChangedMethod;
}
}
}
Usage is like this:
[NotifyPropertyChanged]
[OnChildPropertyChanged]
class WiringListViewModel
{
public IMainViewModel MainViewModel { get; private set; }
public WiringListViewModel(IMainViewModel mainViewModel)
{
MainViewModel = mainViewModel;
}
private void OnChildPropertyChanged(Object sender, PropertyChangedEventArgs e)
{
if (sender == MainViewModel)
{
Debug.Print("Child is changing!");
}
}
}
This will apply to all child properties of the class that implement INotifyPropertyChanged. If you want to be more selective, you could add another simple Attribute (such as [InterestingChild]) and use the presence of that attribute in the MethodPointcut.
I discovered a bug in the above. The SelectProperties method should be changed to:
private IEnumerable<PropertyInfo> SelectProperties(Type type)
{
const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public;
return from property in type.GetProperties(bindingFlags)
where typeof(INotifyPropertyChanged).IsAssignableFrom(property.PropertyType)
select property;
}
Previously, it would only work when the property had a setter (even if only a private setter). If the property only had a getter, you would not get any notification. Note that this still only provides a single level of notification (it won't notify you of any change of any object in the hierarchy.) You could accomplish something like this by manually having each implementation of OnChildPropertyChanged pulse an OnPropertyChanged with (null) for the property name, effectively letting any change in a child be considered an overall change in the parent. This could create a lot of inefficiency with data binding, however, as it may cause all bound properties to be reevaluated.
The way I would approach this would be to implement another interface, something like INotifyOnChildChanges
, with a single method on it that matches the PropertyChangedEventHandler
. I would then define another Aspect that wires up the PropertyChanged
event to this handler.
At this point, any class that implemented both INotifyPropertyChanged
and INotifyOnChildChanges
would get notified of child property changes.
I like this idea and may have to implement it myself. Note that I have also found a fair number of circumstances where I want to fire PropertyChanged
outside of a property set (e.g. if the property is actually a calculated value and you have changed one of the components), so wrapping the actual call to PropertyChanged
into a base class is probably optimal. I use a lambda based solution to ensure type safety, which seems to be a pretty common idea.
精彩评论