2010-03-12 6 views
2

Je suis nouveau sur .NET et WPF alors j'espère que je vais poser la question correctement. J'utilise INotifyPropertyChanged mis en œuvre à l'aide PostSharp 1.5:Implémentation de INotifyPropertyChanged avec 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)); 
    } 
} 

}

Alors j'ai deux ou trois classes (utilisateur et adresse) qui mettent en œuvre [NotifyPropertyChanged]. Cela fonctionne très bien. Mais ce que je veux, ce serait que si l'objet enfant change (dans mon exemple d'adresse) que l'objet parent soit averti (dans mon cas user). Serait-il possible d'étendre ce code afin qu'il crée automatiquement des écouteurs sur les objets parents qui écoutent les modifications de ses objets enfants?

+0

Qu'aimeriez-vous que l'auditeur de l'enfant fasse? –

+0

Actuellement, tout ce que je veux, c'est que Parent soit averti (de tout changement sur un enfant - n'importe quelle profondeur). – no9

+0

C'est un problème beaucoup plus difficile.Vous devrez utiliser une sorte de réflexion (si vous ne pouvez pas compter sur vos enfants pour vous informer des changements concernant leurs enfants) et il est toujours un peu hasardeux de décider comment et quand se recourber pendant la réflexion. Quel est le problème motivant qui vous conduit à cette solution? Il peut y avoir des modifications de conception qui pourraient vous aider à simplifier votre tâche. –

Répondre

1

La façon dont j'aborderais cela serait d'implémenter une autre interface, quelque chose comme INotifyOnChildChanges, avec une seule méthode qui correspond au PropertyChangedEventHandler. Je définirais ensuite un autre aspect qui relierait l'événement PropertyChanged à ce gestionnaire.

À ce stade, toute classe qui a implémenté à la fois INotifyPropertyChanged et INotifyOnChildChanges recevrait une notification des modifications de propriété enfant. J'aime cette idée et je pourrais devoir la mettre en œuvre moi-même. Notez que j'ai également trouvé un bon nombre de circonstances où je veux tirer PropertyChanged en dehors d'un ensemble de propriétés (par exemple si la propriété est réellement une valeur calculée et que vous avez changé l'un des composants), enveloppant ainsi l'appel réel en PropertyChanged une classe de base est probablement optimale. I use a lambda based solution to ensure type safety, ce qui semble être une idée assez commune.

+0

En raison de mon long commentaire a dû l'afficher comme une réponse. Si vous trouvez le temps que je serais le plus heureux de mettre en œuvre votre idée ... ofcors avec votre aide :) – no9

3

Je ne sais pas si cela fonctionne dans la version 1.5, mais cela fonctionne dans 2.0. Je n'ai fait que des tests de base (il déclenche la méthode correctement), utilisez-le donc à vos risques et périls.

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

L'utilisation est comme ceci:

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

Cela s'appliquera à toutes les propriétés de l'enfant de la classe qui mettent en œuvre INotifyPropertyChanged. Si vous souhaitez être plus sélectif, vous pouvez ajouter un autre attribut simple (tel que [InterestingChild]) et utiliser la présence de cet attribut dans MethodPointcut.


J'ai découvert un bug dans ce qui précède. La méthode SelectProperties doit être remplacé par:

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

Auparavant, il ne fonctionne que lorsque la propriété avait un setter (même si seulement un setter privé). Si la propriété n'avait qu'un getter, vous n'obtiendriez aucune notification. Notez que cela ne fournit qu'un seul niveau de notification (il ne vous informera d'aucun changement d'objet dans la hiérarchie.) Vous pouvez accomplir quelque chose comme ceci en ayant manuellement chaque implémentation de OnChildPropertyChanged pulsée OnPropertyChanged avec (null) pour le nom de la propriété, en laissant effectivement tout changement dans un enfant être considéré comme un changement global dans le parent. Cependant, cela peut créer beaucoup d'inefficacité avec la liaison de données, car cela peut entraîner la réévaluation de toutes les propriétés liées.

+0

désolé, mais cela ne fonctionne pas dans la version 1.5. Im missing ImportMember et MethodPointCut: S – no9

+0

J'ai aussi essayé ceci sur PostSharp 2.0 (mais mon objectif principal est de le faire sur 1.5). Pourtant, je n'ai pas eu de succès, même sur 2.0 avec elle. L'événement sur l'objet parent ne se déclenche jamais. – no9

+0

Je l'ai vérifié en 2.0. Mon IMainViewModel a exposé une propriété WindowTitle et la classe sous-jacente a implémenté INotifyPropertyChanged. Je définis la valeur de WindowTitle après que mon WiringListViewModel a été instancié et que j'ai pu voir l'impression de texte de débogage indiquant que OnChildPropertyChanged avait été appelé avec MainViewModel. –

Questions connexes