Bij het wissen van een ObservableCollection zijn er geen items in e.OldItems

Ik heb hier iets dat me echt overrompelt.

Ik heb een ObservableCollection van T die is gevuld met items. Ik heb ook een gebeurtenishandler gekoppeld aan de gebeurtenis CollectionChanged.

Wanneer u de verzameling Wis, veroorzaakt dit een CollectionChanged-gebeurtenis waarbij e.Action is ingesteld op NotifyCollectionChangedAction.Reset. Oké, dat is normaal. Maar wat raar is, is dat noch e.OldItems of e.NewItems iets in zich heeft. Ik zou verwachten dat e.OldItems gevuld zouden zijn met alle items die uit de collectie zijn verwijderd.

Heeft iemand anders dit gezien? En zo ja, hoe zijn ze er omheen gekomen?

Enige achtergrond: ik gebruik de gebeurtenis CollectionChanged om een ​​andere gebeurtenis toe te voegen aan en los te koppelen van een andere gebeurtenis en als ik dus geen items in e.OldItems krijg… kan ik die gebeurtenis niet loskoppelen.

p>

VERDUIDELIJKING:
Ik weet wel dat de documentatie niet ronduitstelt dat het zich op deze manier moet gedragen. Maar voor elke andere actie stelt het me op de hoogte van wat het heeft gedaan. Dus mijn veronderstelling is dat het me ook zou vertellen … in het geval van Clear/Reset.

Hieronder staat de voorbeeldcode als u deze zelf wilt reproduceren. Allereerst de 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>

Vervolgens de code achter:

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

Antwoord 1, autoriteit 100%

Er wordt niet beweerd dat het de oude items bevat, omdat Reset niet betekent dat de lijst is gewist

Het betekent dat er iets dramatisch heeft plaatsgevonden, en de kosten van het uitwerken van het toevoegen/verwijderen zouden hoogstwaarschijnlijk hoger zijn dan de kosten van het gewoon opnieuw scannen van de lijst… dus dat is wat u zou moeten doen.

p>

MSDN suggereert een voorbeeld van de hele collectie die opnieuw wordt gesorteerd als kandidaat voor reset.

Te herhalen. Reset betekent niet duidelijk, het betekent dat Uw veronderstellingen over de lijst nu ongeldig zijn. Behandel het alsof het een geheel nieuwe lijst is. Clear is hier een voorbeeld van, maar er kunnen ook andere zijn.

Enkele voorbeelden:
Ik heb zo’n lijst gehad met veel items erin, en het is gekoppeld aan een WPF ListViewom op het scherm weer te geven.
Als je de lijst wist en de gebeurtenis .Resetverhoogt, is de prestatie vrijwel onmiddellijk, maar als je in plaats daarvan veel individuele .Remove-gebeurtenissen verhoogt, is de prestatie verschrikkelijk, omdat WPF verwijdert de items één voor één.
Ik heb ook .Resetin mijn eigen code gebruikt om aan te geven dat de lijst opnieuw is gesorteerd, in plaats van duizenden afzonderlijke Move-bewerkingen uit te voeren. Net als bij Clear is er een grote prestatiehit bij het verhogen van veel individuele evenementen.


Antwoord 2, autoriteit 51%

We hadden hier hetzelfde probleem. De actie Reset in CollectionChanged omvat niet de OldItems. We hadden een tijdelijke oplossing: we gebruikten in plaats daarvan de volgende uitbreidingsmethode:

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

We hebben uiteindelijk de functie Clear() niet ondersteund en hebben een NotSupportedException in de CollectionChanged-gebeurtenis gegooid voor Reset-acties. De RemoveAll activeert een Remove-actie in de CollectionChanged-gebeurtenis, met de juiste OldItems.


Antwoord 3, autoriteit 30%

Ok, ik weet dat dit een heel oude vraag is, maar ik heb een goede oplossing voor het probleem bedacht en ik dacht dat ik die zou delen.
Deze oplossing is geïnspireerd op veel van de geweldige antwoorden hier, maar heeft de volgende voordelen:

  • Het is niet nodig om een ​​nieuwe klasse te maken en methoden uit ObservableCollection te overschrijven
  • Knoei niet met de werking van NotifyCollectionChanged (dus geen gedoe met Reset)
  • Maakt geen gebruik van reflectie

Hier is de code:

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

Deze uitbreidingsmethode voert gewoon een Actionuit die wordt aangeroepen voordat de verzameling wordt gewist.


Antwoord 4, autoriteit 17%

Ok, hoewel ik nog steeds zou willen dat ObservableCollection zich gedroeg zoals ik wilde … de onderstaande code is wat ik uiteindelijk deed. Kortom, ik heb een nieuwe verzameling T gemaakt met de naam TrulyObservableCollection en heb de ClearItems-methode genegeerd die ik vervolgens heb gebruikt om een ​​Clearing-gebeurtenis te genereren.

In de code die deze TrulyObservableCollection gebruikt, gebruik ik deze Clearing-gebeurtenis om door de items die zich op dat moment nog in de collectie bevindente doorlopen om de detach uit te voeren op de gebeurtenis die ik wilde loskoppelen van.

Ik hoop dat deze aanpak ook iemand anders helpt.

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

Antwoord 5, autoriteit 9%

Ik heb dit op een iets andere manier aangepakt, omdat ik me bij één evenement wilde registreren en alle toevoegingen en verwijderingen in de gebeurtenishandler wilde afhandelen. Ik begon met het negeren van de verzameling gewijzigde gebeurtenis en het omleiden van reset-acties naar verwijderingsacties met een lijst met items. Dit ging allemaal mis omdat ik de waarneembare collectie gebruikte als een itembron voor een collectieweergave en kreeg “Bereikacties niet ondersteund”.

Ik heb eindelijk een nieuw evenement gemaakt met de naam CollectionChangedRange, dat werkt zoals ik verwachtte dat de ingebouwde versie zou werken.

Ik kan me niet voorstellen waarom deze beperking zou worden toegestaan ​​en ik hoop dat dit bericht op zijn minst anderen ervan weerhoudt om door de doodlopende weg te gaan die ik deed.

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

Antwoord 6, autoriteit 6%

Dit is hoe ObservableCollection werkt, je kunt dit omzeilen door je eigen lijst buiten de ObservableCollection te houden (toevoegen aan de lijst als de actie Toevoegen is, verwijderen als de actie Verwijderen is enz.), dan kun je alle verwijderde items krijgen ( of toegevoegde items) wanneer actie Reset is door uw lijst te vergelijken met de ObservableCollection.

Een andere optie is om je eigen klasse te maken die IList en INotifyCollectionChanged implementeert, dan kun je gebeurtenissen binnen die klasse koppelen en loskoppelen (of OldItems op Clear zetten als je wilt) – het is echt niet moeilijk, maar het is veel typen.


Antwoord 7, autoriteit 6%

Voor het scenario van het koppelen en loskoppelen van gebeurtenishandlers aan de elementen van de ObservableCollection is er ook een “client-side” oplossing. In de gebeurtenisafhandelingscode kunt u met de methode Bevat controleren of de afzender in de ObservableCollection staat. Pro: je kunt met elke bestaande ObservableCollection werken. Nadelen: de methode Bevat werkt met O(n) waarbij n het aantal elementen in de ObservableCollection is. Dit is dus een oplossing voor kleine ObservableCollections.

Een andere “client-side” oplossing is om een ​​event handler in het midden te gebruiken. Registreer gewoon alle gebeurtenissen bij de gebeurtenishandler in het midden. Deze event handler brengt op zijn beurt de echte event handler op de hoogte via een callback of een event. Als er een Reset-actie plaatsvindt, verwijder dan de callback of gebeurtenis, maak een nieuwe gebeurtenishandler in het midden en vergeet de oude. Deze aanpak werkt ook voor grote ObservableCollections. Ik heb dit gebruikt voor het PropertyChanged-evenement (zie onderstaande code).

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

Antwoord 8, autoriteit 4%

Kijkend naar de NotifyCollectionChangedEventArgs, het lijkt erop dat OldItems alleen items bevat die zijn gewijzigd als gevolg van de actie Vervangen, Verwijderen of Verplaatsen. Het geeft niet aan dat het iets op Clear zal bevatten. Ik vermoed dat Clear de gebeurtenis activeert, maar de verwijderde items niet registreert en de Remove-code helemaal niet aanroept.


Antwoord 9, autoriteit 4%

Nou, ik heb besloten om er zelf vies van te worden.

Microsoft heeft VEEL werk gestoken in het altijd zorgen dat de NotifyCollectionChangedEventArgs geen gegevens heeft bij het aanroepen van een reset. Ik neem aan dat dit een prestatie/geheugen beslissing was. Als je een verzameling met 100.000 elementen reset, neem ik aan dat ze niet al die elementen wilden dupliceren.

Maar aangezien mijn collecties nooit meer dan 100 elementen bevatten, zie ik daar geen probleem in.

Hoe dan ook, ik heb een geërfde klasse gemaakt met de volgende methode:

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

Antwoord 10, autoriteit 4%

De ObservableCollection en de INotifyCollectionChanged-interface zijn duidelijk geschreven met een specifiek gebruik in gedachten: het bouwen van een gebruikersinterface en zijn specifieke prestatiekenmerken.

Als je meldingen van collectiewijzigingen wilt, ben je over het algemeen alleen geïnteresseerd in het toevoegen en verwijderen van evenementen.

Ik gebruik de volgende 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;
    }
}

Ik heb ook mijn eigen overload aan collectie geschreven waar:

  • ClearItems verhoogt Verwijderen
  • InsertItem verhogingen toegevoegd
  • RemoveItem verhoogt Verwijderen
  • SetItem verhoogt Verwijderen en Toegevoegd

Natuurlijk kan AddRange ook worden toegevoegd.


Antwoord 11, autoriteit 2%

Ik was net een deel van de kaartcode in de Silverlight- en WPF-toolkits aan het doornemen en merkte dat ze dit probleem ook hadden opgelost (op een soort vergelijkbare manier) … en ik dacht dat ik door zou gaan en hun oplossing zou posten.

In feite hebben ze ook een afgeleide ObservableCollection gemaakt en ClearItems overschreven, waarbij ze Remove aanroepen bij elk item dat wordt gewist.

Hier is de code:

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

12

U kunt de methode van Clearitems overschrijven en het evenement verhogen met het verwijderen van actie en Outitems.

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

Onderdeel van System.Collections.ObjectModel.ObservableCollection<T>realisatie:

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[]";
}

Antwoord 13

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

Lees deze documentatie met je ogen open en je hersenen ingeschakeld.
Microsoft heeft alles goed gedaan. U moet uw verzameling opnieuw scannen wanneer deze een Reset-melding voor u genereert. Je krijgt een Reset-melding omdat het te duur is om Toevoegen/Verwijderen voor elk item (verwijderd uit en weer toegevoegd aan de collectie) te gebruiken.

Orion Edwards heeft helemaal gelijk (respect, man). Denk alsjeblieft ruimer na bij het lezen van de documentatie.


Antwoord 14

Als je ObservableCollectionniet duidelijk wordt, kun je onderstaande code proberen. het kan je helpen:

private TestEntities context; // This is your context
context.Refresh(System.Data.Objects.RefreshMode.StoreWins, context.UserTables); // to refresh the object context

Other episodes