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 };
}
}
fonte
Respostas:
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
ListView
para exibir na tela.Se você limpar a lista e aumentar o
.Reset
evento, o desempenho é praticamente instantâneo, mas se você aumentar muitos.Remove
eventos individuais , o desempenho é péssimo, pois o WPF remove os itens um por um. Também usei.Reset
em meu próprio código para indicar que a lista foi reordenada, em vez de emitir milhares deMove
operações individuais . Tal como acontece com Clear, há uma grande queda de desempenho ao aumentar muitos eventos individuais.fonte
OldItems
ao 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.Reset
é para indicar uma operação cara, é muito provável que o mesmo raciocínio se aplique à cópia de toda a lista paraOldItems
.Reset
na verdade significa "O conteúdo da coleção foi limpo ." Consulte msdn.microsoft.com/en-us/library/…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:
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.
fonte
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:
Vantagens:
Não há necessidade de se inscrever em um evento adicional (conforme exigido pela resposta aceita)
Não gera um evento para cada objeto removido (algumas outras soluções propostas resultam em vários eventos Removidos).
O assinante só precisa verificar NewItems e OldItems em qualquer evento para adicionar / remover manipuladores de eventos conforme necessário.
Desvantagens:
Nenhum evento de reset
Pequeno (?) Overhead criando cópia da lista.
???
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:
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
fonte
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 ...foreach( NotifyCollectionChangedEventHandler handler in this.CollectionChanged )
Ifhandler.Target is CollectionView
, então você pode disparar o manipulador comAction.Reset
args, 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/529618Ok, 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:
Aqui está o código:
Este método de extensão simplesmente pega um
Action
que será invocado antes que a coleção seja limpa.fonte
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.Reset
args 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.fonte
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.
fonte
BrokenObservableCollection
, nãoTrulyObservableCollection
- você está entendendo mal o que a ação de reinicialização significa.ActuallyUsefulObservableCollection
. :)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.
fonte
É 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.
fonte
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).
fonte
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.
fonte
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:
fonte
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:
Também escrevi minha própria sobrecarga de Coleção, onde:
Obviamente, AddRange também pode ser adicionado.
fonte
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:
fonte
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
fonte
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.
Simples, limpo e contido no código da coleção.
fonte
Eu tive o mesmo problema e esta foi a minha solução. Parece funcionar. Alguém vê algum problema potencial com essa abordagem?
Aqui estão alguns outros métodos úteis em minha classe:
fonte
Encontrei outra solução "simples" derivada de ObservableCollection, mas não é muito elegante porque usa Reflection ... Se você gostou, aqui está minha solução:
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
fonte
Você pode substituir o método ClearItems e gerar o evento com a ação Remove e OldItems.
Parte da
System.Collections.ObjectModel.ObservableCollection<T>
realização:fonte
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.
fonte
Se
ObservableCollection
não estiver ficando claro, você pode tentar o código abaixo. pode te ajudar:fonte