Al borrar una colección observable, no hay elementos en e.OldItems

91

Tengo algo aquí que realmente me está tomando por sorpresa.

Tengo un ObservableCollection de T que está lleno de elementos. También tengo un controlador de eventos adjunto al evento CollectionChanged.

Cuando clara la colección que provoca un evento CollectionChanged con e.Action conjunto de NotifyCollectionChangedAction.Reset. Ok, eso es normal. Pero lo extraño es que ni e.OldItems ni e.NewItems contienen nada. Esperaría que e.OldItems se llene con todos los elementos que se eliminaron de la colección.

¿Alguien más ha visto esto? Y si es así, ¿cómo lo han solucionado?

Algunos antecedentes: estoy usando el evento CollectionChanged para adjuntar y separar de otro evento y, por lo tanto, si no obtengo ningún elemento en e.OldItems ... no podré desconectarme de ese evento.


ACLARACIÓN: Sé que la documentación no establece rotundamente que tiene que comportarse de esta manera. Pero para cualquier otra acción, me está notificando lo que ha hecho. Entonces, mi suposición es que me lo diría ... en el caso de Clear / Reset también.


A continuación se muestra el código de muestra si desea reproducirlo usted mismo. Primero del xaml:

<Window
    x:Class="ObservableCollection.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1"
    Height="300"
    Width="300"
>
    <StackPanel>
        <Button x:Name="addButton" Content="Add" Width="100" Height="25" Margin="10" Click="addButton_Click"/>
        <Button x:Name="moveButton" Content="Move" Width="100" Height="25" Margin="10" Click="moveButton_Click"/>
        <Button x:Name="removeButton" Content="Remove" Width="100" Height="25" Margin="10" Click="removeButton_Click"/>
        <Button x:Name="replaceButton" Content="Replace" Width="100" Height="25" Margin="10" Click="replaceButton_Click"/>
        <Button x:Name="resetButton" Content="Reset" Width="100" Height="25" Margin="10" Click="resetButton_Click"/>
    </StackPanel>
</Window>

A continuación, el código detrás:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Collections.ObjectModel;

namespace ObservableCollection
{
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
            _integerObservableCollection.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(_integerObservableCollection_CollectionChanged);
        }

        private void _integerObservableCollection_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case System.Collections.Specialized.NotifyCollectionChangedAction.Add:
                    break;
                case System.Collections.Specialized.NotifyCollectionChangedAction.Move:
                    break;
                case System.Collections.Specialized.NotifyCollectionChangedAction.Remove:
                    break;
                case System.Collections.Specialized.NotifyCollectionChangedAction.Replace:
                    break;
                case System.Collections.Specialized.NotifyCollectionChangedAction.Reset:
                    break;
                default:
                    break;
            }
        }

        private void addButton_Click(object sender, RoutedEventArgs e)
        {
            _integerObservableCollection.Add(25);
        }

        private void moveButton_Click(object sender, RoutedEventArgs e)
        {
            _integerObservableCollection.Move(0, 19);
        }

        private void removeButton_Click(object sender, RoutedEventArgs e)
        {
            _integerObservableCollection.RemoveAt(0);
        }

        private void replaceButton_Click(object sender, RoutedEventArgs e)
        {
            _integerObservableCollection[0] = 50;
        }

        private void resetButton_Click(object sender, RoutedEventArgs e)
        {
            _integerObservableCollection.Clear();
        }

        private ObservableCollection<int> _integerObservableCollection = new ObservableCollection<int> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 };
    }
}
cplotts
fuente
¿Por qué necesita darse de baja del evento? ¿En qué dirección te suscribes? Los eventos crean una referencia al suscriptor retenido por el recaudador, no al revés. Si los recaudadores son elementos de una colección que se despeja, serán recolectados de forma segura y las referencias desaparecerán, sin fugas. Si los elementos son los suscriptores y los hace referencia a uno de los recaudadores, simplemente establezca el evento en nulo en el recaudador cuando obtenga un reinicio, sin necesidad de cancelar la suscripción de elementos individualmente.
Aleksandr Dubinsky
Créame, sé cómo funciona esto. El evento en cuestión fue en un singleton que se mantuvo durante mucho tiempo ... por lo tanto, los elementos de la colección eran los suscriptores. Su solución de simplemente configurar el evento en nulo no funciona ... ya que el evento aún necesita activarse ... posiblemente notificando a otros suscriptores (no necesariamente a los de la colección).
cplotts

Respuestas:

46

No pretende incluir los elementos antiguos, porque Restablecer no significa que la lista se haya borrado

Significa que ha ocurrido algo dramático, y el costo de resolver las adiciones / eliminaciones probablemente excedería el costo de simplemente volver a escanear la lista desde cero ... así que eso es lo que debe hacer.

MSDN sugiere un ejemplo de toda la colección que se reordena como candidata a reiniciarse.

Reiterar. Restablecer no significa que esté claro , significa que sus suposiciones sobre la lista ahora no son válidas. Trátelo como si fuera una lista completamente nueva . Clear pasa a ser un ejemplo de esto, pero bien podría haber otros.

Algunos ejemplos:
he tenido una lista como esta con muchos elementos en ella, y se ha vinculado a los datos de un WPF ListViewpara mostrar en pantalla.
Si borra la lista y genera el .Resetevento, el rendimiento es prácticamente instantáneo, pero si, en cambio, genera muchos .Removeeventos individuales , el rendimiento es terrible, ya que WPF elimina los elementos uno por uno. También lo he usado .Reseten mi propio código para indicar que la lista ha sido reordenada, en lugar de emitir miles de Moveoperaciones individuales . Al igual que con Clear, hay un gran impacto en el rendimiento cuando se generan muchos eventos individuales.

Orion Edwards
fuente
1
Voy a disentir respetuosamente sobre esta base. Si observa la documentación, dice: Representa una recopilación de datos dinámica que proporciona notificaciones cuando se agregan, eliminan elementos o cuando se actualiza la lista completa (consulte msdn.microsoft.com/en-us/library/ms668613(v=VS .100) .aspx )
cplotts
6
Los documentos establecen que debería notificarle cuando se agreguen / eliminen / actualicen elementos, pero no promete decirle todos los detalles de los elementos ... solo que ocurrió el evento. Desde este punto de vista, el comportamiento está bien. Personalmente, creo que deberían haber puesto todos los elementos OldItemsal limpiar (es solo copiar una lista), pero tal vez hubo algún escenario en el que esto fue demasiado caro. En cualquier caso, si desea una colección que le notifique todos los elementos eliminados, no sería difícil de hacer.
Orion Edwards
2
Bueno, si Resetes para indicar una operación costosa, es muy probable que se aplique el mismo razonamiento para copiar toda la lista a OldItems.
pbalaga
7
Dato curioso: desde .NET 4.5 , en Resetrealidad significa "Se borró el contenido de la colección ". Ver msdn.microsoft.com/en-us/library/…
Athari
9
Esta respuesta no ayuda mucho, lo siento. Sí, puede volver a escanear toda la lista si obtiene un restablecimiento, pero no tiene acceso para eliminar elementos, por lo que es posible que deba eliminar los controladores de eventos de ellos. Este es un gran problema.
Virus721
22

Tuvimos el mismo problema aquí. La acción Restablecer en CollectionChanged no incluye OldItems. Tuvimos una solución: usamos en su lugar el siguiente método de extensión:

public static void RemoveAll(this IList list)
{
   while (list.Count > 0)
   {
      list.RemoveAt(list.Count - 1);
   }
}

Terminamos no admitiendo la función Clear () y lanzando una NotSupportedException en el evento CollectionChanged para las acciones de restablecimiento. RemoveAll desencadenará una acción Eliminar en el evento CollectionChanged, con los OldItems adecuados.

decasteljau
fuente
Buena idea. No me gusta no admitir Clear ya que ese es el método (en mi experiencia) que la mayoría de la gente usa ... pero al menos le está advirtiendo al usuario con una excepción.
cplotts
Estoy de acuerdo, esta no es la solución ideal, pero encontramos que es la mejor solución alternativa aceptable.
decasteljau
¡Se supone que no debes usar los artículos viejos! Lo que se supone que debe hacer es volcar cualquier dato que tenga en la lista y volver a escanearlo como si fuera una nueva lista.
Orion Edwards
16
El problema, Orion, con tu sugerencia ... es el caso de uso que generó esta pregunta. ¿Qué sucede cuando tengo elementos en la lista de los que quiero separar un evento? No puedo simplemente volcar los datos en la lista ... resultaría en pérdidas de memoria / presión.
cplotts
5
La principal desventaja de esta solución es que si elimina 1000 elementos, activa CollectionChanged 1000 veces y la interfaz de usuario tiene que actualizar CollectionView 1000 veces (actualizar los elementos de la interfaz de usuario es costoso). Si no tiene miedo de anular la clase ObservableCollection, puede hacer que active el evento Clear () pero proporcione el evento Args correcto, lo que permite que el código de monitoreo anule el registro de todos los elementos eliminados.
Alain
13

Otra opción es reemplazar el evento Reset con un solo evento Remove que tenga todos los elementos borrados en su propiedad OldItems de la siguiente manera:

public class ObservableCollectionNoReset<T> : ObservableCollection<T>
{
    protected override void ClearItems()
    {
        List<T> removed = new List<T>(this);
        base.ClearItems();
        base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed));
    }

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (e.Action != NotifyCollectionChangedAction.Reset)
            base.OnCollectionChanged(e);
    }
    // Constructors omitted
    ...
}

Ventajas:

  1. No es necesario suscribirse a un evento adicional (como lo requiere la respuesta aceptada)

  2. No genera un evento para cada objeto eliminado (algunas otras soluciones propuestas dan como resultado múltiples eventos Eliminados).

  3. El suscriptor solo necesita verificar NewItems y OldItems en cualquier evento para agregar / eliminar controladores de eventos según sea necesario.

Desventajas:

  1. Sin evento de reinicio

  2. Sobrecarga pequeña (?) Creando una copia de la lista.

  3. ???

EDITAR 2012-02-23

Desafortunadamente, cuando se vincula a los controles basados ​​en listas de WPF, borrar una colección ObservableCollectionNoReset con varios elementos dará como resultado una excepción "Acciones de rango no admitidas". Para usar con controles con esta limitación, cambié la clase ObservableCollectionNoReset a:

public class ObservableCollectionNoReset<T> : ObservableCollection<T>
{
    // Some CollectionChanged listeners don't support range actions.
    public Boolean RangeActionsSupported { get; set; }

    protected override void ClearItems()
    {
        if (RangeActionsSupported)
        {
            List<T> removed = new List<T>(this);
            base.ClearItems();
            base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed));
        }
        else
        {
            while (Count > 0 )
                base.RemoveAt(Count - 1);
        }                
    }

    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (e.Action != NotifyCollectionChangedAction.Reset)
            base.OnCollectionChanged(e);
    }

    public ObservableCollectionNoReset(Boolean rangeActionsSupported = false) 
    {
        RangeActionsSupported = rangeActionsSupported;
    }

    // Additional constructors omitted.
 }

Esto no es tan eficiente cuando RangeActionsSupported es falso (el valor predeterminado) porque se genera una notificación de eliminación por cada objeto de la colección

Grantnz
fuente
Me gusta esto, pero desafortunadamente Silverlight 4 NotifyCollectionChangedEventArgs no tiene un constructor que tome una lista de elementos.
Simon Brangwin
2
Me encantó esta solución, pero no funciona ... No puede generar un NotifyCollectionChangedEventArgs que tenga más de un elemento cambiado a menos que la acción sea "Restablecer". Obtienes una excepción Range actions are not supported., no sé por qué hace esto, pero ahora esto no deja otra opción que eliminar cada elemento de uno en uno ...
Alain
2
@Alain The ObservableCollection no impone esta restricción. Sospecho que es el control WPF al que ha vinculado la colección. Tuve el mismo problema y nunca pude publicar una actualización con mi solución. Editaré mi respuesta con la clase modificada que funciona cuando está vinculada a un control WPF.
grantnz
Veo eso ahora. De hecho, encontré una solución muy elegante que anula el evento CollectionChanged y se repite en foreach( NotifyCollectionChangedEventHandler handler in this.CollectionChanged )If handler.Target is CollectionView, entonces puede disparar el controlador con Action.Resetargumentos; de lo contrario, puede proporcionar los argumentos completos. Lo mejor de ambos mundos, manejador por manejador :). Algo así como lo que hay aquí: stackoverflow.com/a/3302917/529618
Alain
Publiqué mi propia solución a continuación. stackoverflow.com/a/9416535/529618 Muchas gracias por su inspiradora solución. Me llevó a la mitad del camino.
Alain
10

De acuerdo, sé que esta es una pregunta muy antigua, pero se me ocurrió una buena solución al problema y pensé en compartirla. Esta solución se inspira en muchas de las excelentes respuestas aquí, pero tiene las siguientes ventajas:

  • No es necesario crear una nueva clase y anular métodos de ObservableCollection
  • No manipula el funcionamiento de NotifyCollectionChanged (por lo que no se juega con Reset)
  • No hace uso de la reflexión

Aquí está el código:

 public static void Clear<T>(this ObservableCollection<T> collection, Action<ObservableCollection<T>> unhookAction)
 {
     unhookAction.Invoke(collection);
     collection.Clear();
 }

Este método de extensión simplemente toma un Actionque se invocará antes de que se borre la colección.

Abrazo mortal
fuente
Muy buena idea. Sencillo, elegante.
cplotts
9

Encontré una solución que permite al usuario aprovechar la eficiencia de agregar o eliminar muchos elementos a la vez mientras solo se activa un evento, y satisfacer las necesidades de UIElements para obtener los argumentos del evento Action.Reset mientras que todos los demás usuarios lo harían como una lista de elementos agregados y eliminados.

Esta solución implica anular el evento CollectionChanged. Cuando vamos a disparar este evento, podemos mirar el objetivo de cada controlador registrado y determinar su tipo. Dado que solo las clases ICollectionView requieren NotifyCollectionChangedAction.Resetargumentos cuando cambia más de un elemento, podemos seleccionarlos y dar a todos los demás argumentos de eventos adecuados que contengan la lista completa de elementos eliminados o agregados. A continuación se muestra la implementación.

public class BaseObservableCollection<T> : ObservableCollection<T>
{
    //Flag used to prevent OnCollectionChanged from firing during a bulk operation like Add(IEnumerable<T>) and Clear()
    private bool _SuppressCollectionChanged = false;

    /// Overridden so that we may manually call registered handlers and differentiate between those that do and don't require Action.Reset args.
    public override event NotifyCollectionChangedEventHandler CollectionChanged;

    public BaseObservableCollection() : base(){}
    public BaseObservableCollection(IEnumerable<T> data) : base(data){}

    #region Event Handlers
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if( !_SuppressCollectionChanged )
        {
            base.OnCollectionChanged(e);
            if( CollectionChanged != null )
                CollectionChanged.Invoke(this, e);
        }
    }

    //CollectionViews raise an error when they are passed a NotifyCollectionChangedEventArgs that indicates more than
    //one element has been added or removed. They prefer to receive a "Action=Reset" notification, but this is not suitable
    //for applications in code, so we actually check the type we're notifying on and pass a customized event args.
    protected virtual void OnCollectionChangedMultiItem(NotifyCollectionChangedEventArgs e)
    {
        NotifyCollectionChangedEventHandler handlers = this.CollectionChanged;
        if( handlers != null )
            foreach( NotifyCollectionChangedEventHandler handler in handlers.GetInvocationList() )
                handler(this, !(handler.Target is ICollectionView) ? e : new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }
    #endregion

    #region Extended Collection Methods
    protected override void ClearItems()
    {
        if( this.Count == 0 ) return;

        List<T> removed = new List<T>(this);
        _SuppressCollectionChanged = true;
        base.ClearItems();
        _SuppressCollectionChanged = false;
        OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed));
    }

    public void Add(IEnumerable<T> toAdd)
    {
        if( this == toAdd )
            throw new Exception("Invalid operation. This would result in iterating over a collection as it is being modified.");

        _SuppressCollectionChanged = true;
        foreach( T item in toAdd )
            Add(item);
        _SuppressCollectionChanged = false;
        OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List<T>(toAdd)));
    }

    public void Remove(IEnumerable<T> toRemove)
    {
        if( this == toRemove )
            throw new Exception("Invalid operation. This would result in iterating over a collection as it is being modified.");

        _SuppressCollectionChanged = true;
        foreach( T item in toRemove )
            Remove(item);
        _SuppressCollectionChanged = false;
        OnCollectionChangedMultiItem(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new List<T>(toRemove)));
    }
    #endregion
}
Alain
fuente
7

Ok, aunque todavía deseo que ObservableCollection se comporte como deseaba ... el siguiente código es lo que terminé haciendo. Básicamente, creé una nueva colección de T llamada TrulyObservableCollection y anulé el método ClearItems que luego usé para generar un evento de compensación.

En el código que usa este TrulyObservableCollection, utilizo este evento de compensación para recorrer los elementos que todavía están en la colección en ese punto para realizar la separación en el evento del que deseaba desconectarme.

Espero que este enfoque también ayude a otra persona.

public class TrulyObservableCollection<T> : ObservableCollection<T>
{
    public event EventHandler<EventArgs> Clearing;
    protected virtual void OnClearing(EventArgs e)
    {
        if (Clearing != null)
            Clearing(this, e);
    }

    protected override void ClearItems()
    {
        OnClearing(EventArgs.Empty);
        base.ClearItems();
    }
}
cplotts
fuente
1
Necesita cambiar el nombre de su clase a BrokenObservableCollection, no TrulyObservableCollection, está malinterpretando lo que significa la acción de restablecimiento.
Orion Edwards
1
@Orion Edwards: No estoy de acuerdo. Vea mi comentario a su respuesta.
cplotts
1
@Orion Edwards: Oh, espera, ya veo, estás siendo gracioso. Pero entonces realmente debería llamar: ActuallyUsefulObservableCollection. :)
cplotts
6
Lol gran nombre. Estoy de acuerdo en que esto es un descuido serio en el diseño.
devios1
1
Si va a implementar una nueva clase ObservableCollection de todos modos, no es necesario crear un nuevo evento que deba supervisarse por separado. Simplemente puede evitar que ClearItems active un argumento de evento Action = Reset y reemplazarlo con un argumento de evento Action = Remove que contenga una lista e.OldItems de todos los elementos que estaban en la lista. Vea otras soluciones en esta pregunta.
Alain
4

Abordé este de una manera ligeramente diferente, ya que quería registrarme en un evento y manejar todas las adiciones y eliminaciones en el controlador de eventos. Comencé anulando el evento de cambio de colección y redirigiendo las acciones de restablecimiento a las acciones de eliminación con una lista de elementos. Todo esto salió mal porque estaba usando la colección observable como fuente de elementos para una vista de colección y obtuve "Acciones de rango no admitidas".

Finalmente creé un nuevo evento llamado CollectionChangedRange que actúa de la manera que esperaba que actuara la versión incorporada.

No puedo imaginar por qué se permitiría esta limitación y espero que esta publicación al menos evite que otros caigan en el callejón sin salida que yo hice.

/// <summary>
/// An observable collection with support for addrange and clear
/// </summary>
/// <typeparam name="T"></typeparam>
[Serializable]
[TypeConverter(typeof(ExpandableObjectConverter))]
public class ObservableCollectionRange<T> : ObservableCollection<T>
{
    private bool _addingRange;

    [field: NonSerialized]
    public event NotifyCollectionChangedEventHandler CollectionChangedRange;

    protected virtual void OnCollectionChangedRange(NotifyCollectionChangedEventArgs e)
    {
        if ((CollectionChangedRange == null) || _addingRange) return;
        using (BlockReentrancy())
        {
            CollectionChangedRange(this, e);
        }
    }

    public void AddRange(IEnumerable<T> collection)
    {
        CheckReentrancy();
        var newItems = new List<T>();
        if ((collection == null) || (Items == null)) return;
        using (var enumerator = collection.GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                _addingRange = true;
                Add(enumerator.Current);
                _addingRange = false;
                newItems.Add(enumerator.Current);
            }
        }
        OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newItems));
    }

    protected override void ClearItems()
    {
        CheckReentrancy();
        var oldItems = new List<T>(this);
        base.ClearItems();
        OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, oldItems));
    }

    protected override void InsertItem(int index, T item)
    {
        CheckReentrancy();
        base.InsertItem(index, item);
        OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
    }

    protected override void MoveItem(int oldIndex, int newIndex)
    {
        CheckReentrancy();
        var item = base[oldIndex];
        base.MoveItem(oldIndex, newIndex);
        OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, item, newIndex, oldIndex));
    }

    protected override void RemoveItem(int index)
    {
        CheckReentrancy();
        var item = base[index];
        base.RemoveItem(index);
        OnCollectionChangedRange(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index));
    }

    protected override void SetItem(int index, T item)
    {
        CheckReentrancy();
        var oldItem = base[index];
        base.SetItem(index, item);
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, oldItem, item, index));
    }
}

/// <summary>
/// A read only observable collection with support for addrange and clear
/// </summary>
/// <typeparam name="T"></typeparam>
[Serializable]
[TypeConverter(typeof(ExpandableObjectConverter))]
public class ReadOnlyObservableCollectionRange<T> : ReadOnlyObservableCollection<T>
{
    [field: NonSerialized]
    public event NotifyCollectionChangedEventHandler CollectionChangedRange;

    public ReadOnlyObservableCollectionRange(ObservableCollectionRange<T> list) : base(list)
    {
        list.CollectionChangedRange += HandleCollectionChangedRange;
    }

    private void HandleCollectionChangedRange(object sender, NotifyCollectionChangedEventArgs e)
    {
        OnCollectionChangedRange(e);
    }

    protected virtual void OnCollectionChangedRange(NotifyCollectionChangedEventArgs args)
    {
        if (CollectionChangedRange != null)
        {
            CollectionChangedRange(this, args);
        }
    }

}

fuente
Enfoque interesante. Gracias por publicarlo. Si alguna vez tengo problemas con mi propio enfoque, creo que volveré a examinar el suyo.
cplotts
3

Así es como funciona ObservableCollection, puede solucionar esto manteniendo su propia lista fuera de ObservableCollection (agregando a la lista cuando la acción es Agregar, eliminar cuando la acción es Eliminar, etc.), luego puede obtener todos los elementos eliminados (o elementos agregados ) cuando la acción es Restablecer comparando su lista con ObservableCollection.

Otra opción es crear su propia clase que implemente IList e INotifyCollectionChanged, luego puede adjuntar y separar eventos dentro de esa clase (o establecer OldItems en Clear si lo desea); en realidad no es difícil, pero requiere mucho tipeo.

Nir
fuente
Consideré hacer un seguimiento de otra lista, así como sugieres primero, pero parece mucho trabajo innecesario. Su segunda sugerencia está muy cerca de lo que terminé haciendo ... que publicaré como respuesta.
cplotts
3

Para el escenario de adjuntar y desconectar controladores de eventos a los elementos de ObservableCollection, también existe una solución "del lado del cliente". En el código de manejo de eventos, puede verificar si el remitente está en ObservableCollection usando el método Contains. Ventaja: puede trabajar con cualquier ObservableCollection existente. Contras: el método Contains se ejecuta con O (n) donde n es el número de elementos en ObservableCollection. Entonces esta es una solución para pequeñas ObservableCollections.

Otra solución "del lado del cliente" es utilizar un controlador de eventos en el medio. Simplemente registre todos los eventos en el controlador de eventos en el medio. Este controlador de eventos, a su vez, notifica al controlador de eventos real a través de una devolución de llamada o un evento. Si se produce una acción de reinicio, elimine la devolución de llamada o el evento, cree un nuevo controlador de eventos en el medio y olvídese del anterior. Este enfoque también funciona para grandes ObservableCollections. Usé esto para el evento PropertyChanged (ver código a continuación).

    /// <summary>
    /// Helper class that allows to "detach" all current Eventhandlers by setting
    /// DelegateHandler to null.
    /// </summary>
    public class PropertyChangedDelegator
    {
        /// <summary>
        /// Callback to the real event handling code.
        /// </summary>
        public PropertyChangedEventHandler DelegateHandler;
        /// <summary>
        /// Eventhandler that is registered by the elements.
        /// </summary>
        /// <param name="sender">the element that has been changed.</param>
        /// <param name="e">the event arguments</param>
        public void PropertyChangedHandler(Object sender, PropertyChangedEventArgs e)
        {
            if (DelegateHandler != null)
            {
                DelegateHandler(sender, e);
            }
            else
            {
                INotifyPropertyChanged s = sender as INotifyPropertyChanged;
                if (s != null)
                    s.PropertyChanged -= PropertyChangedHandler;
            }   
        }
    }
Chris
fuente
Creo que con su primer enfoque, necesitaría otra lista para rastrear los elementos ... porque una vez que obtenga el evento CollectionChanged con la acción Restablecer ... la colección ya está vacía. No sigo tu segunda sugerencia. Me encantaría un arnés de prueba simple que lo ilustre, pero para agregar, quitar y borrar la colección Observable. Si crea un ejemplo, puede enviarme un correo electrónico a mi nombre seguido de mi apellido en gmail.com.
cplotts
2

Al observar NotifyCollectionChangedEventArgs , parece que OldItems solo contiene elementos modificados como resultado de la acción Reemplazar, Eliminar o Mover. No indica que contendrá nada en Clear. Sospecho que Clear desencadena el evento, pero no registra los elementos eliminados y no invoca el código Eliminar en absoluto.

tvanfosson
fuente
6
Yo también vi eso, pero no me gusta. Me parece un enorme agujero.
cplotts
No invoca el código de eliminación porque no es necesario. Restablecer significa "algo dramático ha sucedido, necesitas empezar de nuevo". Una operación clara es un ejemplo de esto, pero hay otros
Orion Edwards
2

Bueno, decidí ensuciarme yo mismo.

Microsoft puso MUCHO trabajo para asegurarse siempre de que NotifyCollectionChangedEventArgs no tenga ningún dato al llamar a un reinicio. Supongo que esta fue una decisión de rendimiento / memoria. Si está restableciendo una colección con 100.000 elementos, supongo que no querían duplicar todos esos elementos.

Pero dado que mis colecciones nunca tienen más de 100 elementos, no veo ningún problema con eso.

De todos modos creé una clase heredada con el siguiente método:

protected override void ClearItems()
{
    CheckReentrancy();
    List<TItem> oldItems = new List<TItem>(Items);

    Items.Clear();

    OnPropertyChanged(new PropertyChangedEventArgs("Count"));
    OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));

    NotifyCollectionChangedEventArgs e =
        new NotifyCollectionChangedEventArgs
        (
            NotifyCollectionChangedAction.Reset
        );

        FieldInfo field =
            e.GetType().GetField
            (
                "_oldItems",
                BindingFlags.Instance | BindingFlags.NonPublic
            );
        field.SetValue(e, oldItems);

        OnCollectionChanged(e);
    }
HaxElit
fuente
Esto es genial, pero probablemente no funcionaría en nada más que en un entorno de plena confianza. Reflexionar sobre los campos privados requiere plena confianza, ¿verdad?
Paul
1
¿Por qué harías esto? Hay otras cosas que pueden hacer que se active la acción Restablecer, solo porque haya deshabilitado el método claro no significa que haya desaparecido (o que debería hacerlo)
Orion Edwards
Enfoque interesante, pero la reflexión puede ser lenta.
cplotts
2

Tanto la interfaz ObservableCollection como la INotifyCollectionChanged están claramente escritas con un uso específico en mente: la construcción de la interfaz de usuario y sus características de rendimiento específicas.

Cuando desee notificaciones de cambios en la colección, generalmente solo le interesan los eventos Agregar y Eliminar.

Yo uso la siguiente interfaz:

using System;
using System.Collections.Generic;

/// <summary>
/// Notifies listeners of the following situations:
/// <list type="bullet">
/// <item>Elements have been added.</item>
/// <item>Elements are about to be removed.</item>
/// </list>
/// </summary>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
interface INotifyCollection<T>
{
    /// <summary>
    /// Occurs when elements have been added.
    /// </summary>
    event EventHandler<NotifyCollectionEventArgs<T>> Added;

    /// <summary>
    /// Occurs when elements are about to be removed.
    /// </summary>
    event EventHandler<NotifyCollectionEventArgs<T>> Removing;
}

/// <summary>
/// Provides data for the NotifyCollection event.
/// </summary>
/// <typeparam name="T">The type of elements in the collection.</typeparam>
public class NotifyCollectionEventArgs<T> : EventArgs
{
    /// <summary>
    /// Gets or sets the elements.
    /// </summary>
    /// <value>The elements.</value>
    public IEnumerable<T> Items
    {
        get;
        set;
    }
}

También escribí mi propia sobrecarga de Colección donde:

  • ClearItems aumenta la eliminación
  • InsertItem aumenta Agregado
  • RemoveItem aumenta la eliminación
  • SetItem aumenta la eliminación y agregación

Por supuesto, también se puede agregar AddRange.

Rick Beerendonk
fuente
+1 por señalar que Microsoft diseñó ObservableCollection con un caso de uso específico en mente ... y con un ojo en el rendimiento. Estoy de acuerdo. Dejo un hueco para otras situaciones, pero estoy de acuerdo.
cplotts
-1 Puede que me interese todo tipo de cosas. A menudo necesito el índice de elementos agregados / eliminados. Es posible que desee optimizar el reemplazo. Etc. El diseño de INotifyCollectionChanged es bueno. El problema que debería solucionarse es que nadie en MS lo implementó.
Aleksandr Dubinsky
1

Estaba revisando parte del código de gráficos en los kits de herramientas de Silverlight y WPF y noté que también resolvieron este problema (de una manera similar) ... y pensé en seguir adelante y publicar su solución.

Básicamente, también crearon un ObservableCollection derivado y anularon ClearItems, llamando a Remove en cada elemento que se borraba.

Aquí está el código:

/// <summary>
/// An observable collection that cannot be reset.  When clear is called
/// items are removed individually, giving listeners the chance to detect
/// each remove event and perform operations such as unhooking event 
/// handlers.
/// </summary>
/// <typeparam name="T">The type of item in the collection.</typeparam>
public class NoResetObservableCollection<T> : ObservableCollection<T>
{
    public NoResetObservableCollection()
    {
    }

    /// <summary>
    /// Clears all items in the collection by removing them individually.
    /// </summary>
    protected override void ClearItems()
    {
        IList<T> items = new List<T>(this);
        foreach (T item in items)
        {
            Remove(item);
        }
    }
}
cplotts
fuente
Solo quiero señalar que no me gusta este enfoque tanto como el que marqué como respuesta ... ya que obtienes un evento NotifyCollectionChanged (con una acción Eliminar) ... para CADA elemento que se elimina.
cplotts
1

Este es un tema candente ... porque en mi opinión, Microsoft no hizo su trabajo correctamente ... una vez más. No me malinterpretes, me gusta Microsoft, ¡pero no son perfectos!

Leí la mayoría de los comentarios anteriores. Estoy de acuerdo con todos los que piensan que Microsoft no programó Clear () correctamente.

En mi opinión, al menos, se necesita un argumento para que sea posible desprender objetos de un evento ... pero también entiendo el impacto que tiene. Entonces, pensé en esta solución propuesta.

Espero que haga felices a todos, o al menos a casi todos ...

Eric

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Reflection;

namespace WpfUtil.Collections
{
    public static class ObservableCollectionExtension
    {
        public static void RemoveAllOneByOne<T>(this ObservableCollection<T> obsColl)
        {
            foreach (T item in obsColl)
            {
                while (obsColl.Count > 0)
                {
                    obsColl.RemoveAt(0);
                }
            }
        }

        public static void RemoveAll<T>(this ObservableCollection<T> obsColl)
        {
            if (obsColl.Count > 0)
            {
                List<T> removedItems = new List<T>(obsColl);
                obsColl.Clear();

                NotifyCollectionChangedEventArgs e =
                    new NotifyCollectionChangedEventArgs
                    (
                        NotifyCollectionChangedAction.Remove,
                        removedItems
                    );
                var eventInfo =
                    obsColl.GetType().GetField
                    (
                        "CollectionChanged",
                        BindingFlags.Instance | BindingFlags.NonPublic
                    );
                if (eventInfo != null)
                {
                    var eventMember = eventInfo.GetValue(obsColl);
                    // note: if eventMember is null
                    // nobody registered to the event, you can't call it.
                    if (eventMember != null)
                        eventMember.GetType().GetMethod("Invoke").
                            Invoke(eventMember, new object[] { obsColl, e });
                }
            }
        }
    }
}
Eric Ouellet
fuente
Sigo pensando que Microsoft debería proporcionar una forma de poder borrar con notificación. Sigo pensando que pierden el tiro al no proporcionar esa vía. Lo siento ! No estoy diciendo que el claro deba eliminarse, ¡ya que falta algo! Para obtener un acoplamiento bajo, a veces tenemos que ser avisados ​​de lo que se eliminó.
Eric Ouellet
1

Para simplificarlo, ¿por qué no anula el método ClearItem y hace lo que quiera allí, es decir, separar los elementos del evento?

public class PeopleAttributeList : ObservableCollection<PeopleAttributeDto>,    {
{
  protected override void ClearItems()
  {
    Do what ever you want
    base.ClearItems();
  }

  rest of the code omitted
}

Simple, limpio y contenido dentro del código de colección.

Stéphane
fuente
Eso está muy cerca de lo que hice en realidad ... vea la respuesta aceptada.
cplotts
0

Tuve el mismo problema y esta fue mi solución. Parece funcionar. ¿Alguien ve algún problema potencial con este enfoque?

// overriden so that we can call GetInvocationList
public override event NotifyCollectionChangedEventHandler CollectionChanged;

protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
    NotifyCollectionChangedEventHandler collectionChanged = CollectionChanged;
    if (collectionChanged != null)
    {
        lock (collectionChanged)
        {
            foreach (NotifyCollectionChangedEventHandler handler in collectionChanged.GetInvocationList())
            {
                try
                {
                    handler(this, e);
                }
                catch (NotSupportedException ex)
                {
                    // this will occur if this collection is used as an ItemsControl.ItemsSource
                    if (ex.Message == "Range actions are not supported.")
                    {
                        handler(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
                    }
                    else
                    {
                        throw ex;
                    }
                }
            }
        }
    }
}

Aquí hay algunos otros métodos útiles en mi clase:

public void SetItems(IEnumerable<T> newItems)
{
    Items.Clear();
    foreach (T newItem in newItems)
    {
        Items.Add(newItem);
    }
    NotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}

public void AddRange(IEnumerable<T> newItems)
{
    int index = Count;
    foreach (T item in newItems)
    {
        Items.Add(item);
    }
    NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List<T>(newItems), index);
    NotifyCollectionChanged(e);
}

public void RemoveRange(int startingIndex, int count)
{
    IList<T> oldItems = new List<T>();
    for (int i = 0; i < count; i++)
    {
        oldItems.Add(Items[startingIndex]);
        Items.RemoveAt(startingIndex);
    }
    NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new List<T>(oldItems), startingIndex);
    NotifyCollectionChanged(e);
}

// this needs to be overridden to avoid raising a NotifyCollectionChangedEvent with NotifyCollectionChangedAction.Reset, which our other lists don't support
new public void Clear()
{
    RemoveRange(0, Count);
}

public void RemoveWhere(Func<T, bool> criterion)
{
    List<T> removedItems = null;
    int startingIndex = default(int);
    int contiguousCount = default(int);
    for (int i = 0; i < Count; i++)
    {
        T item = Items[i];
        if (criterion(item))
        {
            if (removedItems == null)
            {
                removedItems = new List<T>();
                startingIndex = i;
                contiguousCount = 0;
            }
            Items.RemoveAt(i);
            removedItems.Add(item);
            contiguousCount++;
        }
        else if (removedItems != null)
        {
            NotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItems, startingIndex));
            removedItems = null;
            i = startingIndex;
        }
    }
    if (removedItems != null)
    {
        NotifyCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItems, startingIndex));
    }
}

private void NotifyCollectionChanged(NotifyCollectionChangedEventArgs e)
{
    OnPropertyChanged(new PropertyChangedEventArgs("Count"));
    OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
    OnCollectionChanged(e);
}
hipehumano
fuente
0

Encontré otra solución "simple" derivada de ObservableCollection, pero no es muy elegante porque usa Reflection ... Si te gusta aquí está mi solución:

public class ObservableCollectionClearable<T> : ObservableCollection<T>
{
    private T[] ClearingItems = null;

    protected override void OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
    {
        switch (e.Action)
        {
            case System.Collections.Specialized.NotifyCollectionChangedAction.Reset:
                if (this.ClearingItems != null)
                {
                    ReplaceOldItems(e, this.ClearingItems);
                    this.ClearingItems = null;
                }
                break;
        }
        base.OnCollectionChanged(e);
    }

    protected override void ClearItems()
    {
        this.ClearingItems = this.ToArray();
        base.ClearItems();
    }

    private static void ReplaceOldItems(System.Collections.Specialized.NotifyCollectionChangedEventArgs e, T[] olditems)
    {
        Type t = e.GetType();
        System.Reflection.FieldInfo foldItems = t.GetField("_oldItems", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        if (foldItems != null)
        {
            foldItems.SetValue(e, olditems);
        }
    }
}

Aquí guardo los elementos actuales en un campo de matriz en el método ClearItems, luego intercepto la llamada de OnCollectionChanged y sobrescribo el campo privado e._oldItems (a través de Reflections) antes de iniciar la base.

Formentz
fuente
0

Puede anular el método ClearItems y generar un evento con la acción Eliminar y OldItems.

public class ObservableCollection<T> : System.Collections.ObjectModel.ObservableCollection<T>
{
    protected override void ClearItems()
    {
        CheckReentrancy();
        var items = Items.ToList();
        base.ClearItems();
        OnPropertyChanged(new PropertyChangedEventArgs("Count"));
        OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
        OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, items, -1));
    }
}

Parte de la System.Collections.ObjectModel.ObservableCollection<T>realización:

public class ObservableCollection<T> : Collection<T>, INotifyCollectionChanged, INotifyPropertyChanged
{
    protected override void ClearItems()
    {
        CheckReentrancy();
        base.ClearItems();
        OnPropertyChanged(CountString);
        OnPropertyChanged(IndexerName);
        OnCollectionReset();
    }

    private void OnPropertyChanged(string propertyName)
    {
        OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
    }

    private void OnCollectionReset()
    {
        OnCollectionChanged(new   NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    private const string CountString = "Count";

    private const string IndexerName = "Item[]";
}
Artem Illarionov
fuente
-4

http://msdn.microsoft.com/en-us/library/system.collections.specialized.notifycollectionchangedaction(VS.95).aspx

Lea esta documentación con los ojos abiertos y el cerebro encendido. Microsoft hizo todo bien. Debes volver a escanear tu colección cuando te envíe una notificación de reinicio. Recibe una notificación de reinicio porque lanzar Agregar / Eliminar para cada elemento (que se elimina y se vuelve a agregar a la colección) es demasiado costoso.

Orion Edwards tiene toda la razón (respeto, hombre). Por favor piense más al leer la documentación.

Dima
fuente
5
De hecho, creo que usted y Orion tienen razón en su comprensión de cómo Microsoft lo diseñó para que funcione. :) Sin embargo, este diseño me causó problemas que necesitaba solucionar para mi situación. Esta situación también es común ... y por qué publiqué esta pregunta.
cplotts
Creo que debería mirar mi pregunta (y la respuesta marcada) un poco más. No estaba sugiriendo eliminar para todos los elementos.
cplotts
Y para que conste, respeto la respuesta de Orion ... Creo que nos estábamos divirtiendo un poco el uno con el otro ... al menos así es como lo tomé.
cplotts
Una cosa importante: no tiene que separar los procedimientos de manejo de eventos de los objetos que está eliminando. El desprendimiento se realiza automáticamente.
Dima
1
Entonces, en resumen, los eventos no se separan automáticamente al eliminar un objeto de una colección.
cplotts
-4

Si ObservableCollectionno se aclara, puede probar este código a continuación. puede ayudarte:

private TestEntities context; // This is your context

context.Refresh(System.Data.Objects.RefreshMode.StoreWins, context.UserTables); // to refresh the object context
Manas
fuente