Table of Contents

ListChange<T> Class

Represents changes to a list for incremental UI updates.

Namespace: DevBitsLab.Feeds
Assembly: DevBitsLab.Feeds.dll

public abstract record ListChange<T>

Overview

ListChange<T> describes what changed in a list, enabling:

  • Incremental updates — Apply only what changed
  • UI optimization — No full list re-renders
  • Animation support — Animate specific changes

Change Types

Type Description Properties
ItemAdded Item inserted Item, Index
ItemRemoved Item deleted Item, Index
ItemUpdated Item replaced OldItem, NewItem, Index
ItemMoved Item reordered Item, OldIndex, NewIndex
ListReset Entire list replaced NewItems
BatchChange Multiple changes Changes

Pattern Matching

Handle changes with switch expressions:

listFeed.ItemsChanged += (sender, e) => {
    switch (e.Change) {
        case ListChange<Product>.ItemAdded added:
            Console.WriteLine($"Added: {added.Item.Name} at {added.Index}");
            break;
            
        case ListChange<Product>.ItemRemoved removed:
            Console.WriteLine($"Removed: {removed.Item.Name} from {removed.Index}");
            break;
            
        case ListChange<Product>.ItemUpdated updated:
            Console.WriteLine($"Updated [{updated.Index}]: {updated.OldItem.Name} → {updated.NewItem.Name}");
            break;
            
        case ListChange<Product>.ItemMoved moved:
            Console.WriteLine($"Moved: {moved.Item.Name} from {moved.OldIndex} to {moved.NewIndex}");
            break;
            
        case ListChange<Product>.ListReset reset:
            Console.WriteLine($"Reset with {reset.NewItems.Count} items");
            break;
            
        case ListChange<Product>.BatchChange batch:
            Console.WriteLine($"Batch: {batch.Changes.Count} changes");
            foreach (var c in batch.Changes) ProcessChange(c);
            break;
    }
};

ApplyTo Method

Apply changes directly to any IList<T>:

var observable = new ObservableCollection<Product>();

listFeed.ItemsChanged += (s, e) => {
    e.Change.ApplyTo(observable);
};

Behavior by type:

Change ApplyTo Action
ItemAdded list.Insert(Index, Item)
ItemRemoved list.RemoveAt(Index)
ItemUpdated list[Index] = NewItem
ItemMoved Remove + Insert
ListReset Clear + AddRange
BatchChange Apply each in order

Factory Methods

Create changes programmatically:

var addChange = ListChange<Product>.Add(product, index);
var removeChange = ListChange<Product>.Remove(product, index);
var updateChange = ListChange<Product>.Update(oldItem, newItem, index);
var moveChange = ListChange<Product>.Move(item, oldIndex, newIndex);
var resetChange = ListChange<Product>.Reset(newItems);
var batchChange = ListChange<Product>.Batch(changesList);

Change Type Details

ItemAdded

public sealed record ItemAdded(T Item, int Index) : ListChange<T>

Properties:

  • Item — The item that was added
  • Index — Position where inserted

Example:

if (change is ListChange<Order>.ItemAdded added) {
    grid.InsertRow(added.Index, CreateRow(added.Item));
}

ItemRemoved

public sealed record ItemRemoved(T Item, int Index) : ListChange<T>

Properties:

  • Item — The item that was removed
  • Index — Position it was removed from

Example:

if (change is ListChange<Order>.ItemRemoved removed) {
    grid.RemoveRow(removed.Index);
    // removed.Item available for undo support
}

ItemUpdated

public sealed record ItemUpdated(T OldItem, T NewItem, int Index) : ListChange<T>

Properties:

  • OldItem — Previous value
  • NewItem — New value
  • Index — Position in list

Example:

if (change is ListChange<Product>.ItemUpdated updated) {
    // Animate price change
    if (updated.OldItem.Price != updated.NewItem.Price) {
        AnimatePriceChange(updated.Index, 
            updated.OldItem.Price, 
            updated.NewItem.Price);
    }
    grid.UpdateRow(updated.Index, updated.NewItem);
}

ItemMoved

public sealed record ItemMoved(T Item, int OldIndex, int NewIndex) : ListChange<T>

Properties:

  • Item — The item that moved
  • OldIndex — Original position
  • NewIndex — New position

Example:

if (change is ListChange<Task>.ItemMoved moved) {
    AnimateRowMove(moved.OldIndex, moved.NewIndex);
}

ListReset

public sealed record ListReset(IReadOnlyList<T> NewItems) : ListChange<T>

Properties:

  • NewItems — Complete new list contents

Example:

if (change is ListChange<Product>.ListReset reset) {
    grid.ClearRows();
    foreach (var item in reset.NewItems) {
        grid.AddRow(CreateRow(item));
    }
}

BatchChange

public sealed record BatchChange(IReadOnlyList<ListChange<T>> Changes) : ListChange<T>

Properties:

  • Changes — List of individual changes

Example:

if (change is ListChange<Product>.BatchChange batch) {
    grid.BeginUpdate();
    try {
        foreach (var c in batch.Changes) {
            ProcessChange(c);
        }
    } finally {
        grid.EndUpdate();
    }
}

ListChangedEventArgs<T>

Event args for ItemsChanged.

public sealed class ListChangedEventArgs<T> : EventArgs {
    public ListChange<T> Change { get; }
}

Note: Uses object pooling. Don't store references beyond the handler.

listFeed.ItemsChanged += (s, e) => {
    ProcessChange(e.Change);  // ✅ Use immediately
    // _lastChange = e;       // ❌ Don't store
};

Common Patterns

Full UI Sync

void SyncToUI(ListChange<Product> change) {
    switch (change) {
        case ListChange<Product>.ItemAdded a:
            InsertRow(a.Index, a.Item);
            break;
        case ListChange<Product>.ItemRemoved r:
            RemoveRow(r.Index);
            break;
        case ListChange<Product>.ItemUpdated u:
            UpdateRow(u.Index, u.NewItem);
            break;
        case ListChange<Product>.ItemMoved m:
            MoveRow(m.OldIndex, m.NewIndex);
            break;
        case ListChange<Product>.ListReset reset:
            RebuildAll(reset.NewItems);
            break;
        case ListChange<Product>.BatchChange batch:
            BeginBatchUpdate();
            foreach (var c in batch.Changes) SyncToUI(c);
            EndBatchUpdate();
            break;
    }
}

Undo Support

Stack<ListChange<Product>> _undoStack = new();

listFeed.ItemsChanged += (s, e) => {
    _undoStack.Push(e.Change);
};

void Undo() {
    if (_undoStack.TryPop(out var change)) {
        // Create inverse change
        var inverse = change switch {
            ListChange<Product>.ItemAdded a => 
                ListChange<Product>.Remove(a.Item, a.Index),
            ListChange<Product>.ItemRemoved r => 
                ListChange<Product>.Add(r.Item, r.Index),
            // etc.
        };
        inverse.ApplyTo(listFeed.Value as IList<Product>);
    }
}

See Also