-
Notifications
You must be signed in to change notification settings - Fork 0
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
- 2. Install a scheduler
- 3. Declare state with
[State] - 4. A complete WPF example
- 5. Write from a background feed
- 6. Group related writes in a batch
- 7. Apply a full quote
- 8. Calculate a derived value
- 9. Collections
- 10. Async commands
- 11. Use Rambla in tests
- 12. Metrics
- 13. Limit the refresh rate
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.
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.
Do these steps:
- Make the class
partial. - Extend
RamblaState. - 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.
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.
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.
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.
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.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.
}
}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.
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.
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.
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.
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.
Rambla — state for real-time .NET desktop applications · MIT · github.com/nicoseijas/Rambla · This wiki follows ASD-STE100.
Use Rambla
Reference