使用PostSharp 1.5实现INotifyPropertyChanged

时间:2010-03-12 12:30:15

标签: c# wpf inotifypropertychanged postsharp

我是.NET和WPF的新手,所以我希望我会正确地提出这个问题。 我正在使用PostSharp 1.5实现的INotifyPropertyChanged:

[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));
    }
}

}

然后我有几个实现[NotifyPropertyChanged]的类(用户和地址)。 它工作正常。但我想要的是,如果子对象更改(在我的示例地址中)父对象得到通知(在我的情况下是用户)。是否可以扩展此代码,以便在父对象上自动创建侦听器,以侦听其子对象中的更改?

2 个答案:

答案 0 :(得分:3)

我不确定这是否适用于v1.5,但这适用于2.0。我只进行了基本测试(它正确地激发了方法),因此使用风险自负。

/// <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;
        }
    }
}

用法是这样的:

[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!");
        }
    }
}

这将适用于实现INotifyPropertyChanged的类的所有子属性。如果您想要更具选择性,可以添加另一个简单属性(例如[InterestingChild])并在MethodPointcut中使用该属性的存在。


我在上面发现了一个错误。 SelectProperties方法应更改为:

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;
    }

以前,它只会在属性有一个setter时工作(即使只是一个私有的setter)。如果酒店只有吸气剂,您将不会收到任何通知。请注意,这仍然只提供单级别的通知(它不会通知您层次结构中任何对象的任何更改。)您可以通过手动将OnChildPropertyChanged脉冲的每个实现与OnPropertyChanged一起使用(null)来完成类似的操作。属性名称,有效地让孩子的任何变化都被视为父母的整体变化。但是,这可能会导致数据绑定效率低下,因为它可能会导致重新评估所有绑定属性。

答案 1 :(得分:1)

我接近这个的方法是实现另一个接口,比如INotifyOnChildChanges,其上有一个与PropertyChangedEventHandler匹配的方法。然后我将定义另一个Aspect,它将PropertyChanged事件连接到此处理程序。

此时,任何同时实现INotifyPropertyChangedINotifyOnChildChanges的类都会收到有关子属性更改的通知。

我喜欢这个想法,可能必须自己实施。请注意,我还发现了很多情况,我想在属性集之外触发PropertyChanged(例如,如果属性实际上是一个计算值并且您已经更改了其中一个组件),那么包装实际调用PropertyChanged到基类可能是最佳的。 I use a lambda based solution to ensure type safety,这似乎是一个非常普遍的想法。