ListFeed<T> Class
Reactive list with incremental change tracking for efficient UI updates.
Namespace: DevBitsLab.Feeds
Assembly: DevBitsLab.Feeds.dll
public sealed class ListFeed<T> : IListFeed<T>, IDisposable
Overview
ListFeed<T> provides a reactive collection that:
- Tracks changes — Add, Remove, Update, Move notifications
- Thread-safe — Internal locking for concurrent access
- Batch operations — Atomic multi-item updates
- Source sync — Track changes from another feed
Creating List Feeds
Empty
Creates an empty list in HasValue state.
var cartItems = ListFeed<CartItem>.Empty();
cartItems.Add(new CartItem { ProductId = 1 });
FromItems
Creates from existing items.
// From collection
var products = ListFeed<Product>.FromItems(existingProducts);
// From params
var statuses = ListFeed<string>.FromItems(
"Pending", "Active", "Completed");
Create (Auto-loading)
Loads items asynchronously, starts immediately.
var ordersFeed = ListFeed<Order>.Create(async ct => {
return await orderApi.GetOrdersAsync(ct);
});
ordersFeed.StateChanged += (s, e) => {
if (ordersFeed.HasValue) {
Console.WriteLine($"Loaded {ordersFeed.Count} orders");
}
};
CreateDeferred
Manual loading via RefreshAsync().
var reportData = ListFeed<ReportRow>.CreateDeferred(async ct => {
return await reportService.GenerateAsync(ct);
});
// Later
await reportData.RefreshAsync();
FromFeed (Change Tracking)
Creates from another feed with automatic diff tracking.
// Simple comparison
var productsFeed = Feed<IEnumerable<Product>>.Create(LoadProductsAsync);
var listFeed = ListFeed<Product>.FromFeed(productsFeed,
(p1, p2) => p1.Id == p2.Id);
// Key-based (better for updates)
var listFeed = ListFeed<Order>.FromFeed(ordersFeed, o => o.Id);
When source refreshes, only actual changes raise ItemsChanged.
Properties
| Property | Type | Description |
|---|---|---|
Value |
IReadOnlyList<T> |
Current items (never null) |
Count |
int |
Number of items |
State |
FeedState |
Current state |
IsLoading |
bool |
Loading in progress |
HasValue |
bool |
Data loaded |
HasError |
bool |
Error occurred |
Error |
Exception? |
Error details |
IsPaused |
bool |
Updates paused |
Events
ItemsChanged
Incremental change notifications.
listFeed.ItemsChanged += (sender, e) => {
e.Change.ApplyTo(observableCollection);
};
StateChanged
Feed state transitions.
listFeed.StateChanged += (s, e) => {
loadingSpinner.Visible = listFeed.IsLoading;
};
Operations
Adding Items
listFeed.Add(item); // Add to end
listFeed.Insert(0, item); // Insert at position
Removing Items
listFeed.RemoveAt(0); // Remove at index
listFeed.Remove(item); // Remove by value (returns bool)
Updating Items
// By index
listFeed.Update(0, newItem);
// By key
listFeed.Update(updatedItem, item => item.Id);
Moving Items
listFeed.Move(fromIndex, toIndex);
Replacing All Items
listFeed.Clear(); // Remove all
listFeed.Reset(newItems); // Replace with new items
Batch Updates
Perform multiple operations with a single notification:
listFeed.BatchUpdate(editor => {
editor.Add(item1);
editor.Add(item2);
editor.RemoveAt(0);
editor.Move(1, 3);
});
// Single ItemsChanged with BatchChange
Benefits:
- Single
ItemsChangednotification - Atomic changes
- Better performance
Synchronizing from Source Feed
With Item Comparer
var sourceFeed = Feed<IEnumerable<Product>>.Create(LoadAllAsync);
var listFeed = ListFeed<Product>.FromFeed(
sourceFeed,
(a, b) => a.Id == b.Id); // Compare by ID
With Key Selector (Recommended)
var listFeed = ListFeed<Order>.FromFeed(
ordersFeed,
order => order.Id);
Benefits of key selector:
- Detects updates (same key, different values)
- Tracks moves accurately
- Distinguishes add/remove from update
Thread Safety
ListFeed<T> is fully thread-safe with internal locking for all operations:
// Safe from any thread
Task.Run(() => listFeed.Add(item));
// Concurrent modifications are safe
await Task.WhenAll(
Task.Run(() => listFeed.Add(item1)),
Task.Run(() => listFeed.Add(item2)),
Task.Run(() => listFeed.RemoveAt(0))
);
Thread Safety Guarantees
| Operation | Thread-Safe |
|---|---|
Property access (Value, Count, State) |
✅ Yes |
Add / Insert |
✅ Yes |
Remove / RemoveAt |
✅ Yes |
Update |
✅ Yes |
Move |
✅ Yes |
Clear / Reset |
✅ Yes |
BatchUpdate |
✅ Yes (atomic) |
RefreshAsync |
✅ Yes |
Dispose |
✅ Yes |
| Event subscription | ✅ Yes |
Batch Updates Are Atomic
When using BatchUpdate, all operations execute atomically under a single lock:
// Other threads see either all changes or none
listFeed.BatchUpdate(editor => {
editor.Add(item1);
editor.Add(item2);
editor.RemoveAt(0);
});
Event Handler Threading
Important: Event handlers (
ItemsChanged,StateChanged) are invoked on the thread that triggered the change. Marshal to the UI thread when updating UI:
listFeed.ItemsChanged += (s, e) => {
// WPF
Dispatcher.Invoke(() => e.Change.ApplyTo(Products));
// WinUI / MAUI
DispatcherQueue.TryEnqueue(() => e.Change.ApplyTo(Products));
};
Reading During Modifications
The Value property returns a snapshot that is safe to enumerate:
// Safe - iterates over a snapshot
foreach (var item in listFeed.Value) {
Process(item);
}
// Concurrent modification won't affect the loop
Task.Run(() => listFeed.Add(newItem));
Performance
| Operation | Mean | Allocations |
|---|---|---|
Access Count |
~10 ns | 0 |
Access Value |
~10 ns | 0 |
Empty() |
~23 ns | 168 B |
FromItems(10) |
~35 ns | 232 B |
FromItems(100) |
~49 ns | 592 B |
FromItems(1000) |
~248 ns | 4,192 B |
Update |
~2.5 µs | 0 |
Add |
~3.3 µs | 0 |
BatchUpdate(10) |
~7 µs | 0 |
Common Patterns
MVVM binding (INotifyCollectionChanged)
ListFeed already implements INotifyCollectionChanged, so you can bind it directly without mirroring into an ObservableCollection:
public class ProductsViewModel : IDisposable {
public ListFeed<Product> Products { get; }
public ProductsViewModel() {
Products = ListFeed<Product>.Create(LoadProductsAsync);
}
public void Dispose() => Products.Dispose();
}
// XAML
// <ListView ItemsSource="{x:Bind ViewModel.Products}"> ... </ListView>
Shopping Cart
var cart = ListFeed<CartItem>.Empty();
// Add item or update quantity
void AddToCart(Product product, int qty) {
var existing = cart.Value.FirstOrDefault(c => c.ProductId == product.Id);
if (existing != null) {
cart.Update(existing with { Quantity = existing.Quantity + qty },
c => c.ProductId);
} else {
cart.Add(new CartItem { ProductId = product.Id, Quantity = qty });
}
}
// Total updates reactively
var totalFeed = cart.AggregateWithItemChanges(items =>
items.Sum(i => i.Price * i.Quantity));
Upsert by key
Update if found, otherwise add—handy for syncing lists by identity:
var products = ListFeed<Product>.FromItems(existingProducts);
// Returns true when updated, false when added
var wasUpdated = products.Upsert(
new Product { Id = 42, Name = "Updated" },
p => p.Id);
// wasUpdated == true if item 42 existed; otherwise item appended
You can also supply separate create/update delegates when you want to avoid constructing the item unless needed or need the current value:
var wasUpdated = products.Upsert(
key: 42,
createFunc: () => new Product { Id = 42, Name = "Created" },
updateFunc: current => current with { Name = current.Name + "!" },
keySelector: p => p.Id);
Filtered/Sorted Views
var products = ListFeed<Product>.Create(LoadAllAsync);
var activeProducts = products.Where(p => p.IsActive);
var sortedByPrice = products.OrderBy(p => p.Price);
var top10 = products.OrderByDescending(p => p.Sales).Take(10);
// All update automatically when source changes
See Also
IListFeed<T>— InterfaceListChange<T>— Change typesListFeedExtensions— LINQ operationsFeed<T>— Single-value feed