Skip to content

Collections

Nicolás Seijas edited this page Jul 28, 2026 · 1 revision

Collections

ObservableCollection<T> is not thread-safe. It also raises one notification for each change. Rambla has two collections that accept writes from any thread and raise the minimum number of notifications for each flush.

  • RamblaList<T> — an ordered list.
  • RamblaDictionary<TKey,TValue> — a keyed collection with a stable order.

Both types are in the core package. Both use the same scheduler and the same flush model as RamblaState.

The most important rule

A read gives the visible contents, not the pending target.

The visible contents always agree with the notifications that Rambla raised. WPF and Avalonia need this condition. Thus a new item is not in Count before the next flush.

var rows = new RamblaList<string>(ImmediateStateScheduler.Instance);

rows.Add("BTC-USD");
// With a deferred scheduler, rows.Count is 0 here.
// After the flush, rows.Count is 1.

This behavior is different from RamblaState. A property getter of a RamblaState gives the new value immediately. A collection cannot do this without a violation of the rule above.

RamblaList<T>

using Rambla;

var rows = new RamblaList<SymbolRow>();          // uses the default scheduler
var seeded = new RamblaList<int>(scheduler, new[] { 1, 2, 3 });

Mutators

Member Function
Add(item) Puts the item at the end.
Insert(index, item) Puts the item at the index.
Remove(item) Removes the first equal item. Gives true if it was in the pending target.
RemoveAt(index) Removes the item at the index.
Replace(index, item) Replaces the item at the index.
Clear() Removes all the items.
ReplaceSnapshot(items) Replaces the full contents.
Batch(action) Runs the action in one batch.
BeginUpdate() Opens a batch. Dispose the scope to close it.

Readers

Count, the indexer and the enumeration give the visible contents. The non-generic IList view is read-only. Thus WPF uses a virtualized ListCollectionView for a large list.

The flush

The flush compares the previous visible contents with the pending target. Then it raises the minimum number of Add, Remove and Replace notifications. If more than ResetThreshold items changed, the flush raises one Reset notification. The default value of ResetThreshold is 32.

rows.ResetThreshold = 64;

A batch that adds an item and then removes the same item raises no notification.

ReplaceSnapshot

ReplaceSnapshot reads the full sequence before it changes the collection. Then it calculates the minimum difference.

rows.ReplaceSnapshot(newRows);   // Only the true changes make notifications.

If the sequence makes an error during the read operation, the collection does not change.

RamblaDictionary<TKey,TValue>

The dictionary obeys all the rules of the list. It has these additional rules.

var quotes = new RamblaDictionary<string, decimal>();

quotes.Batch(() =>
{
    quotes["BTC-USD"] = 60_000m;
    quotes["ETH-USD"] = 3_000m;
});
  1. The order is the insertion order. A new key goes to the end. An update of a key keeps the position of that key. Thus the notifications carry correct indexes, and you can bind an ItemsControl to the dictionary.
  2. The last value of a key wins. If your code writes one key many times before the flush, the flush raises a maximum of one Replace notification.
  3. The mutators use the pending target. Add makes an error if the key is already in the pending target. TryAdd gives false in that condition. Remove gives true if the key was in the pending target.
  4. The readers use the visible contents. These members are this[key], TryGetValue, ContainsKey, Keys, Values and the enumeration.
  5. ReplaceSnapshot is atomic. A duplicate key or an error during the read operation leaves the dictionary unchanged.

Row objects

Keep one RamblaState object for each row. Then the collection changes only when your application adds or removes a row. The fields of the row change at the high rate.

public partial class SymbolRow : RamblaState
{
    [State] private decimal _bid;
    [State] private decimal _ask;

    public SymbolRow(string symbol, IStateScheduler scheduler) : base(scheduler)
        => Symbol = symbol;

    public string Symbol { get; }
}

This structure is the structure of the market dashboard sample.

Limits of V1

  • The collections do not raise Move notifications. A change of the order gives Replace notifications.
  • The diagnostics package does not give per-property data for a collection.

Refer to Core Semantics for the full contract.

Clone this wiki locally