Table of Contents

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 ItemsChanged notification
  • 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
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