The aria::binding::ViewModel base class is the structural backbone of every Aria MVVM application. It provides lifecycle management, automatic cleanup, child composition, and structured concurrency — everything a ViewModel needs beyond raw reactive primitives.
Header: aria/binding/view_model.hpp
Why a Base Class?
Without a common base, every ViewModel reinvents the same bookkeeping:
- Who cleans up subscriptions when the VM dies?
- How do you guarantee in-flight async work finishes before teardown?
- How do parent–child VMs coordinate activation?
ViewModel solves all three. It is not a pure interface — it carries real state (is_active, subscription bag, child list, destroy hooks) and provides default implementations you can override.
Quick Start
public:
GreetingVm() : greet([this](const std::string& who) {
}) {
track(name.
on_changed([
this](
const std::string&) {
greeting.set("Hello, " + name.get() + "!");
}));
}
};
Base class for view models.
Definition view_model.hpp:32
Definition property.hpp:103
void set(const T &new_val)
Commit a new value.
Definition property.hpp:179
::aria::Subscription on_changed(std::function< void(const T &)> fn)
Run fn(new_value) every time the value changes.
Definition property.hpp:200
@ Command
Synchronous Command<Args...> execution.
Definition diagnostics.hpp:70
Three things happening:
- Property<T> — observable state. Changes propagate to subscribers.
- Command<T> — user-action entry point. Takes a parameter, executes a lambda.
- track() — registers a Subscription in the VM's bag. Destroyed automatically.
Lifecycle: Activate / Deactivate
Every ViewModel starts inactive. activate() and deactivate() control the lifecycle:
auto vm = std::make_shared<GreetingVm>();
assert(!vm->is_active().get());
vm->activate();
assert(vm->is_active().get());
vm->deactivate();
assert(!vm->is_active().get());
Override on_activate() / on_deactivate() to react:
public:
void on_activate() override {
}
void on_deactivate() override {
}
};
Guarantees:
| Property | Guarantee |
| Idempotent | Calling activate() twice does nothing on the second call |
| Atomic | is_active flips inside reactive::batch — observers see one flush |
| Ordered | on_activate() runs before is_active becomes true |
| Symmetric | deactivate() reverses: children first, then hook, then flag |
Child Composition
ViewModels form a tree. Activating a parent activates all children; deactivating propagates downward:
auto parent = std::make_shared<DashboardVm>();
auto sidebar = std::make_shared<SidebarVm>();
auto content = std::make_shared<ContentVm>();
parent->add_child(sidebar);
parent->add_child(content);
parent->activate();
parent->deactivate();
Typical pattern: a top-level ShellVm owns child VMs for each screen region.
Automatic Cleanup
Subscription Bag
track(subscription) adds a subscription to the VM's internal SubscriptionBag. When the VM is destroyed, every tracked subscription is released — no dangling callbacks.
public:
ProfileVm(DataService& svc) {
track(user_name.on_changed([&](const std::string&) {
svc.mark_dirty();
}));
}
};
Destroy Hooks
For cleanup that isn't a Subscription (closing file handles, unregistering observers):
public:
CameraVm() {
camera_ = open_camera();
add_destroy_hook([this] { camera_->close(); });
}
private:
Camera* camera_;
};
Destroy hooks fire in reverse registration order. Exceptions are routed through aria::report_callback_failure — they never escape the destructor.
Structured Concurrency with ViewModelScope
When a ViewModel launches async work, you need to guarantee it finishes (or cancels) before the VM dies. ViewModelScope ties a CoroutineScope to the VM's lifetime:
Header: aria/binding/view_model_scope.hpp
public:
PollingVm() {
scope_.attach(*this);
}
void start_polling() {
co_await schedule_after(timer_, 1s);
refresh_data();
}
});
}
private:
aria::binding::ViewModelScope scope_;
};
Definition cancellation.hpp:182
void throw_if_cancelled() const
Throw OperationCancelled if cancelled. Call at safe await points.
Definition cancellation.hpp:193
bool is_cancelled() const noexcept
Definition cancellation.hpp:188
When PollingVm is destroyed:
- The destroy hook calls scope_.cancel_and_join()
- All in-flight coroutines receive a cancellation signal
- The destructor waits (up to 5 s default) for them to exit
- Stuck coroutines are reported as leaks via the async error sink
Common Patterns
Property + Command (Synchronous)
The simplest ViewModel: observable state + user actions.
public:
aria::Command<> decrement{[
this]() { count.
set(count.
get() - 1); }};
aria::Command<> reset{[
this]() { count.
set(0); }};
};
Definition command.hpp:154
T get() const
Auto-tracked read.
Definition property.hpp:138
Property + Computed (Derived State)
Computed values auto-update when their dependencies change:
public:
aria::Property<int> people{1};
aria::Computed<double> tipAmount{[this] {
return bill.
get() * tipPercent.
get() / 100.0;
}};
aria::Computed<double> total{[this] {
return bill.
get() + tipAmount.
get();
}};
aria::Computed<double> perPerson{[this] {
return total.
get() / people.
get();
}};
};
T get() const
Return the cached value, ensuring it is up to date.
Definition computed.hpp:135
AsyncCommand (Three-State Async)
AsyncCommand manages is_running, error, and result properties automatically:
public:
: login(ui, worker, [this](
std::string user,
std::string pass)
->
aria::async::Task<LoginResult> {
co_await aria::async::sleep_for(500ms);
if (pass.empty()) throw std::runtime_error("Empty password");
co_return LoginResult{"Welcome, " + user};
})
{
scope_.attach(*this);
}
aria::async::AsyncCommand<LoginResult, std::string, std::string> login;
private:
aria::binding::ViewModelScope scope_;
};
Abstract executor interface — schedules a callable to run "somewhere".
Definition executor.hpp:36
auto schedule_on(IExecutor &exec)
Schedule a coroutine to resume on the given executor.
Definition executor.hpp:396
Definition validation_key.hpp:110
Shared Draft Across Child VMs
Multiple child VMs sharing a common data object:
struct WizardDraft {
aria::Property<std::string> theme{"Light"};
};
public:
std::shared_ptr<WizardDraft> draft;
explicit Step1Vm(std::shared_ptr<WizardDraft> d) : draft(std::move(d)) {}
};
public:
std::shared_ptr<WizardDraft> draft;
explicit Step2Vm(std::shared_ptr<WizardDraft> d) : draft(std::move(d)) {}
};
Cross-Platform Bridge (JNI / AppKit / UIKit)
The ViewModel stays in C++. Platform adapters observe Property changes and push them to native UI:
C++ ViewModel → Property.on_changed → JNI / ObjC bridge → Native UI
← Command.execute ← Button tap ← Native UI
See adapter guides for platform-specific wiring:
API Reference
Constructor / Destructor
| Method | Notes |
| ViewModel() | Default constructor. is_active starts false. |
| virtual ~ViewModel() | Runs destroy hooks in reverse order, cleans up bag and children. |
Lifecycle
| Method | Notes |
| Property<bool>& is_active() | Read/write activation flag. |
| virtual void on_activate() | Override to react to activation. Runs before is_active flips. |
| virtual void on_deactivate() | Override to react to deactivation. Runs before is_active flips. |
| void activate() | Idempotent activation. Propagates to children. |
| void deactivate() | Idempotent deactivation. Propagates to children. |
Composition
| Method | Notes |
| void add_child(shared_ptr<ViewModel>) | Register a child VM. Activated/deactivated with parent. |
| void track(Subscription) | Auto-release subscription on destruction. |
| void add_destroy_hook(function<void()>) | Run cleanup on destruction. Reverse order. Exceptions routed to error sink. |
Internals
| Member | Notes |
| SubscriptionBag& bag() | Protected access to the subscription bag. |
Type Alias
| Alias | Notes |
| using IViewModel = ViewModel | Compatibility alias for WPF/Avalonia/MAUI naming conventions. Despite the I prefix, this is a concrete base class with state. |
See Also