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 addedIndex— 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 removedIndex— 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 valueNewItem— New valueIndex— 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 movedOldIndex— Original positionNewIndex— 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
IListFeed<T>— List feed interfaceListFeed<T>— ImplementationListFeedExtensions— LINQ operations