Реализация INotifyPropertyChanged с PostSharp 1.5

Я новичок в .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]. Это работает нормально. Но я хочу, чтобы при изменении дочернего объекта (в моем примере адреса) родительский объект получал уведомление (в моем случае пользователь). Можно ли расширить этот код, чтобы он автоматически создавал прослушиватели для родительских объектов, которые прослушивают изменения в своих дочерних объектах?


person no9    schedule 12.03.2010    source источник
comment
Что бы вы хотели, чтобы ребенок-слушатель делал?   -  person Dan Bryant    schedule 14.03.2010
comment
На самом деле на данный момент все, что я хочу, это чтобы Родитель был уведомлен (о любых изменениях в дочернем элементе - любой глубине).   -  person no9    schedule 15.03.2010
comment
Это гораздо более сложная проблема. Вам придется использовать какое-то отражение (если вы не можете полагаться на своих детей, чтобы они уведомляли вас об изменениях в их детях), и всегда немного рискованно решать, как и когда рекурсировать во время отражения. Какая мотивирующая проблема привела вас к этому решению? Возможны изменения в дизайне, которые помогут упростить вашу задачу.   -  person Dan Bryant    schedule 15.03.2010
comment
Кстати, было бы хорошо, если бы этот пост можно было пометить как PostSharp. Создатель инструмента читает эти форумы и сможет ответить на ваш вопрос о версии 1.5 лучше, чем я.   -  person Dan Bryant    schedule 15.03.2010
comment
я пометил это тегом PostSharp, а также опубликовал на форуме PostSharp ... теперь я жду ответа .... заставить это работать было бы очень важно. Потеря всего кода NPC и поддержание чистоты классов - моя мечта сбылась!   -  person no9    schedule 17.03.2010


Ответы (2)


Я не уверен, работает ли это в версии 1.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 пульсировать OnPropertyChanged с (null) для имени свойства, что фактически позволяет любому изменению в дочернем элементе считаться общим изменением в родительском. Однако это может значительно снизить эффективность привязки данных, поскольку может привести к повторной оценке всех связанных свойств.

person Dan Bryant    schedule 14.03.2010
comment
извините, но это не работает в 1.5. Мне не хватает атрибутов ImportMember и MethodPointCut: S - person no9; 15.03.2010
comment
я также пробовал это на PostSharp 2.0 (но все же моя главная цель - сделать это на 1.5). Тем не менее, у меня не было никакого успеха даже на 2.0 с ним. Событие на родительском объекте никогда не запускается. - person no9; 15.03.2010
comment
Я проверил это в 2.0. Моя IMainViewModel выставила свойство WindowTitle, а базовый класс реализовал INotifyPropertyChanged. Я установил значение WindowTitle после того, как был создан экземпляр моей модели WiringListViewModel, и я мог видеть текст Debug, указывающий, что OnChildPropertyChanged был вызван с помощью MainViewModel. - person Dan Bryant; 15.03.2010
comment
не могу найти проблему. Я создал простой тест, но OnChildPropertyChanged никогда не вызывается: S ... если вам интересно, я могу отправить вам свой тестовый проект, чтобы вы могли помочь мне найти ошибку - person no9; 17.03.2010
comment
Пожалуйста, пришлите мне свой тестовый проект на [email protected]. Сейчас я использую этот аспект в одном из своих проектов, поэтому было бы неплохо посмотреть, есть ли случай сбоя, который я не обнаружил. Я согласен, использование аспектов действительно помогает поддерживать чистоту; теперь я могу пользоваться всей магией привязки WPF без утомительных (и подверженных ошибкам) ​​накладных расходов на ручную реализацию шаблона Notify. - person Dan Bryant; 17.03.2010

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

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

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

person Ben Von Handorf    schedule 12.03.2010
comment
Из-за моего длинного комментария пришлось опубликовать его как ответ. Если вы найдете время, я был бы очень рад реализовать вашу идею ... ofcors с вашей помощью :) - person no9; 15.03.2010