|
Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
|
BindingEngine bridges the gap between C++ ViewModels and native platform views. It manages the lifecycle of every binding — VM→View and View→VM — and handles thread marshalling automatically.
Include: #include "aria/binding/binding_engine.hpp"
| Policy | VM→View Path | Use Case |
|---|---|---|
| Direct | Synchronous, same thread | Single-threaded apps, tests |
| SmartMarshal | Direct on UI thread, post from background | Production apps |
| AlwaysPost | Always post via dispatcher | Tests needing deterministic ordering |
Render any Property<T> into a read-only text view through a T -> std::string projection. This is the lightweight one-way alternative to a full bidirectional Converter when the view never writes back (labels, status lines):
Bind a Property<std::optional<T>> to a read-only text view. When the optional holds a value it is rendered through the projection; when it is std::nullopt the view shows empty_text (default: empty string):
Async-agnostic by design. Both helpers operate purely on Property<T>, so BindingEngine stays decoupled from aria-async at the API level (the engine never names an AsyncCommand type). They are the idiomatic way to render an AsyncCommand's observable properties — last_error_message (a Property<std::string>) with bind_text_projected, and last_result (a Property<std::optional<R>>) with bind_optional_text — without teaching BindingEngine about coroutines. See View-Destroy Cancellation.
Two-way bindings synchronize in both directions: VM changes update the view, and user input in the view writes back to the VM.
When the VM type doesn't match the view type, you have two options.
For a read-only label, pass a plain projection functor (T -> std::string). No parsing back, no Converter needed:
When the view must also write back, supply a Converter<T, std::string>:
On the View → Model direction a built-in converter reports unparseable input through try_to_model (returning std::nullopt) or by throwing ConversionError; in both cases the engine skips the model write, so the previous value stays authoritative rather than being clobbered with a default-constructed one.
All live in aria::binding::converters and return a Converter<T, U>:
| Factory | From | To |
|---|---|---|
| identity_string() | string | string |
| int_to_string() | int | string |
| double_to_string(int precision = 2) | double | string |
| bool_to_yes_no() | bool | string ("yes" / "no"; parses yes/true/1 and no/false/0) |
For anything else, construct a Converter<T, U> directly (or use bind_text_projected when the binding is one-way).
Two-way bindings create a potential loop: VM change → view update → view callback → VM set (same value). The engine suppresses this automatically:
Views can be destroyed before the engine. When a view is destroyed:
Called automatically in the engine's destructor.
Platform adapters implement IViewAdapter to teach the engine how to talk to native widgets:
See adapter guides for platform-specific implementations:
Every binding the engine ships, with its direction and the source types it accepts. Two rules explain the whole table:
| Method | Direction | Source | View channel |
|---|---|---|---|
| bind_text | Two-way | Property<string> | text input |
| bind_text_oneway | VM→View | Property<string> / Computed<string> | label text |
| bind_bool | Two-way | Property<bool> | checkbox / switch |
| bind_bool_oneway | VM→View | Property<bool> / Computed<bool> | checkbox / switch |
| bind_int | Two-way | Property<int> | spin box / slider |
| bind_int_oneway | VM→View | Property<int> / Computed<int> | spin box / progress bar |
| bind_int64 | Two-way | Property<int64_t> | 64-bit numeric input |
| bind_int64_oneway | VM→View | Property<int64_t> / Computed<int64_t> | 64-bit numeric display |
| bind_uint64 | Two-way | Property<uint64_t> | unsigned numeric input |
| bind_uint64_oneway | VM→View | Property<uint64_t> / Computed<uint64_t> | unsigned numeric display |
| bind_float | Two-way | Property<float> | slider / opacity |
| bind_float_oneway | VM→View | Property<float> / Computed<float> | slider / opacity |
| bind_double | Two-way | Property<double> | slider / double spin box |
| bind_double_oneway | VM→View | Property<double> / Computed<double> | numeric display |
| Method | Direction | Source | Notes |
|---|---|---|---|
| bind_text_projected | VM→View | any ReadOnlyReactive of T | renders via T → string; the everyday "formatted label" binding |
| bind_optional_text | VM→View | any ReadOnlyReactive of optional<T> | renders *opt via T → string; nullopt shows empty_text |
| bind_text_converted | Two-way | Property<T> | full Converter<T, string>, parses text back into T |
| bind_text_converted_oneway | VM→View | any ReadOnlyReactive of T | uses only the converter's to_view |
| Method | Direction | Source | Notes |
|---|---|---|---|
| bind_visible | VM→View | Property<bool> / Computed<bool> | inherently one-way — the view never writes visibility back |
| bind_enabled | VM→View | Property<bool> / Computed<bool> | inherently one-way |
| bind_command | View→VM | Command<Args...> | click → execute(args...); also drives enabled from can_execute(args...) |
| bind_view_lifetime | — | std::function<void()> | fires once on view-destroy (or engine teardown); use to cancel in-flight async work |
| adopt | — | Subscription | hands an arbitrary subscription to the view's per-view bucket, released on view-destroy |
When no binding fits, write the on_changed by hand and hand the resulting Subscription to adopt(view, std::move(sub)). That keeps the lifetime story identical to a real binding — released on view-destroy — and avoids the usual workaround of a long-lived subscription vector that never releases anything.
Composite labels are the common case: a binding has exactly one source, so a label reading two values either gets a Computed on the ViewModel that combines them (then it is a one-line bind_text_oneway), or stays a manual on_changed + adopt.
For HTTP input, construct the engine with the graph owner's dispatcher and BindingEngine::DispatchPolicy::SmartMarshal (or AlwaysPost). These policies apply to both binding directions, including converted text and commands. Direct requires callbacks to already run on the graph thread. Keep binding setup, teardown, Property and Command lifetimes on that thread. Posted input is discarded if its view binding is cleared or destroyed before delivery.