Skip to content

Getting Started

Nicolás Seijas edited this page Jul 28, 2026 · 3 revisions

Getting Started

The core package is netstandard2.0. Thus it operates with many .NET versions. The repository builds with .NET 10. The UI adapters are separate packages.


1. Install the packages

The packages are on NuGet. The current version is 0.5.1.

dotnet add package Rambla            # the core, with the source generators
dotnet add package Rambla.Wpf        # for WPF
dotnet add package Rambla.Avalonia   # for Avalonia
dotnet add package Rambla.Diagnostics  # optional. See Diagnostics.

Install the core package and one adapter package. The core package contains the [State] and [StateCommand] source generators.

2. Install a scheduler

The scheduler moves the flush to the UI thread. Install it one time, at the start of the application, on the UI thread.

Warning: if you do not install a scheduler, Rambla uses the ImmediateStateScheduler. That scheduler raises the notifications on the thread that writes the state. A background write then touches the UI from the wrong thread, and the UI framework makes an error.

For WPF:

// App.xaml.cs
using Rambla.Wpf;

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        DispatcherStateScheduler.InstallAsDefault();
    }
}

For Avalonia:

// App.axaml.cs
using Rambla.Avalonia;

public override void OnFrameworkInitializationCompleted()
{
    DispatcherStateScheduler.InstallAsDefault();
    base.OnFrameworkInitializationCompleted();
}

Both adapters post the flush at Background priority. Thus a high update rate does not stop the input and the render operations. To put a limit on the number of flushes in one second, refer to Schedulers.

3. Declare state with [State]

Do these steps:

  1. Make the class partial.
  2. Extend RamblaState.
  3. Put [State] on each backing field.
using Rambla;

public partial class MarketViewModel : RamblaState
{
    [State] private decimal _bid;
    [State] private decimal _ask;
    [State] private decimal _last;
    [State] private decimal _pnl;
}

The generator removes the first underscore and makes the first letter uppercase. The field _bid gives the property Bid. For each field, the generator writes this property:

public decimal Bid
{
    get => _bid;
    set => SetField(ref _bid, value);
}

If the code is not correct, the generator makes an error. The errors are RMB001 to RMB005. Examples are a class that is not partial, a static field, a readonly field, and a name that is already in use.

4. A complete WPF example

The view model uses the scheduler from step 2:

using Rambla;

public partial class TickerViewModel : RamblaState
{
    [State] private string _symbol = "BTC-USD";
    [State] private decimal _price;
    [State] private decimal _change;
}

Bind to the properties as you bind to any INotifyPropertyChanged object:

<StackPanel>
    <TextBlock Text="{Binding Symbol}" FontWeight="Bold" />
    <TextBlock Text="{Binding Price, StringFormat=N2}" FontSize="28" />
    <TextBlock Text="{Binding Change, StringFormat='{}{0:+0.00;-0.00}%'}" />
</StackPanel>

Warning: in WPF, Run.Text binds two-way by default. A two-way binding to a read-only property makes an error when the binding attaches. For a Run, always set Mode=OneWay.

Set the data context and start the feed:

public partial class MainWindow : Window
{
    private readonly TickerViewModel _vm = new();

    public MainWindow()
    {
        InitializeComponent();
        DataContext = _vm;
        _ = FeedAsync();
    }

    private async Task FeedAsync()
    {
        var random = new Random();
        while (true)
        {
            // This code runs on a background thread.
            _vm.Price = 60_000m + random.Next(-500, 500);
            _vm.Change = (decimal)((random.NextDouble() * 4) - 2);
            await Task.Delay(1);
        }
    }
}

The feed writes approximately 1000 times in one second. Rambla coalesces these writes into a small number of notifications.

5. Write from a background feed

The producer does not know about the UI thread:

async Task RunFeedAsync(TickerViewModel vm, IAsyncEnumerable<Quote> stream, CancellationToken token)
{
    await foreach (Quote quote in stream.WithCancellation(token))
    {
        vm.Price = quote.Price;
        vm.Change = quote.Change;
    }
}

If a value changes many times before the next flush, the UI receives only the last value. Refer to Philosophy for the limits of this behavior.

6. Group related writes in a batch

A batch gives notification coherence. The UI does not show a new Bid together with an old Ask.

using (vm.BeginUpdate())
{
    vm.Bid = quote.Bid;
    vm.Ask = quote.Ask;
    vm.Last = quote.Last;
}

// The lambda form does the same:
vm.Update(() =>
{
    vm.Bid = quote.Bid;
    vm.Ask = quote.Ask;
});

Warning: a batch does not give state atomicity. A reader on a different thread can see a new Bid together with an old Ask before the batch closes. For state atomicity, publish an immutable object. Refer to Core Semantics.

7. Apply a full quote

One method that applies a full quote in one batch is a good pattern:

public partial class MarketViewModel : RamblaState
{
    [State] private decimal _bid;
    [State] private decimal _ask;
    [State] private decimal _last;
    [State] private decimal _volume;

    public void Apply(Quote quote)
    {
        using (BeginUpdate())
        {
            Bid = quote.Bid;
            Ask = quote.Ask;
            Last = quote.Last;
            Volume = quote.Volume;
        }
    }
}

vm.Apply(quote);   // One flush for each quote, from any thread.

8. Calculate a derived value

V1 has no [DependsOn] attribute. Thus, calculate the derived value in the same batch as its inputs:

public void Apply(decimal bid, decimal ask)
{
    using (BeginUpdate())
    {
        Bid = bid;
        Ask = ask;
        Spread = ask - bid;   // One flush contains the three notifications.
    }
}

9. Collections

Use RamblaList<T> for a list and RamblaDictionary<TKey,TValue> for a keyed collection. Both accept writes from any thread. Both raise the minimum number of notifications for each flush.

var rows = new RamblaList<SymbolRow>();

rows.Batch(() =>
{
    rows.Add(new SymbolRow("BTC-USD"));
    rows.Add(new SymbolRow("ETH-USD"));
});

Warning: a read of a Rambla collection gives the visible contents, not the pending target. The new item is not in Count before the next flush. Refer to Collections for the full rules.

10. Async commands

Put [StateCommand] on an async method. The generator writes the command and the state of its run.

public partial class SearchViewModel : RamblaState
{
    // Generates: SearchCommand, IsSearching, SearchError, CancelSearchCommand
    [StateCommand(CancelPrevious = true)]
    private async Task SearchAsync(CancellationToken token)
        => Results = await _api.SearchAsync(Query, token);
}

Bind the button to SearchCommand. Refer to Async Commands.

11. Use Rambla in tests

Give the scheduler to the constructor. The ImmediateStateScheduler runs each flush immediately. Thus the test is deterministic.

using Rambla.Scheduling;

var vm = new MarketViewModel(ImmediateStateScheduler.Instance);
var changed = new List<string?>();
vm.PropertyChanged += (_, e) => changed.Add(e.PropertyName);

vm.Bid = 1.23m;

Assert.Contains(nameof(vm.Bid), changed);

To test the coalescing behavior, use a scheduler that collects the flushes and runs them when the test commands it. The repository contains an example with the name ManualStateScheduler.

12. Metrics

The metrics count the mutations, the flushes and the notifications. They are disabled by default. Enable them for one state or for all the states:

var vm = new MarketViewModel(scheduler, collectMetrics: true);
// or:
RamblaOptions.Default.CollectMetrics = true;

StateMetrics metrics = vm.Metrics;
Console.WriteLine($"{metrics.Mutations} mutations, {metrics.Notifications} notifications");

For rates, hot properties and recommendations, use the Rambla.Diagnostics package. Refer to Diagnostics.

13. Limit the refresh rate

The adapters post each flush as soon as the dispatcher is available. To put a maximum on the number of flushes in one second, use the ThrottlingStateScheduler:

// WPF. Keep the object and dispose it at shutdown.
_scheduler = DispatcherStateScheduler.InstallThrottledAsDefault(60);

RamblaOptions.Default.MaxRefreshRate gives the default limit. Refer to Schedulers.

Clone this wiki locally