Ao limpar uma ObservableCollection, não há itens em e.OldItems

91

Tenho algo aqui que realmente está me pegando desprevenido.

Eu tenho uma ObservableCollection de T que é preenchida com itens. Eu também tenho um manipulador de eventos anexado ao evento CollectionChanged.

Quando você limpa a coleção, ele causa um evento CollectionChanged com e.Action definido como NotifyCollectionChangedAction.Reset. Ok, isso é normal. Mas o que é estranho é que nem e.OldItems nem e.NewItems contém nada. Eu esperaria que e.OldItems fosse preenchido com todos os itens removidos da coleção.

alguém mais viu isso? E se sim, como eles contornaram isso?

Alguns antecedentes: Estou usando o evento CollectionChanged para anexar e desanexar de outro evento e, portanto, se eu não obtiver nenhum item em e.OldItems ... não poderei desanexar desse evento.


ESCLARECIMENTO: Eu sei que a documentação não declara abertamente que ele deve se comportar dessa maneira. Mas, para todas as outras ações, ele está me notificando do que fez. Portanto, suponho que isso me diria ... no caso de Limpar / Redefinir também.


Abaixo está o código de exemplo, se você deseja reproduzi-lo você mesmo. Primeiro o 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 seguir, o código por trás de:

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
fonte
Por que você precisa cancelar a inscrição do evento? Em que direção você está se inscrevendo? Os eventos criam uma referência ao assinante mantido pelo raiser, e não o contrário. Se os raisers forem itens em uma coleção que é apagada, eles serão coletados como lixo com segurança e as referências desaparecerão - sem vazamento. Se os itens são os assinantes e referenciados por um raiser, então apenas defina o evento como null no raiser quando você obtiver um Reset - não há necessidade de cancelar individualmente os itens.
Aleksandr Dubinsky
Acredite em mim, eu sei como isso funciona. O evento em questão foi em um singleton que durou muito tempo ... então os itens da coleção eram os assinantes. Sua solução de apenas definir o evento como nulo não funciona ... já que o evento ainda precisa ser disparado ... possivelmente notificando outros assinantes (não necessariamente aqueles na coleção).
cplotts de

Respostas:

46

Não afirma incluir os itens antigos, porque Redefinir não significa que a lista foi apagada

Isso significa que algo dramático aconteceu, e o custo de trabalhar para adicionar / remover provavelmente ultrapassaria o custo de apenas escanear novamente a lista do zero ... então é isso que você deve fazer.

O MSDN sugere um exemplo de toda a coleção sendo reclassificada como candidata a redefinição.

Reiterar. Reiniciar não significa claro , significa que suas suposições sobre a lista agora são inválidas. Trate-o como se fosse uma lista inteiramente nova . Acontece que Clear é uma instância disso, mas pode muito bem haver outras.

Alguns exemplos:
Eu tenho uma lista como esta com muitos itens nela, e ela foi ligada a um WPF ListViewpara exibir na tela.
Se você limpar a lista e aumentar o .Resetevento, o desempenho é praticamente instantâneo, mas se você aumentar muitos .Removeeventos individuais , o desempenho é péssimo, pois o WPF remove os itens um por um. Também usei .Resetem meu próprio código para indicar que a lista foi reordenada, em vez de emitir milhares de Moveoperações individuais . Tal como acontece com Clear, há uma grande queda de desempenho ao aumentar muitos eventos individuais.

Orion Edwards
fonte
1
Vou discordar respeitosamente nesta base. Se você olhar a documentação, ela afirma: Representa uma coleta de dados dinâmica que fornece notificações quando itens são adicionados, removidos ou quando toda a lista é atualizada (consulte msdn.microsoft.com/en-us/library/ms668613(v=VS .100) .aspx )
cplotts de
6
Os documentos afirmam que ele deve notificá-lo quando os itens forem adicionados / removidos / atualizados, mas não promete dizer todos os detalhes dos itens ... apenas que o evento ocorreu. Deste ponto de vista, o comportamento é bom. Pessoalmente, acho que eles deveriam ter colocado todos os itens OldItemsao limpar (é apenas copiar uma lista), mas talvez houvesse algum cenário em que isso fosse muito caro. De qualquer forma, se você quiser uma coleção que o notifique sobre todos os itens excluídos, não seria difícil de fazer.
Orion Edwards
2
Bem, se Reseté para indicar uma operação cara, é muito provável que o mesmo raciocínio se aplique à cópia de toda a lista para OldItems.
pbalaga
7
Fato engraçado: desde o .NET 4.5 , Resetna verdade significa "O conteúdo da coleção foi limpo ." Consulte msdn.microsoft.com/en-us/library/…
Athari
9
Esta resposta não ajuda muito, desculpe. Sim, você pode verificar novamente a lista inteira se obtiver um Reset, mas não tem acesso para remover itens, que pode ser necessário para remover manipuladores de eventos deles. Isto é um grande problema.
Virus721
22

Tivemos o mesmo problema aqui. A ação Reset em CollectionChanged não inclui os OldItems. Tínhamos uma solução alternativa: em vez disso, usamos o seguinte método de extensão:

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

Acabamos não suportando a função Clear () e lançando um evento NotSupportedException em CollectionChanged para ações Reset. O RemoveAll irá disparar uma ação Remove no evento CollectionChanged, com os OldItems apropriados.

decasteljau
fonte
Boa ideia. Não gosto de não suportar Clear, pois esse é o método (em minha experiência) que a maioria das pessoas usa ... mas pelo menos você está avisando o usuário com uma exceção.
cplotts
Concordo, esta não é a solução ideal, mas descobrimos que é a solução alternativa mais aceitável.
decasteljau
Você não deve usar os itens antigos! O que você deve fazer é descartar todos os dados que você tiver na lista e verificar novamente como se fosse uma nova lista!
Orion Edwards
16
O problema, Orion, com sua sugestão ... é o caso de uso que gerou essa pergunta. O que acontece quando tenho itens na lista dos quais desejo desanexar um evento? Não posso simplesmente descartar os dados da lista ... isso resultaria em vazamentos / pressão de memória.
cplotts de
5
A principal desvantagem desta solução é que se você remover 1000 itens, você dispara CollectionChanged 1000 vezes e a IU tem que atualizar o CollectionView 1000 vezes (atualizar os elementos da IU é caro). Se você não tem medo de substituir a classe ObservableCollection, pode fazer com que ela dispare o evento Clear (), mas forneça o evento Args correto, permitindo que o código de monitoramento cancele o registro de todos os elementos removidos.
Alain
13

Outra opção é substituir o evento Reset por um único evento Remove que tem todos os itens desmarcados em sua propriedade OldItems da seguinte forma:

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

Vantagens:

  1. Não há necessidade de se inscrever em um evento adicional (conforme exigido pela resposta aceita)

  2. Não gera um evento para cada objeto removido (algumas outras soluções propostas resultam em vários eventos Removidos).

  3. O assinante só precisa verificar NewItems e OldItems em qualquer evento para adicionar / remover manipuladores de eventos conforme necessário.

Desvantagens:

  1. Nenhum evento de reset

  2. Pequeno (?) Overhead criando cópia da lista.

  3. ???

EDITAR 23/02/2012

Infelizmente, quando vinculado a controles baseados em lista WPF, limpar uma coleção ObservableCollectionNoReset com vários elementos resultará em uma exceção "Ações de intervalo não suportadas". Para ser usado com controles com essa limitação, alterei a classe ObservableCollectionNoReset para:

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

Isso não é tão eficiente quando RangeActionsSupported é falso (o padrão) porque uma notificação de remoção é gerada por objeto na coleção

Grantnz
fonte
Eu gosto disso, mas infelizmente o Silverlight 4 NotifyCollectionChangedEventArgs não tem um construtor que leva uma lista de itens.
Simon Brangwin
2
Adorei essa solução, mas não funciona ... Você não tem permissão para acionar um NotifyCollectionChangedEventArgs que tenha mais de um item alterado, a menos que a ação seja "Redefinir". Você recebe uma exceção Range actions are not supported., não sei por que isso acontece, mas agora não há opção a não ser remover cada item de cada vez ...
Alain
2
@Alain O ObservableCollection não impõe essa restrição. Suspeito que seja o controle WPF ao qual você vinculou a coleção. Eu tive o mesmo problema e nunca cheguei a postar uma atualização com minha solução. Vou editar minha resposta com a classe modificada que funciona quando vinculada a um controle WPF.
grantnz
Eu vejo isso agora. Na verdade, eu encontrei uma solução muito elegante que substitui o evento CollectionChanged e faz um loop sobre foreach( NotifyCollectionChangedEventHandler handler in this.CollectionChanged )If handler.Target is CollectionView, então você pode disparar o manipulador com Action.Resetargs, caso contrário, você pode fornecer os args completos. O melhor dos dois mundos em uma base de manipulador por manipulador :). Mais ou menos como o que está aqui: stackoverflow.com/a/3302917/529618
Alain
Eu postei minha própria solução abaixo. stackoverflow.com/a/9416535/529618 Um enorme agradecimento a você por sua solução inspiradora. Me pegou no meio do caminho.
Alain
10

Ok, eu sei que esta é uma pergunta muito antiga, mas eu encontrei uma boa solução para o problema e pensei em compartilhar. Esta solução se inspira em muitas das ótimas respostas aqui, mas tem as seguintes vantagens:

  • Não há necessidade de criar uma nova classe e modificar métodos de ObservableCollection
  • Não interfere no funcionamento de NotifyCollectionChanged (portanto, não mexa com Reset)
  • Não faz uso de reflexão

Aqui está o código:

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

Este método de extensão simplesmente pega um Actionque será invocado antes que a coleção seja limpa.

DeadlyEmbrace
fonte
Ótima ideia. Simples, elegante.
cplotts
9

Eu encontrei uma solução que permite ao usuário capitalizar sobre a eficiência de adicionar ou remover muitos itens por vez enquanto apenas dispara um evento - e satisfazer as necessidades de UIElements para obter os argumentos de evento Action.Reset enquanto todos os outros usuários fariam como uma lista de elementos adicionados e removidos.

Esta solução envolve a substituição do evento CollectionChanged. Quando vamos disparar este evento, podemos realmente olhar para o destino de cada manipulador registrado e determinar seu tipo. Como apenas as classes ICollectionView requerem NotifyCollectionChangedAction.Resetargs quando mais de um item é alterado, podemos destacá-los e fornecer a todos os outros args de evento adequados que contêm a lista completa de itens removidos ou adicionados. Abaixo está a implementação.

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
fonte
7

Ok, embora eu ainda deseje que ObservableCollection se comporte como eu gostaria ... o código abaixo é o que acabei fazendo. Basicamente, criei uma nova coleção de T chamada TrulyObservableCollection e substituí o método ClearItems que usei para acionar um evento Clearing.

No código que usa este TrulyObservableCollection, eu uso este evento Clearing para percorrer os itens que ainda estão na coleção naquele ponto para fazer a desanexação no evento que eu desejava desanexar.

Espero que essa abordagem ajude outra pessoa também.

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
fonte
1
Você precisa renomear sua classe para BrokenObservableCollection, não TrulyObservableCollection- você está entendendo mal o que a ação de reinicialização significa.
Orion Edwards
1
@Orion Edwards: Eu discordo. Veja meu comentário a sua resposta.
cplotts de
1
@Orion Edwards: Oh, espere, eu vejo, você está sendo engraçado. Mas então eu realmente deveria chamá-lo: ActuallyUsefulObservableCollection. :)
cplotts
6
Lol, ótimo nome. Eu concordo que este é um descuido sério no design.
devios1
1
Se você for implementar uma nova classe ObservableCollection de qualquer maneira, não há necessidade de criar um novo evento que deve ser monitorado separadamente. Você pode simplesmente impedir que ClearItems acione args de evento Action = Reset e substituí-lo por args de evento Action = Remove que contém uma lista e.OldItems de todos os itens que estavam na lista. Veja outras soluções nesta questão.
Alain
4

Eu resolvi isso de uma maneira um pouco diferente, pois queria me registrar em um evento e lidar com todas as adições e remoções no manipulador de eventos. Comecei substituindo o evento de alteração da coleção e redirecionando as ações de redefinição para ações de remoção com uma lista de itens. Tudo deu errado porque eu estava usando a coleção observável como uma fonte de itens para uma visualização de coleção e obtive "Ações de intervalo não suportadas".

Finalmente criei um novo evento chamado CollectionChangedRange, que atua da maneira que eu esperava que a versão embutida atuasse.

Não consigo imaginar por que essa limitação seria permitida e espero que este post pelo menos impeça outros de irem para o beco sem saída que eu fiz.

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

}

fonte
Abordagem interessante. Obrigado por postar. Se algum dia eu tiver problemas com minha própria abordagem, acho que revisarei a sua.
cplotts de
3

É assim que ObservableCollection funciona. Você pode contornar isso mantendo sua própria lista fora de ObservableCollection (adicionar à lista quando a ação for Adicionar, remover quando a ação for Remover etc.), então você pode obter todos os itens removidos (ou itens adicionados ) quando a ação é reiniciada, comparando sua lista com ObservableCollection.

Outra opção é criar sua própria classe que implemente IList e INotifyCollectionChanged, então você pode anexar e desanexar eventos de dentro dessa classe (ou definir OldItems em Clear, se desejar) - não é realmente difícil, mas exige muita digitação.

Nir
fonte
Considerei manter o controle de outra lista tão bem quanto você sugeriu primeiro, mas parece ser muito trabalho desnecessário. A sua segunda sugestão é muito próxima do que acabei fazendo ... a qual postarei como resposta.
cplotts
3

Para o cenário de anexar e desanexar manipuladores de eventos aos elementos da ObservableCollection, também há uma solução "do lado do cliente". No código de tratamento de eventos, você pode verificar se o remetente está em ObservableCollection usando o método Contains. Pro: você pode trabalhar com qualquer ObservableCollection existente. Contras: o método Contains é executado com O (n) onde n é o número de elementos em ObservableCollection. Portanto, esta é uma solução para pequenas ObservableCollections.

Outra solução "do lado do cliente" é usar um manipulador de eventos no meio. Basta registrar todos os eventos no manipulador de eventos do meio. Este manipulador de eventos, por sua vez, notifica o manipulador de eventos real por meio de um retorno de chamada ou um evento. Se uma ação Reset ocorrer, remova o retorno de chamada ou evento, crie um novo manipulador de eventos no meio e esqueça o antigo. Essa abordagem também funciona para grandes ObservableCollections. Eu usei isso para o evento PropertyChanged (veja o código abaixo).

    /// <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
fonte
Acredito que com sua primeira abordagem, eu precisaria de outra lista para rastrear os itens ... porque uma vez que você obtém o evento CollectionChanged com a ação Reset ... a coleção já está vazia. Não estou de acordo com sua segunda sugestão. Eu adoraria um teste simples ilustrando isso, mas para adicionar, remover e limpar a ObservableCollection. Se você criar um exemplo, pode enviar-me um e-mail com meu nome seguido do meu sobrenome em gmail.com.
cplotts de
2

Olhando para NotifyCollectionChangedEventArgs , parece que OldItems contém apenas itens alterados como resultado da ação Substituir, Remover ou Mover. Não indica que conterá algo em Clear. Suspeito que Clear dispara o evento, mas não registra os itens removidos e não invoca o código Remove.

Tvanfosson
fonte
6
Eu também vi isso, mas não gostei. Parece um buraco aberto para mim.
cplotts de
Ele não invoca o código de remoção porque não é necessário. Reiniciar significa "algo dramático aconteceu, você precisa começar de novo". Uma operação clara é um exemplo disso, mas há outros
Orion Edwards
2

Bem, eu decidi me sujar com isso sozinho.

A Microsoft se esforçou muito para sempre garantir que NotifyCollectionChangedEventArgs não tivesse nenhum dado ao chamar uma reinicialização. Estou assumindo que foi uma decisão de desempenho / memória. Se você estiver redefinindo uma coleção com 100.000 elementos, presumo que eles não quisessem duplicar todos esses elementos.

Mas visto que minhas coleções nunca têm mais de 100 elementos, não vejo problema nisso.

De qualquer forma, criei uma classe herdada com o seguinte 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
fonte
Isso é legal, mas provavelmente não funcionaria em nada além de um ambiente de confiança total. Refletir em campos privados requer confiança total, certo?
Paulo,
1
Por que você faria isso? Existem outras coisas que podem fazer com que a ação Reset seja acionada - só porque você desativou o método clear não significa que ele foi embora (ou deveria)
Orion Edwards
Abordagem interessante, mas a reflexão pode ser lenta.
cplotts de
2

O ObservableCollection, bem como a interface INotifyCollectionChanged, são claramente escritos com um uso específico em mente: construção de IU e suas características de desempenho específicas.

Quando você deseja notificações de mudanças na coleção, geralmente está interessado apenas em Adicionar e Remover eventos.

Eu uso a seguinte interface:

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

Também escrevi minha própria sobrecarga de Coleção, onde:

  • ClearItems levanta a remoção
  • Aumentos de InsertItem adicionados
  • RemoveItem levanta Removendo
  • SetItem levanta Remoção e Adicionado

Obviamente, AddRange também pode ser adicionado.

Rick Beerendonk
fonte
+1 por apontar que a Microsoft projetou ObservableCollection com um caso de uso específico em mente ... e com um olho no desempenho. Concordo. Deixou um buraco para outras situações, mas concordo.
cplotts de
-1 Posso estar interessado em todos os tipos de coisas. Freqüentemente, preciso do índice de itens adicionados / removidos. Posso querer otimizar a substituição. Etc. O design de INotifyCollectionChanged é bom. O problema que deve ser corrigido é que ninguém na MS o implementou.
Aleksandr Dubinsky
1

Eu estava apenas examinando alguns dos códigos de gráficos nos kits de ferramentas do Silverlight e WPF e percebi que eles também resolveram esse problema (de uma forma semelhante) ... e pensei em prosseguir e postar a solução deles.

Basicamente, eles também criaram um ObservableCollection derivado e substituíram ClearItems, chamando Remove em cada item sendo limpo.

Aqui está o 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
fonte
Só quero salientar que não gosto dessa abordagem tanto quanto daquela que marquei como resposta ... já que você recebe um evento NotifyCollectionChanged (com uma ação Remover) ... para CADA item sendo removido.
cplotts de
1

Este é um assunto quente ... porque, em minha opinião, a Microsoft não fez seu trabalho direito ... mais uma vez. Não me entenda mal, gosto da Microsoft, mas eles não são perfeitos!

Eu li a maioria dos comentários anteriores. Concordo com todos aqueles que pensam que a Microsoft não programou Clear () corretamente.

Na minha opinião, pelo menos, é preciso um argumento para possibilitar a separação de objetos de um evento ... mas também entendo o impacto disso. Então, pensei nessa solução proposta.

Espero que isso faça todos felizes, ou pelo menos, quase 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
fonte
Ainda acho que a Microsoft deve fornecer uma maneira de limpar com notificação. Ainda acho que eles erram o alvo por não fornecerem dessa forma. Desculpe ! Não estou dizendo que claro deve ser removido, se está faltando alguma coisa !!! Para obter baixo acoplamento, às vezes precisamos ser avisados ​​sobre o que foi removido.
Eric Ouellet,
1

Para mantê-lo simples, por que você não sobrescreve o método ClearItem e faz o que quiser lá, ou seja, Desanexar os itens do evento.

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

  rest of the code omitted
}

Simples, limpo e contido no código da coleção.

Stéphane
fonte
Isso é muito próximo do que eu realmente fiz ... veja a resposta aceita.
cplotts
0

Eu tive o mesmo problema e esta foi a minha solução. Parece funcionar. Alguém vê algum problema potencial com essa abordagem?

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

Aqui estão alguns outros métodos úteis em minha classe:

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);
}
hypehuman
fonte
0

Encontrei outra solução "simples" derivada de ObservableCollection, mas não é muito elegante porque usa Reflection ... Se você gostou, aqui está minha solução:

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

Aqui, salvo os elementos atuais em um campo de array no método ClearItems, então intercepto a chamada de OnCollectionChanged e sobrescrevo o campo privado e._oldItems (por meio de Reflections) antes de lançar base.OnCollectionChanged

Formentz
fonte
0

Você pode substituir o método ClearItems e gerar o evento com a ação Remove e 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 da System.Collections.ObjectModel.ObservableCollection<T>realização:

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
fonte
-4

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

Leia esta documentação com os olhos abertos e o cérebro ligado. A Microsoft fez tudo certo. Você deve digitalizar novamente sua coleção quando ela emitir uma notificação de redefinição para você. Você recebe uma notificação de redefinição porque adicionar / remover para cada item (sendo removido e adicionado de volta à coleção) é muito caro.

Orion Edwards está completamente certo (respeito, cara). Por favor, pense mais ao ler a documentação.

Dima
fonte
5
Na verdade, acho que você e o Orion estão corretos em sua compreensão de como a Microsoft o projetou para funcionar. :) Este design, entretanto, me causou problemas que eu precisava solucionar para a minha situação. Essa situação também é comum ... e por que eu postei essa pergunta.
cplotts de
Eu acho que você deve olhar para a minha pergunta (e resposta marcada) um pouco mais. Eu não estava sugerindo a remoção de todos os itens.
cplotts de
E para que conste, eu respeito a resposta de Orion ... Acho que estávamos apenas nos divertindo um com o outro ... pelo menos foi assim que eu entendi.
cplotts de
Uma coisa importante: você não precisa separar os procedimentos de tratamento de eventos dos objetos que está removendo. O desprendimento é feito automaticamente.
Dima,
1
Portanto, em resumo, os eventos não são desanexados automaticamente ao remover um objeto de uma coleção.
cplotts de
-4

Se ObservableCollectionnão estiver ficando claro, você pode tentar o código abaixo. pode te ajudar:

private TestEntities context; // This is your context

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