Why Plain Lists Aren't Enough
INotifyPropertyChanged handles change notification for a single property, but it says nothing about items being added to or removed from a collection referenced by that property. If a ViewModel exposes a plain List<Customer> and code later calls customers.Add(newCustomer), no PropertyChanged event fires because the Customers property reference itself didn't change — only its contents did — so a ListBox or DataGrid bound to that list will not display the new item. ObservableCollection<T> solves this by implementing INotifyCollectionChanged, raising a CollectionChanged event with details about exactly what changed (Add, Remove, Replace, Move, or Reset) so that ItemsControls can update incrementally, inserting or removing just the affected visual row rather than rebuilding the entire list.
Cricket analogy: A plain list is like a printed team sheet handed out before the match — if a substitute fielder comes on mid-innings, the printed sheet in your hand never updates, unlike a live app that pushes the change.
CollectionChanged and UI Virtualization
When ObservableCollection<T> raises CollectionChanged, the event args (NotifyCollectionChangedEventArgs) include the Action (Add, Remove, Replace, Move, Reset) along with the affected item(s) and their index, which lets a bound ItemsControl like ListView or DataGrid apply a surgical, incremental UI update instead of a full re-layout. This matters enormously for performance with UI virtualization: a VirtualizingStackPanel only realizes visual containers for items currently scrolled into view, and an incremental Add/Remove lets it adjust the scrollable extent and realize/derealize just the affected rows, whereas calling something equivalent to Clear() followed by re-adding every item (a Reset action) forces the panel to discard and rebuild its entire visual state, which is visibly slower for large collections.
Cricket analogy: An incremental Add is like a live scorecard app inserting just the new ball-by-ball entry, whereas a full Reset is like the app tearing down and rebuilding the entire innings history from ball one.
public class OrdersViewModel : ViewModelBase
{
public ObservableCollection<Order> Orders { get; } = new ObservableCollection<Order>();
public void AddOrder(Order order)
{
// Raises CollectionChanged with Action=Add — bound ListView inserts one row.
Orders.Add(order);
}
public void ReplaceAll(IEnumerable<Order> freshOrders)
{
// Avoid Clear()+re-Add in a loop for large sets; batch via Reset once instead.
Orders.Clear();
foreach (var o in freshOrders) Orders.Add(o);
}
}Threading and Cross-Thread Updates
ObservableCollection<T> is not thread-safe for mutation from a background thread when it's bound to a UI element — the CollectionChanged event must be raised on the UI thread because the ItemsControl subscribed to it expects to touch its visuals there, and calling Add or Remove from a worker thread typically throws a NotSupportedException ('This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread'). The standard fix is to marshal the mutation back onto the UI thread via Dispatcher.Invoke/BeginInvoke (WPF) or DispatcherQueue (WinUI), or to use a specialized thread-safe alternative collection that batches and synchronizes updates internally, such as community libraries built specifically for this scenario.
Cricket analogy: Cross-thread mutation is like a substitute trying to physically update the paper scorecard from the boundary rope mid-play — only the designated scorer at the table (the UI thread) is allowed to touch it.
Mutating an ObservableCollection<T> bound to a UI control from a background thread typically throws a NotSupportedException at runtime. Always marshal Add/Remove/Clear calls back to the UI thread with Dispatcher.Invoke (WPF) or DispatcherQueue.TryEnqueue (WinUI), or collect results off-thread and apply them to the collection in one batch on the UI thread.
CollectionChanged's NotifyCollectionChangedAction.Reset is fired by Clear() and provides no information about what was removed, forcing bound ItemsControls to fully re-layout. For large replace operations, consider whether an incremental diff (removing only changed items and adding only new ones) would preserve scroll position and virtualization state better than a full Clear-and-rebuild.
- A plain List<T> exposed on a ViewModel does not notify bound ItemsControls when items are added or removed, only INotifyPropertyChanged does that for the property reference itself.
- ObservableCollection<T> implements INotifyCollectionChanged, raising CollectionChanged with the specific Action (Add, Remove, Replace, Move, Reset) and affected items/index.
- Incremental Add/Remove notifications let virtualized ItemsControls update just the affected visual rows instead of a full re-layout.
- Clear() raises a Reset action, which forces a full UI re-layout and loses virtualization/scroll-position efficiency for large collections.
- Mutating an ObservableCollection bound to a UI element from a background thread typically throws NotSupportedException.
- Background-thread updates must be marshaled to the UI thread via Dispatcher.Invoke/BeginInvoke or DispatcherQueue.
- For bulk replacements, batching changes and minimizing Reset actions preserves better UI performance and user scroll position.
Practice what you learned
1. Why doesn't a bound ListBox update when items are added to a plain List<T> property on a ViewModel?
2. What interface does ObservableCollection<T> implement to notify UI elements of content changes?
3. What NotifyCollectionChangedAction is raised when Clear() is called on an ObservableCollection<T>?
4. What typically happens if you call Add() on an ObservableCollection<T> bound to a UI control from a non-UI background thread?
5. Why is an incremental Add notification generally better for performance than a full Reset when adding one item to a large collection?
Was this page helpful?
You May Also Like
INotifyPropertyChanged
Understand the interface that powers change notification in MVVM, how it wires up data binding, and how to implement it efficiently.
Data Binding Fundamentals
Learn how data binding connects UI elements to underlying data sources so views update automatically without manual synchronization code.
Two-Way Binding Explained
A deep dive into TwoWay binding mechanics — how edits flow from the UI back to the ViewModel, when updates trigger, and common pitfalls.
Related Reading
Related Study Notes in Microsoft Technologies
Browse all study notesWindows 10 / UWP Development Study Notes
.NET · 30 topics
Microsoft TechnologiesWindows Batch Scripting Study Notes
Batch · 30 topics
Microsoft TechnologiesMFC (Microsoft Foundation Classes) Study Notes
C++ · 30 topics
Microsoft TechnologiesSilverlight Study Notes
.NET · 30 topics
Microsoft TechnologiesXAML Study Notes
.NET · 30 topics
Microsoft TechnologiesWPF (Windows Presentation Foundation) Study Notes
.NET · 30 topics