Реализация INotifyPropertyChanged с PostSharp 1.5 - PullRequest
2 голосов
/ 12 марта 2010

Я новичок в .NET и WPF, поэтому я надеюсь, что задам вопрос правильно. Я использую INotifyPropertyChanged, реализованный с использованием 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));
    }
}

}

Тогда у меня есть пара классов (пользователь и адрес), которые реализуют [NotifyPropertyChanged]. Работает нормально. Но то, что я хочу, это то, что, если дочерний объект изменяется (в моем примере адреса), родительский объект получает уведомление (в моем случае пользователь). Можно ли было бы расширить этот код, чтобы он автоматически создавал прослушиватели родительских объектов, которые прослушивают изменения в своих дочерних объектах?

Ответы [ 2 ]

3 голосов
/ 14 марта 2010

Я не уверен, что это работает в 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;
    }

Раньше это работало только тогда, когда в свойстве был установщик (даже если только частный установщик). Если бы у собственности был только получатель, вы не получили бы никакого уведомления. Обратите внимание, что это по-прежнему обеспечивает только один уровень уведомлений (он не будет уведомлять вас о каких-либо изменениях какого-либо объекта в иерархии.) Вы можете сделать что-то подобное, вручную установив для каждой реализации OnChildPropertyChanged pulse значение OnPropertyChanged со значением (null) для имя свойства, фактически позволяющее считать любое изменение в дочернем элементе общим изменением в родительском. Однако это может создать большую неэффективность при связывании данных, поскольку может привести к переоценке всех связанных свойств.

1 голос
/ 12 марта 2010

Я бы подошел к этому как к реализации другого интерфейса, например, INotifyOnChildChanges, с единственным методом, который соответствует PropertyChangedEventHandler. Затем я бы определил другой аспект, который связывает событие PropertyChanged с этим обработчиком.

На этом этапе любой класс, в котором реализованы INotifyPropertyChanged и INotifyOnChildChanges, получит уведомление об изменениях дочерних свойств.

Мне нравится эта идея, и, возможно, мне придется реализовать ее самостоятельно. Обратите внимание, что я также нашел довольно много обстоятельств, когда я хочу запустить PropertyChanged за пределами набора свойств (например, если свойство на самом деле является вычисляемым значением, и вы изменили один из компонентов), поэтому перенос фактического вызова на PropertyChanged в базовый класс, вероятно, является оптимальным. Я использую лямбда-решение для обеспечения безопасности типов , что кажется довольно распространенной идеей.

...