2010-11-10 22 views
26

ACTUALIZACIÓN: Problema resuelto, consulte my Answer.Suscribirse a INotifyPropertyChanged para anidados (secundarios) los objetos

Estoy buscando un lugar limpio y solución elegante para controlar el evento INotifyPropertyChanged de objetos (niño) anidados. Código de ejemplo:

public class Person : INotifyPropertyChanged { 

    private string _firstName; 
    private int _age; 
    private Person _bestFriend; 

    public string FirstName { 
    get { return _firstName; } 
    set { 
     // Short implementation for simplicity reasons 
     _firstName = value; 
     RaisePropertyChanged("FirstName"); 
    } 
    } 

    public int Age { 
    get { return _age; } 
    set { 
     // Short implementation for simplicity reasons 
     _age = value; 
     RaisePropertyChanged("Age"); 
    } 
    } 

    public Person BestFriend { 
    get { return _bestFriend; } 
    set { 
     // - Unsubscribe from _bestFriend's INotifyPropertyChanged Event 
     // if not null 

     _bestFriend = value; 
     RaisePropertyChanged("BestFriend"); 

     // - Subscribe to _bestFriend's INotifyPropertyChanged Event if not null 
     // - When _bestFriend's INotifyPropertyChanged Event is fired, i'd like 
     // to have the RaisePropertyChanged("BestFriend") method invoked 
     // - Also, I guess some kind of *weak* event handler is required 
     // if a Person instance i beeing destroyed 
    } 
    } 

    // **INotifyPropertyChanged implementation** 
    // Implementation of RaisePropertyChanged method 

} 

Enfoque a la BestFriend propiedad y su valor colocador. Ahora Sé que podría hacerlo manualmente, implementando todos los pasos descritos en los comentarios. Pero esto va a ser un montón de código, especialmente cuando estoy planeando tener muchas propiedades secundarias implementando INotifyPropertyChanged de esta manera. Por supuesto, no van a ser siempre del mismo tipo, lo único que tienen en común es la interfaz INotifyPropertyChanged.

La razón es que, en mi situación real, tengo un objeto "Artículo" complejo (en carrito) que tiene propiedades de objeto anidadas en varias capas (El artículo tiene un objeto "Licencia", que puede tener objetos secundarios de nuevo) y necesito que me notifiquen sobre cualquier cambio individual del "Artículo" para poder volver a calcular el precio.

¿Tiene algunos buenos consejos o incluso algunos implementación para ayudarme a resolver esto?

Desafortunadamente, no puedo/puedo usar pasos posteriores a la compilación como PostSharp para lograr mi objetivo.

Muchas gracias por adelantado,
- Thomas

+0

AFAIK, la mayoría de las implementaciones de enlace no * esperan * que el evento se propague de esa manera. * No * has * cambiado el valor de 'BestFriend', después de todo. –

Respuesta

19

ya que no pude encontrar una solución lista para usar, hice una implementación personalizada basada en las sugerencias de Pieters (y Marks) (¡gracias!).

Usando las clases, usted será notificado de cualquier cambio en un árbol de objetos profundo, esto funciona para cualquier INotifyPropertyChanged Tipos de aplicación y INotifyCollectionChanged colecciones * ejecución (Obviamente, estoy usando el ObservableCollection para eso).

Espero que esta sea una solución bastante limpia y elegante, aunque no está completamente probada y hay espacio para mejoras. Es bastante fácil de usar, basta con crear una instancia de ChangeListener utilizando su Create método estático y pasar su INotifyPropertyChanged:

var listener = ChangeListener.Create(myViewModel); 
listener.PropertyChanged += 
    new PropertyChangedEventHandler(listener_PropertyChanged); 

la PropertyChangedEventArgs proporcionan una PropertyName que será siempre el "camino" lleno de sus objetos. Por ejemplo, si cambia el nombre de "BestFriend" de su Persona, el PropertyName será "BestFriend.Name", si el BestFriend tiene una colección de Children y usted cambia su edad, el valor será "BestFriend.Children []. Age" y así. No se olvide de Dispose cuando se destruye su objeto, entonces (con suerte) se dará de baja completamente de todos los oyentes del evento.

Compila en .NET (probado en 4) y Silverlight (probado en 4). Debido a que el código en separó en tres clases, que acabo de publicar el código para gist 705450 donde podrá tomar todo: https://gist.github.com/705450 **

*) Una de las razones de que el código está trabajando es que el ObservableCollection también implementa INotifyPropertyChanged, de lo contrario no funcionaría como se desee, esto es una advertencia conocida

**) utilizar de forma gratuita, publicada bajo MIT License

+0

muy útil. thx :) – Marco

+1

Me tomé la libertad de incluir tu esencia en un paquete nuget: https://www.nuget.org/packages/RecursiveChangeNotifier/ – LOST

+0

Gracias @LOST, espero que ayude a alguien – thmshd

16

Creo que lo que estamos buscando es algo así como la unión de WPF.

Cómo INotifyPropertyChanged obras es imprescindible que el RaisePropertyChanged("BestFriend");única ser fored cuando la propiedad BestFriend cambios. No cuando algo en el objeto mismo cambia.

Cómo implementar esto es mediante un controlador de eventos de dos pasos INotifyPropertyChanged. Su oyente se registraría en el evento cambiado del Person. Cuando el BestFriend se establece/cambia, se registra en el evento cambiado del BestFriendPerson. Luego, comienzas a escuchar eventos cambiados de ese objeto.

Así es exactamente como WPF binding implementa esto. La escucha de cambios de objetos anidados se realiza a través de ese sistema.

La razón por la que esto no va a funcionar cuando lo implementa en Person es que los niveles pueden llegar a ser muy profundos y el evento cambiado de BestFriend ya no significa nada ("¿qué ha cambiado?"). Este problema aumenta cuando tiene relaciones circulares donde, por ejemplo, el mejor amigo de tu monstruo es la madre de tu mejor enemigo. Luego, cuando una de las propiedades cambia, obtienes un desbordamiento de la pila.

Por lo tanto, la forma en que resolvería esto es crear una clase con la que puede construir oyentes. Por ejemplo, construiría un oyente en BestFriend.FirstName.Esa clase luego pondría un controlador de eventos en el evento cambiado de Person y escuchará los cambios en BestFriend. Luego, cuando eso cambie, pone un oyente en BestFriend y escucha los cambios de FirstName. Luego, cuando eso cambie, envía un evento y puedes escucharlo. Eso es básicamente cómo funciona el enlace de WPF.

Consulte http://msdn.microsoft.com/en-us/library/ms750413.aspx para obtener más información sobre el enlace WPF.

+0

Gracias por su respuesta, realmente no estaba al tanto de algunos problemas que describió. Actualmente, solo ** cancelar la suscripción ** a Eventos está causando algunos dolores de cabeza. Por ejemplo, 'BestFriend' podría establecerse en' null'. ¿Es realmente posible anular la suscripción de esa manera, sin tener implementado un tipo de 'INotifyPropertyChanging'? – thmshd

+0

Cuando el oyente (el objeto que describo) obtiene la primera referencia a 'Persona', copia la referencia a' BestFriend' y registra un oyente a esa referencia. Si el 'BestFriend' cambia (por ejemplo, a' nulo'), primero desconecta el evento de la referencia copiada, copia la nueva referencia (posiblemente 'null') y registra el manejador de eventos en eso (si no es' nulo'). El truco aquí es que absolutamente necesitas copiar la referencia a tu oyente en lugar de usar la propiedad 'BestFriend' de' Persona'. Eso debería resolver tus problemas. –

+0

Muy bien, intentaré implementar esto y publicar una solución cuando haya terminado. +1 voto al menos para ti =) – thmshd

3

solución interesante Thomas.

Encontré otra solución. Se llama patrón de diseño de propagador. Puede encontrar más información en la web (por ejemplo, en CodeProject: Propagator in C# - An Alternative to the Observer Design Pattern).

Básicamente, es un patrón para actualizar objetos en una red de dependencia. Es muy útil cuando los cambios de estado deben ser empujados a través de una red de objetos. Un cambio de estado está representado por un objeto en sí que viaja a través de la red de Propagadores. Al encapsular el cambio de estado como un objeto, los Propagadores se acoplan libremente.

Un diagrama de clases de las clases reutilizables propagador:

A class diagram of the re-usable Propagator classes

Leer más en CodeProject.

+0

+1 Gracias por tu contribución, ' ll seguramente echar un vistazo más de cerca – thmshd

0

Escribí una ayuda fácil para hacer esto. Simplemente llame a BubblePropertyChanged (x => x.BestFriend) en su modelo de vista padre. nótese bien.hay una suposición de que tiene un método llamado NotifyPropertyChagned en su padre, pero puede adaptarlo.

 /// <summary> 
    /// Bubbles up property changed events from a child viewmodel that implements {INotifyPropertyChanged} to the parent keeping 
    /// the naming hierarchy in place. 
    /// This is useful for nested view models. 
    /// </summary> 
    /// <param name="property">Child property that is a viewmodel implementing INotifyPropertyChanged.</param> 
    /// <returns></returns> 
    public IDisposable BubblePropertyChanged(Expression<Func<INotifyPropertyChanged>> property) 
    { 
     // This step is relatively expensive but only called once during setup. 
     MemberExpression body = (MemberExpression)property.Body; 
     var prefix = body.Member.Name + "."; 

     INotifyPropertyChanged child = property.Compile().Invoke(); 

     PropertyChangedEventHandler handler = (sender, e) => 
     { 
      this.NotifyPropertyChanged(prefix + e.PropertyName); 
     }; 

     child.PropertyChanged += handler; 

     return Disposable.Create(() => { child.PropertyChanged -= handler; }); 
    } 
+0

He intentado agregar esto pero obtengo El nombre 'Desechable' no existe en este contexto. ¿Qué es desechable? –

+0

Desechable es una clase de ayuda concreta en extensiones reactivas que crea objetos concretos que implementan IDisposable con varios comportamientos. Sin embargo, puede eliminar ese código y manejar el evento desengancharse explícitamente si no desea aprender las alegrías si IDisposable en este momento (aunque vale la pena el esfuerzo). – DanH

0

He estado buscando en la web por un día y ahora me encontré otra solución agradable de Sacha Barber:

http://www.codeproject.com/Articles/166530/A-Chained-Property-Observer

Se crearon referencias débiles dentro de un encadenado Propiedad Observador. Consulte el artículo si desea ver otra forma excelente de implementar esta característica.

Y también quiero mencionar una buena aplicación con las extensiones reactivas @ http://www.rowanbeach.com/rowan-beach-blog/a-system-reactive-property-change-observer/

Solución Este trabajo sólo para un nivel de Observador, no una cadena completa de los observadores.

+0

Gracias por la actualización. La solución de Sacha es obviamente la más avanzada, aunque puedo recordar que la mía también funciona bien, de todos modos es un tema que no he tocado desde hace un tiempo :) – thmshd

0

Salida mi solución en CodeProject: http://www.codeproject.com/Articles/775831/INotifyPropertyChanged-propagator que hace exactamente lo que necesita - ayuda a propagar (en forma elegante) propiedades dependientes cuando las dependencias pertinentes en esta o cualquier modelos de vista anidados cambian:

public decimal ExchTotalPrice 
{ 
    get 
    { 
     RaiseMeWhen(this, has => has.Changed(_ => _.TotalPrice)); 
     RaiseMeWhen(ExchangeRate, has => has.Changed(_ => _.Rate)); 
     return TotalPrice * ExchangeRate.Rate; 
    } 
} 
Cuestiones relacionadas