docs
Loading...
Searching...
No Matches
Context

#include <Ultralight/dom/data/Context.h>

Overview

Data-binding context for attached Views.

A Context registers your C++ instances under names that HTML markup can reference, exposing their fields and actions to the pages loaded by each attached View. Your markup declares where values appear and which interactions trigger native actions, keeping page presentation separate from your application's data.

Calling Sync() once per frame delivers input from the page to your native handlers and broadcasts changed values in a single batch to keep those pages current.

This example binds a player instance to a context and attaches it to a View:

struct Player {
int health = 80;
int potions = 3;
void UsePotion();
static constexpr auto schema = dd::Schema(
dd::Field("health", &Player::health),
dd::Field("potions", &Player::potions),
dd::Action("usePotion"));
};
Player player;
dd::Context ctx = dd::Context::Create();
dd::Binding binding = ctx.Bind("player", player) // the page calls it "player"
.OnAction<"usePotion">(&Player::UsePotion); // the button below calls this
if (!ctx.AttachTo(view.get()))
Log("couldn't attach the data context");
view->LoadURL("file:///hud.html");
// Once per frame:
ctx.Sync(); // runs UsePotion() if clicked, then updates the page
std::string schema() const
Get the schema of every bound instance as JSON, with their current values.
Definition Context.h:826

The loaded page references bound fields and click actions directly in markup:

<p>Health: {{player.health}}</p>
<button type="button" ul-on:click="player.usePotion">
Drink ({{player.potions}} left)
</button>

Binding Instances

Calling Bind() registers an instance under a binding name that forms the root of every markup path on the page.

Always store the returned dom::data::Binding handle alongside your data, because destroying it unbinds the instance immediately (see dom::data::Binding).

A context can bind an instance by reference, by value, or through a smart pointer:

auto ally = std::make_shared<Player>();
dd::Binding b1 = ctx.Bind("player", player); // borrows player
dd::Binding b2 = ctx.Bind("preview", Player {}); // owns its own Player
dd::Binding b3 = ctx.Bind("ally", ally); // shares ownership of *ally

Each binding form defines who keeps the bound instance alive:

  • Binding by reference borrows the instance. Native code retains ownership and must keep the instance alive while it remains bound.
  • Binding by value moves ownership into the binding. The dom::data::Binding owns the instance and destroys it when it unbinds.
  • Binding through a smart pointer manages lifetime automatically. An owning pointer keeps the instance alive, while an expired weak pointer skips updates and leaves the last values on the page.

Attaching to Views

Call AttachTo() to connect a Context to a View. You only attach once– every page that View loads receives the bindings automatically, including pages restored from the back-forward cache.

A single Context can attach to multiple Views, and a View can hold bindings from multiple Contexts. One call to Sync() synchronizes all attached Views at once.

Data bindings work only in a View's main frame.

Origin Rules

By default, only local file:// pages and pages loaded with View::LoadHTML() receive bindings.

Passing custom origin rules in AttachOptions replaces the default policy completely. To allow local files alongside remote origins, include file://* in the rule list (see OriginRules for pattern syntax).

Pages loaded with View::LoadHTML() without a URL have an opaque origin that no custom rule matches, so they receive bindings only under the default policy. When loaded with an explicit URL, they match against that URL's origin.

Home Thread

The thread that calls Create() becomes the home thread for that Context. In a single-threaded application, this is the Renderer's thread. You bind instances, register handlers, and call Sync() on this thread. When you modify bound data, make those changes on the home thread between calls to Sync().

When multiple worker threads need to modify bound data, give each thread its own Context. If the thread that calls Sync() changes over time (such as across worker threads in a job system), call CreateThreadSafe() instead so any thread can bind data, register handlers, and call Sync().

Finding Mistakes

A mistake in markup drops only the broken binding, allowing the rest of the page to bind normally.

When developer mode is on (Config::diagnostics), the library outputs warnings for markup mistakes to the native Logger and the page's console.

Note
Keep calling Sync() while the page is visible, even when your game is paused, so actions and change requests from the page continue to reach native handlers.
Note
dom::data::Context is unrelated to js::Context.
See also
dom::data::Binding, dom::data::TypeTraits, dom::data::Schema(), PostTask(), DefineFormat()

Static Public Member Functions

static Context Create ()
 Create a Context whose home thread is the calling thread.
static Context CreateThreadSafe ()
 Create a Context that any thread can use.
static Context Adopt (ULDOMDataContext handle)
 Wrap a C handle you own, taking ownership of it.
static Context FromBorrowed (ULDOMDataContext handle)
 Wrap a C handle someone else owns, without owning the context.

Public Member Functions

 Context (const Context &)=delete
Context & operator= (const Context &)=delete
 Context (Context &&other) noexcept
 Move constructor (other becomes empty).
Context & operator= (Context &&other) noexcept
 Move assignment (other becomes empty).
 ~Context ()
 Destroy this handle.
 operator bool () const
 Whether or not this Context is valid (false after a move or LeakRef()).
template<typename T>
requires (Described<T> && !std::is_const_v<T>)
Binding< T > Bind (std::string_view name, T &instance)
 Bind an instance of a described type under a binding name.
template<typename T>
requires Described<T>
Binding< T > Bind (std::string_view name, const T &instance)
 Bind a const instance under a binding name.
template<typename T>
requires (Described<std::remove_cvref_t<T>> && !std::is_lvalue_reference_v<T>)
Binding< std::remove_cvref_t< T > > Bind (std::string_view name, T &&instance)
 Bind an instance by value under a binding name.
template<typename H>
requires BindableHolder<H>
Binding< std::remove_cv_t< typename HolderTraits< std::remove_cvref_t< H > >::element_type > > Bind (std::string_view name, H &&holder)
 Bind an instance through a smart pointer under a binding name.
bool AttachTo (View *view, const AttachOptions &options={})
 Attach this Context to a View.
void DetachFrom (View *view)
 Detach this Context from a View.
void Sync ()
 Deliver input from the page to your handlers and send your changes to the pages.
bool PostAction (std::string_view name)
 Send an action to a bound instance's handler as if the page fired it.
template<typename P>
requires Described<std::remove_cvref_t<P>>
bool PostAction (std::string_view name, const P &payload)
 Send an action with a payload struct (see the payloadless overload).
template<typename P>
requires (!Described<std::decay_t<P>> && ScalarPayload<std::decay_t<P>>)
bool PostAction (std::string_view name, const P &payload)
 Send an action with a single-value payload (see the payloadless overload).
template<typename F>
requires (std::is_invocable_v<std::decay_t<F>&> || std::is_invocable_v<std::decay_t<F>&, Document>)
void PostTask (F &&callback)
 Run a callable on the Renderer's thread once the pages show your latest data.
template<typename Fn>
Context & DefineFormat (std::string_view name, Fn &&fn)
 Define a formatter for {{path|name}} text.
Context & UndefineFormat (std::string_view name)
 Remove a formatter.
uint64_t generation () const
 Get the generation of the last update Sync() sent to the pages.
void set_action_queue_capacity (uint32_t capacity)
 Set how many actions can wait for the next Sync().
uint32_t action_queue_capacity () const
 Get how many actions can wait for the next Sync().
std::string schema () const
 Get the schema of every bound instance as JSON, with their current values.
bool DumpSchema (const char *utf8_path, DumpAt when=DumpAt::NextSync)
 Write the JSON from schema() to a file.
ULDOMDataContext raw () const
 Get the C handle, for passing to the <Ultralight/CAPI/CAPI_DOMData.h> functions.
ULDOMDataContext LeakRef ()
 Give up ownership of the C handle and return it.

Constructor & Destructor Documentation

◆ Context() [1/2]

Context ( const Context & )
delete

◆ Context() [2/2]

Context ( Context && other)
inlinenoexcept

Move constructor (other becomes empty).

Parameters
otherThe Context to move from.

◆ ~Context()

~Context ( )
inline

Destroy this handle.

Destroying the Context detaches it from every View (the pages drop its bindings the same way DetachFrom() does) and destroys its bindings, handlers, and formatters (and anything they capture) right here, on the calling thread. So destroy it on its home thread. A Binding that outlives it stays safe to use, but its instance is no longer read.

Note
A Context from FromBorrowed() doesn't own the context, so destroying it changes nothing, and neither does destroying one while another owning handle exists.

Member Function Documentation

◆ action_queue_capacity()

uint32_t action_queue_capacity ( ) const
inline

Get how many actions can wait for the next Sync().

Returns
Returns the limit (0 for an empty Context).
Note
Safe to call from any thread.

◆ Adopt()

Context Adopt ( ULDOMDataContext handle)
inlinestatic

Wrap a C handle you own, taking ownership of it.

Parameters
handleA handle from the C API that you would otherwise destroy with ulDestroyDOMDataContext() (NULL gives an empty Context).
Returns
Returns a Context that destroys handle when it's done.

◆ AttachTo()

bool AttachTo ( View * view,
const AttachOptions & options = {} )
inlinenodiscard

Attach this Context to a View.

Every page the View loads then gets this Context's bindings, starting with the current page during a later Renderer::Update(). The View keeps the Context attached until you detach it or destroy its last owning Context.

A page gets the bindings only if the origin rules allow it. With no rules, only your application's own content does (local pages and pages loaded with View::LoadHTML()). Rules have the scheme://host[:port] form (see OriginRules for the full syntax), and they replace that default, so add file://* if you still want local pages:

if (!ctx.AttachTo(view.get(), { .origin_rules = { "https://*.mygame.com", "file://*" } }))
Log("an origin rule failed to parse");

A page whose origin isn't allowed shows its authored markup, and its controls send no input. A page loaded with View::LoadHTML() without a URL has an opaque origin– no rule matches it and no filter allows one, so it gets bindings only with the default rules. Attaching an already attached Context replaces its flags and rules.

Parameters
viewThe View to attach to.
optionsThe attach flags and the origin rules (see AttachOptions).
Returns
Returns true on success, or false if view is nullptr, this Context is empty, a flag is unknown, or a rule failed to parse. An earlier attachment stays as it was. An unknown flag or a rule that failed to parse also logs a warning that says why.
Note
Safe to call from any thread.
See also
DetachFrom()

◆ Bind() [1/4]

template<typename T>
requires Described<T>
Binding< T > Bind ( std::string_view name,
const T & instance )
inlinenodiscard

Bind a const instance under a binding name.

This works like the non-const overload, except that handlers which are member functions of the bound type (eg, OnAction<"respawn">(&HUD::Respawn)) do nothing, since they would change the instance. Register a lambda or another receiver instead.

Parameters
nameThe binding name pages use.
instanceThe instance to bind. Ownership remains with the caller.
Returns
Returns the Binding (empty if name isn't a valid binding name).

◆ Bind() [2/4]

template<typename H>
requires BindableHolder<H>
Binding< std::remove_cv_t< typename HolderTraits< std::remove_cvref_t< H > >::element_type > > Bind ( std::string_view name,
H && holder )
inlinenodiscard

Bind an instance through a smart pointer under a binding name.

holder can be a std::shared_ptr, RefPtr, or moved std::unique_ptr. It can also be a std::weak_ptr, WeakPtr, or your own smart pointer type with an ultralight::HolderTraits specialization. The Binding keeps the holder (a copy or the moved std::unique_ptr)– an owning pointer keeps the instance alive while it's bound. Sync() locks the holder while it reads the instance.

Once a weak holder expires, Sync() skips the binding and pages keep its last values. Handlers that are member functions of the bound type skip it too, and do nothing at all when the holder points to a const type.

Parameters
nameThe binding name pages use.
holderThe smart pointer to the instance.
Returns
Returns the Binding (empty if name isn't a valid binding name).

◆ Bind() [3/4]

template<typename T>
requires (Described<std::remove_cvref_t<T>> && !std::is_lvalue_reference_v<T>)
Binding< std::remove_cvref_t< T > > Bind ( std::string_view name,
T && instance )
inlinenodiscard

Bind an instance by value under a binding name.

The Binding takes ownership of the moved-in instance and destroys it when it unbinds. Use this for data you build once and don't change afterwards.

Handlers that are member functions of the bound type (eg, OnAction<"respawn">(&HUD::Respawn)) run on the owned instance. They're the only way to change it after binding.

Parameters
nameThe binding name pages use.
instanceThe instance to move into the Binding.
Returns
Returns the Binding (empty if name isn't a valid binding name).

◆ Bind() [4/4]

template<typename T>
requires (Described<T> && !std::is_const_v<T>)
Binding< T > Bind ( std::string_view name,
T & instance )
inlinenodiscard

Bind an instance of a described type under a binding name.

Pages reach the instance's fields through the binding name (eg, hud.health), and each Sync() sends your changes to them. The Binding borrows instance, so keep it alive while it's bound.

Binding a name again replaces the earlier binding (see Binding).

Parameters
nameThe binding name pages use. It starts with a letter or an underscore, followed by letters, digits, and underscores (eg, hud or main_menu).
instanceThe instance to bind. Ownership remains with the caller.
Returns
Returns the Binding (empty if name isn't a valid binding name, and the library logs a warning).
Warning
Destroying the Binding unbinds the instance, so a ctx.Bind(...) statement whose result you don't keep binds and unbinds at once.

◆ Create()

Context Create ( )
inlinestatic

Create a Context whose home thread is the calling thread.

Returns
Returns the new Context.

◆ CreateThreadSafe()

Context CreateThreadSafe ( )
inlinestatic

Create a Context that any thread can use.

Use this when the thread that calls Sync() changes over time (eg, a job system that syncs on different worker threads). Any thread can then bind, sync, and register handlers. The calls take turns (each one waits until the running one finishes), and handlers run on the thread that called Sync().

Warning

Only the Context's own calls are thread-safe, not your bound instances. Sync() reads them without any lock you can see, so don't change an instance while another thread may be syncing. To hand an instance to another thread, use your own synchronization.

A handler that waits on another thread that needs this Context deadlocks. Calling back into the Context from the handler itself is fine.

Returns
Returns the new Context.

◆ DefineFormat()

template<typename Fn>
Context & DefineFormat ( std::string_view name,
Fn && fn )
inline

Define a formatter for {{path|name}} text.

A formatter turns a bool, number, or string value into the text the page shows:

ctx.DefineFormat("comma", [](dd::Value v) {
return GroupDigits(v.Or(int64_t(0)));
});
<span>{{hud.gold|comma}} gold</span>

Only {{path}} text uses formatters (ul-text doesn't take one), and only for the bindings of this Context. Defining a name again replaces its formatter. A new formatter shows the next time the text changes.

Parameters
nameThe formatter name, used after the pipe. It starts with a letter or an underscore, followed by letters, digits, and underscores. A name that doesn't defines nothing (the library logs a warning).
fnThe formatter. It takes the value as a dd::Value and returns the text (a String, a std::string, or a C string).
Returns
Returns this Context, for chaining.
Warning
Formatters run on the Renderer's thread. They must return the same text for the same value, must not throw, and must not call the data-binding API.

◆ DetachFrom()

void DetachFrom ( View * view)
inline

Detach this Context from a View.

During a later Renderer::Update(), the View's page rebuilds its bindings from the Contexts still attached. When none are left, the page gets its authored markup back ({{path}} text, ul-if templates, and no rows). Values already written by ul-text, classes, attributes, and styles stay.

Parameters
viewThe View to detach from (nullptr does nothing).
Note
Safe to call from any thread.

◆ DumpSchema()

bool DumpSchema ( const char * utf8_path,
DumpAt when = DumpAt::NextSync )
inline

Write the JSON from schema() to a file.

With DumpAt::NextSync (the default), the library writes the file at the end of the next Sync() on the home thread, so you can call this from any thread (eg, from a debug hotkey). A write that fails then logs a warning with the path.

With DumpAt::Now, the library writes the file before this returns. Call it on the home thread.

if (debug_key_pressed)
ctx.DumpSchema("ui/schema.json");
Parameters
utf8_pathThe file's path as a UTF-8 string (on Windows too). An existing file is replaced.
whenWhen to write the file.
Returns
Returns false if utf8_path is NULL or empty, or for an empty Context. With DumpAt::Now, also returns false if the write fails.

◆ FromBorrowed()

Context FromBorrowed ( ULDOMDataContext handle)
inlinestatic

Wrap a C handle someone else owns, without owning the context.

The result works like any Context, but destroying it never detaches the context or destroys its bindings, and it doesn't keep the context attached once its owners are gone.

Parameters
handleThe borrowed handle (NULL gives an empty Context).
Returns
Returns a Context with its own (non-owning) reference, so you can keep it. Its raw() is a different handle than handle.

◆ generation()

uint64_t generation ( ) const
inline

Get the generation of the last update Sync() sent to the pages.

The generation goes up by one with each update.

Returns
Returns the generation (0 before the first update or for an empty Context).

◆ LeakRef()

ULDOMDataContext LeakRef ( )
inline

Give up ownership of the C handle and return it.

This Context becomes empty.

Returns
Returns the handle. You must call ulDestroyDOMDataContext() when finished.

◆ operator bool()

operator bool ( ) const
inlineexplicit

Whether or not this Context is valid (false after a move or LeakRef()).

◆ operator=() [1/2]

Context & operator= ( const Context & )
delete

◆ operator=() [2/2]

Context & operator= ( Context && other)
inlinenoexcept

Move assignment (other becomes empty).

This Context releases its own handle first.

Parameters
otherThe Context to move from.
Returns
Returns this Context.

◆ PostAction() [1/3]

bool PostAction ( std::string_view name)
inline

Send an action to a bound instance's handler as if the page fired it.

Use this where you have no Binding at hand, eg, in a DOM listener that handles an interaction the markup can't express. Where you have the Binding, use Binding::PostAction() instead (it checks the action name at compile time).

The action reaches its handler at the next Sync(), in order with the actions from the page. An action with an unknown binding or action name is dropped there, with a warning when DOM diagnostics are on (see "Finding Mistakes" in the class overview).

Parameters
nameThe binding name and the action name joined by a dot (eg, "inv.sort"). Only actions of the bound type itself can be sent (not a row's actions).
Returns
Returns true if the action was queued, or false if name isn't in that form or the action queue is full.
Note
Safe to call from any thread.
Note
Each call takes one entry in the action queue, even for an action declared dd::OnlyLatest (repeats merge only when Sync() delivers them).
See also
set_action_queue_capacity()

◆ PostAction() [2/3]

template<typename P>
requires (!Described<std::decay_t<P>> && ScalarPayload<std::decay_t<P>>)
bool PostAction ( std::string_view name,
const P & payload )
inline

Send an action with a single-value payload (see the payloadless overload).

Use this for an action declared with one bool, number, string, or reflected enum value (eg, dd::Action<double>("seek")):

ctx.PostAction("player.seek", 0.5);
Parameters
nameThe binding name and the action name joined by a dot.
payloadThe value to send.
Returns
Returns true if the action was queued, or false if name isn't in that form or the action queue is full.

◆ PostAction() [3/3]

template<typename P>
requires Described<std::remove_cvref_t<P>>
bool PostAction ( std::string_view name,
const P & payload )
inline

Send an action with a payload struct (see the payloadless overload).

ctx.PostAction("inv.moveItem", MoveItem { .from = a, .to = b });

The payload is a described struct whose fields are all bool, integer, floating-point, string, or reflected enum values (checked at compile time). The action's handler receives it as its declared payload type (a handler whose payload type doesn't match doesn't receive it).

Parameters
nameThe binding name and the action name joined by a dot.
payloadThe payload to send.
Returns
Returns true if the action was queued, or false if name isn't in that form or the action queue is full.

◆ PostTask()

template<typename F>
requires (std::is_invocable_v<std::decay_t<F>&> || std::is_invocable_v<std::decay_t<F>&, Document>)
void PostTask ( F && callback)
inline

Run a callable on the Renderer's thread once the pages show your latest data.

The callable runs during a later Renderer::Update(), after the pages have applied the data from:

  • This Sync, when you post from a handler inside Sync().
  • The last Sync, when you post from anywhere else.

How often it runs depends on its parameters:

  • No parameters runs once after every attached View has applied the data. When no View is attached, it runs during a later Renderer::Update() without waiting for a page.
  • A dom::Document runs once for each attached View and gets that View's document, so it can read the page your data produced. With no attached Views, it runs once with an empty Document.
ctx.PostTask([] { SignalUIReady(); }); // once
ctx.PostTask([](dom::Document doc) { // once per View
dom::Element row = doc.querySelector("#roster li");
// ...
});
The root of a page's DOM tree in a View or frame.
Definition Document.h:106
Element querySelector(std::string_view selectors) const
Find the first element matching a CSS selector (querySelector).
Definition Document.h:203
A handle to an element on a page.
Definition Element.h:142
ContainerSpec< Children... > row(ContainerOptions options, Children... children)
Describe a row container with child elements.
Definition Builder.h:125

A View whose page is still loading holds it up until that page shows the data. A page that doesn't use this Context (eg, its origin isn't allowed) doesn't hold it up.

Parameters
callbackThe callable to run. Capture by value, since it runs later (and on another thread unless your home thread is the Renderer's thread).
Note
Safe to call from any thread.
Note
The library destroys the callable after its last run. If no Renderer exists, or the Renderer shuts down first, it's destroyed without running.

◆ raw()

ULDOMDataContext raw ( ) const
inline

Get the C handle, for passing to the <Ultralight/CAPI/CAPI_DOMData.h> functions.

Returns
Returns the handle (NULL for an empty Context). This Context still owns it, so don't destroy it.

◆ schema()

std::string schema ( ) const
inline

Get the schema of every bound instance as JSON, with their current values.

The mock data tools read this JSON, so you can build and test pages in a browser with your app's real model shapes (see DumpSchema() to write it to a file). It has:

  • format, api, and version: "ul-schema", "data", and 1.
  • models: maps each binding name to its type in types.
  • types: lists each type's entries in schema order (with their names, value types, flags, and child types).
  • values: maps each binding name to its current values, read through its schema in the form mock data uses. A list holds at most its first 100 rows, and actions and dd::Internal entries have no values.

A reader should ignore keys and value types it doesn't know. The version changes only for a change that would break an existing reader.

Since the values are your app's real data, you should only write them out from development builds.

Returns
Returns the JSON text (empty for an empty Context).
Note
This reads every bound instance (like Sync() does), so it isn't free. Call it on the home thread.

◆ set_action_queue_capacity()

void set_action_queue_capacity ( uint32_t capacity)
inline

Set how many actions can wait for the next Sync().

(Default: 1024)

When the queue is full, new actions are dropped (the library logs a warning). For an action the page fires continuously (eg, a scroll position), declare it dd::OnlyLatest so each new one replaces the waiting one instead of raising the limit.

Parameters
capacityThe number of actions (values below 1 become 1).
Note
Safe to call from any thread.

◆ Sync()

void Sync ( )
inline

Deliver input from the page to your handlers and send your changes to the pages.

Call this once per tick on the home thread. Each call does three things in order:

  1. Delivers input from the page to your handlers (changes first, then actions).
  2. Reads every bound instance through its schema.
  3. Sends what changed to the pages as one update.

Pages show the update during a later Renderer::Update(), and they never show part of one. A Sync with nothing to send is cheap.

A type can control how its instances are read with a static Sync function, which reads fields with Model::Sync() (see "Three Kinds of Sync" in the Model class description).

Note
A Sync() called from inside one of your handlers does nothing (the library logs a warning).

◆ UndefineFormat()

Context & UndefineFormat ( std::string_view name)
inline

Remove a formatter.

Text that uses it shows the plain value (and logs a warning) from the next time it changes.

Parameters
nameThe formatter name.
Returns
Returns this Context, for chaining.

The documentation for this class was generated from the following file: