views:

364

answers:

3

Hello all.

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 = 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?

+1  A: 

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.

Ben Von Handorf
Due to my long comment had to post it as an anwser. If you find the time i would be most excited to implement your idea ... ofcors with your assistance :)
no9
A: 

Your aproach sounds promising, altho i feel like i do not have the skills to implement it. I was thinking about it alot and i figured out i would have to consider only properties that implement a setter in the first place. I guess reflection would come in handy, but again i dont know where to start :) Another thing is that i would need this to work on any level. Lets say i have implemented the aspect on class user that has a child class on property address and lets say the address has another child that has couple of properties. If i think about it, there would need to be a recursion used somewhere ... again i had no success withi this... If you find this idea interesting and i can see you posses the skills to do it i would like to encourage you to help me with this matter. I am stalled with my application because i feel that this is an important thing to have. And yet i do not understand that this hasnt beem implemented yet. I came to .NET only a while ago from Smalltalk and im confused with what you might consider the basics.

no9
+2  A: 

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.

Dan Bryant
sorry, but this does not work in 1.5. Im missing ImportMember and MethodPointCut attribute :S
no9
i have also tried this on PostSharp 2.0 (but still my main goal is to do this on 1.5). Yet i did not have any success even on 2.0 with it. The event on parent object never fires up.
no9
I have verified it in 2.0. My IMainViewModel exposed a property WindowTitle and the underlying class implemented INotifyPropertyChanged. I set the value of WindowTitle after my WiringListViewModel has been instantiated and I could see the Debug text print indicating that the OnChildPropertyChanged had been called with MainViewModel.
Dan Bryant
I cant find the problem. I created a simple test, yet OnChildPropertyChanged never gets called :S ... if you are interested i could send you my test project so you can help me find an error
no9
Please do send me your test project at [email protected]. I'm using this aspect in one of my projects now, so it would be good to see if there is a failure case I haven't discovered. I agree, using aspects really does help with keeping things clean; now I can have all the magic of WPF binding without the tedious (and error-prone) overhead of manually implementing the Notify pattern.
Dan Bryant