2008-11-06 33 views
32

¿Hay una colección (BCL o de otro tipo) que tiene las siguientes características:ObservableCollection que también monitorea los cambios en los elementos de la colección

envía evento si se cambia recogida y envía evento si cualquiera de los elementos de la colección envía un evento PropertyChanged. Tipo de ObservableCollection<T> donde T: INotifyPropertyChanged y la colección también está supervisando los elementos para ver los cambios.

Podría envolver una colección observable y hacer el evento de suscripción/desuscripción cuando los elementos de la colección se agregan/eliminan, pero me preguntaba si alguna colección existente ya lo hizo.

Respuesta

43

Hicimos una rápida implementación a mí mismo:

public class ObservableCollectionEx<T> : ObservableCollection<T> where T : INotifyPropertyChanged 
{ 
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) 
    { 
     Unsubscribe(e.OldItems); 
     Subscribe(e.NewItems); 
     base.OnCollectionChanged(e); 
    } 

    protected override void ClearItems() 
    { 
     foreach(T element in this) 
      element.PropertyChanged -= ContainedElementChanged; 

     base.ClearItems(); 
    } 

    private void Subscribe(IList iList) 
    { 
     if (iList != null) 
     { 
      foreach (T element in iList) 
       element.PropertyChanged += ContainedElementChanged; 
     } 
    } 

    private void Unsubscribe(IList iList) 
    { 
     if (iList != null) 
     { 
      foreach (T element in iList) 
       element.PropertyChanged -= ContainedElementChanged; 
     } 
    } 

    private void ContainedElementChanged(object sender, PropertyChangedEventArgs e) 
    { 
     OnPropertyChanged(e); 
    } 
} 

admitió que sería un poco confuso y engañoso tener el fuego PropertyChanged en la colección cuando la propiedad que cambió realmente está en un elemento de contenido, pero sería se ajusta a mi propósito específico. Podría extenderse con un nuevo evento que se active dentro de ContainerElementChanged

¿Pensamientos?

EDIT: Debe tener en cuenta que el BCL ObservableCollection sólo expone la interfaz INotifyPropertyChanged través de una implementación explícita lo que sería necesario para proporcionar un molde con el fin de insertarse en el caso de este modo:

ObservableCollectionEx<Element> collection = new ObservableCollectionEx<Element>(); 
((INotifyPropertyChanged)collection).PropertyChanged += (x,y) => ReactToChange(); 

Edit2: manejo Añadido de ClearItems, gracias Josh

Edit3: añadido un darse de baja correcta para PropertyChanged, gracias Marcos

EDIT4: Wow, esto es realmente aprender-as-you-go :). KP notó que el evento se disparó con la colección como remitente y no con el elemento cuando el elemento a contenido cambia. Sugirió declarar un evento PropertyChanged en la clase marcada con new. Esto tendría un par de temas que voy a tratar de ilustrar con el ejemplo siguiente:

// work on original instance 
    ObservableCollection<TestObject> col = new ObservableCollectionEx<TestObject>(); 
    ((INotifyPropertyChanged)col).PropertyChanged += (s, e) => { Trace.WriteLine("Changed " + e.PropertyName); }; 

    var test = new TestObject(); 
    col.Add(test); // no event raised 
    test.Info = "NewValue"; //Info property changed raised 

    // working on explicit instance 
    ObservableCollectionEx<TestObject> col = new ObservableCollectionEx<TestObject>(); 
    col.PropertyChanged += (s, e) => { Trace.WriteLine("Changed " + e.PropertyName); }; 

    var test = new TestObject(); 
    col.Add(test); // Count and Item [] property changed raised 
    test.Info = "NewValue"; //no event raised 

se puede ver en la muestra que 'de primer orden' el evento tiene el efecto secundario que tiene que ser extremadamente cuidadoso de los cuales tipo de variable que utiliza al suscribirse al evento, ya que eso determina qué eventos recibe.

+0

Marcando esto como la respuesta como nada mejor (en mi opinión) ha surgido –

+4

Sin embargo, hay un problema con esta clase. Si llama a Clear(), el evento OnCollectionChanged recibirá una notificación de reinicio y no tendrá acceso a los elementos que se eliminaron de la colección. Esto puede mitigarse anulando ClearItems y anulando la suscripción a los manejadores antes de llamar a base.ClearItems(). – Josh

+0

Bien atrapado, Josh. He actualizado el código para reflejar esto. –

0

Echa un vistazo a C5 Generic Collection Library. Todas sus colecciones contienen eventos que puede usar para adjuntar devoluciones de llamadas cuando los elementos se agregan, eliminan, insertan, borran o cuando cambia la colección.

Estoy trabajando para algunas extensiones de esa biblioteca here que en el futuro cercano deberían permitir eventos de "vista previa" que podrían permitirle cancelar un agregar o cambiar.

3

Si desea utilizar algo incorporado en el marco, puede usar FreezableCollection. Entonces querrá escuchar el Changed event.

Se produce cuando se modifica el Freezable o un objeto que contiene.

Aquí hay una pequeña muestra. El método collection_Changed se llamará dos veces.

public partial class Window1 : Window 
{ 
    public Window1() 
    { 
     InitializeComponent(); 

     FreezableCollection<SolidColorBrush> collection = new FreezableCollection<SolidColorBrush>(); 
     collection.Changed += collection_Changed; 
     SolidColorBrush brush = new SolidColorBrush(Colors.Red); 
     collection.Add(brush); 
     brush.Color = Colors.Blue; 
    } 

    private void collection_Changed(object sender, EventArgs e) 
    { 
    } 
} 
+7

También debe tener en cuenta que FreezableCollections está restringido para contener solo los elementos heredados de DependencyObject. –

7

@ soren.enemaerke: habría hecho este comentario en su entrada respuesta, pero no puedo (No sé por qué, tal vez porque no tengo muchos puntos rep). De todos modos, solo pensé que mencionaría que en el código que publicaste no creo que el Unsubscribe funcione correctamente porque está creando una nueva lambda en línea y luego tratando de eliminar el controlador de eventos.

que cambiaría la adición/quitar líneas de controlador de eventos a algo como:

element.PropertyChanged += ContainedElementChanged; 

y

element.PropertyChanged -= ContainedElementChanged; 

Y a continuación, cambiar el método de firma ContainedElementChanged a:

private void ContainedElementChanged(object sender, PropertyChangedEventArgs e) 

Este reconocería que la eliminación es para el mismo controlador que el complemento y luego eliminarlo corr ectly. Espero que esto ayude a alguien :)

+1

Gracias Marcar, actualicé mi respuesta para reflejar sus ideas ... –

1

usaría ReactiveUI'sReactiveCollection:

reactiveCollection.Changed.Subscribe(_ => ...); 
0

@ soren.enemaerke Hecho esto una respuesta para poder escribir código adecuado según la sección de comentarios en su respuesta sería hacerla ilegible. El único problema que he tenido con la solución es que el elemento particular que desencadena el evento PropertyChanged se pierde y no tiene forma de saberlo en la llamada PropertyChanged.

col.PropertyChanged += (s, e) => { Trace.WriteLine("Changed " + e.PropertyName) 

Para solucionar este He creado una nueva clase PropertyChangedEventArgsEx y ha cambiado el método ContainedElementChanged dentro de su clase.

nueva clase

public class PropertyChangedEventArgsEx : PropertyChangedEventArgs 
{ 
    public object Sender { get; private set; } 

    public PropertyChangedEventArgsEx(string propertyName, object sender) 
     : base(propertyName) 
    { 
     this.Sender = sender; 
    } 
} 

cambios en su clase

private void ContainedElementChanged(object sender, PropertyChangedEventArgs e) 
    { 
     var ex = new PropertyChangedEventArgsEx(e.PropertyName, sender); 
     OnPropertyChanged(ex); 
    } 

Después de esto, usted puede obtener el elemento real Sender en col.PropertyChanged += (s, e) echando e a PropertyChangedEventArgsEx

((INotifyPropertyChanged)col).PropertyChanged += (s, e) => 
     { 
      var argsEx = (PropertyChangedEventArgsEx)e; 
      Trace.WriteLine(argsEx.Sender.ToString()); 
     }; 

De nuevo, debe tener en cuenta que el s aquí está la colección de elementos, no el elemento real que activó el evento. De ahí la nueva propiedad Sender en la clase PropertyChangedEventArgsEx.

0

Rxx 2.0 contiene operators que junto con este conversion operator para ObservableCollection<T> hace que sea fácil lograr su objetivo.

ObservableCollection<MyClass> collection = ...; 

var changes = collection.AsCollectionNotifications<MyClass>(); 
var itemChanges = changes.PropertyChanges(); 
var deepItemChanges = changes.PropertyChanges(
    item => item.ChildItems.AsCollectionNotifications<MyChildClass>()); 

La siguiente propiedad cambió los patrones de notificación son compatibles con MyClass y MyChildClass:

  • INotifyPropertyChanged
  • [propiedad] patrón cambió evento (herencia, para el uso de los componentes del Modelo)
  • WPF dependencia propiedades
0

La forma más sencilla de hacerlo es simplemente hacer

using System.ComponentModel; 
public class Example 
{ 
    BindingList<Foo> _collection; 

    public Example() 
    { 
     _collection = new BindingList<Foo>(); 
     _collection.ListChanged += Collection_ListChanged; 
    } 

    void Collection_ListChanged(object sender, ListChangedEventArgs e) 
    { 
     MessageBox.Show(e.ListChangedType.ToString()); 
    } 

} 

La clase BindingList como estado en SENCE .NET 2.0. Se activará el evento ListChanged en cualquier momento que un artículo en la colección se dispare INotifyPropertyChanged.

Cuestiones relacionadas