Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
ViewModel

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

#include "aria/aria.hpp"
class GreetingVm : public aria::binding::ViewModel {
public:
GreetingVm() : greet([this](const std::string& who) {
name.set(who);
}) {
// Auto-clean: subscription dies with the VM
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:

  1. Property<T> — observable state. Changes propagate to subscribers.
  2. Command<T> — user-action entry point. Takes a parameter, executes a lambda.
  3. 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()); // starts inactive
vm->activate();
assert(vm->is_active().get()); // now active
vm->deactivate();
assert(!vm->is_active().get()); // back to inactive

Override on_activate() / on_deactivate() to react:

class HomeVm : public aria::binding::ViewModel {
public:
void on_activate() override {
// Start polling, warm caches, etc.
}
void on_deactivate() override {
// Pause animations, release transient resources, etc.
}
};

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(); // sidebar and content also activate
parent->deactivate(); // sidebar and content also 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.

class ProfileVm : public aria::binding::ViewModel {
public:
ProfileVm(DataService& svc) {
track(user_name.on_changed([&](const std::string&) {
svc.mark_dirty();
}));
// More subscriptions...
}
// No destructor boilerplate — bag cleans up automatically
};

Destroy Hooks

For cleanup that isn't a Subscription (closing file handles, unregistering observers):

class CameraVm : public aria::binding::ViewModel {
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

class PollingVm : public aria::binding::ViewModel {
public:
PollingVm() {
scope_.attach(*this); // Wire scope to VM's destroy hook
}
void start_polling() {
while (!tok.is_cancelled()) {
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
Definition task.hpp:78

When PollingVm is destroyed:

  1. The destroy hook calls scope_.cancel_and_join()
  2. All in-flight coroutines receive a cancellation signal
  3. The destructor waits (up to 5 s default) for them to exit
  4. Stuck coroutines are reported as leaks via the async error sink

Common Patterns

Property + Command (Synchronous)

The simplest ViewModel: observable state + user actions.

class CounterVm : public aria::binding::ViewModel {
public:
aria::Command<> increment{[this]() { count.set(count.get() + 1); }};
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:

class TipCalcVm : public aria::binding::ViewModel {
public:
aria::Property<int> tipPercent{15};
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:

class LoginVm : public aria::binding::ViewModel {
public:
: login(ui, worker, [this](std::string user, std::string pass)
-> aria::async::Task<LoginResult> {
co_await aria::async::schedule_on(worker);
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 signal.hpp:12
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"};
};
class Step1Vm : public aria::binding::ViewModel {
public:
std::shared_ptr<WizardDraft> draft;
explicit Step1Vm(std::shared_ptr<WizardDraft> d) : draft(std::move(d)) {}
};
class Step2Vm : public aria::binding::ViewModel {
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