Data binding (C++ integration)

Data binding eliminates a lot of JavaScript boilerplate by automatically synchronizing your C++ data with your JavaScript front-end.

Overview

As with any other powerful feature, data binding requires a few steps to set up. In order for an object to be used outside of your C++ code, it needs to be registered first. To illustrate how this works let’s look at a short example.

Creating a named model

To start, you need to expose an object to the UI. We’ll call the exposed C++ object a “model” as it will serve as the backing store of the data.

Before creating a model, you will need to write a CoherentBind for your object type and also expose all of the nested types as properties of the type you wish to expose.

You can now create a named model using the cohtml::View::CreateModel method:

struct Player
{
    int LeftPos;
};

Player g_Player{ 123 };

// Tell Cohtml how to bind the player
void CoherentBind(cohtml::Binder* binder, Player* player)
{
    if (auto type = binder->RegisterType("Player", player))
    {
        type.Property("leftPos", &Player::LeftPos);
    }
}

void RegisterMyModel()
{
    // Expose g_Player with the name "g_TestPlayer" to JS
    m_View->CreateModel("g_TestPlayer", &g_Player);
}

The cohtml::View::CreateModel invocation registers a model named g_TestPlayer which corresponds to the g_Player C++ instance. The CoherentBind above takes care of exporting the player class just like it would with standard binding.

We aren’t done yet though - to save on computation Cohtml will only update the element if you tell it to do so.

Update the model

// Somewhere in your game loop
void Update()
{
    ...
    // Change the player:
    g_Player->SetScore(score);
    // Tell Cohtml the player changed:
    view->UpdateWholeModel(&g_Player);
    // Finally, tell Cohtml that now is a good time to synchronize all changes:
    view->SynchronizeModels();
}

Note that cohtml::View::UpdateWholeModel only marks the model as dirty but doesn’t do any work. The synchronization is done inside cohtml::View::SynchronizeModels which allows you to call it once for all changes. This improves performance as multiple changes within the same frame won’t trigger multiple syncs.

C++ Model Details

Updating a model

To signify that a model needs updating use cohtml::View::UpdateWholeModel. It will update all properties of the model, including those that haven’t actually changed.

m_View->CreateModel("player", m_Player.get());
m_Player->SetScore(42);
m_View->UpdateWholeModel(m_Player.get());
m_View->SynchronizeModels();

cohtml::View::SynchronizeModels updates all the models that have been marked to have been changed.

C++ Property conversion

To convert from the C++ values of the properties of the model the cohtml::Property interface is used. Simple expressions (like data-bind-value="{{g_Player.score}}") are converted directly to the C++ type used by Cohtml for the specific data-bind operation. This is implemented via the cohtml::Property::ToNumber, cohtml::Property::ToString, cohtml::Property::ToBoolean, cohtml::Property::ToColor methods. They in turn call the cohtml::CoherentToNumber, cohtml::CoherentToString, cohtml::CoherentToBoolean and cohtml::CoherentToColor functions. Overloading these functions will allow to convert from your C++ type to a Cohtml type. For example, to convert from MyColor to renoir::Color add

namespace cohtml {
template <>
struct CoherentToColor<const MyColor&>
{
    bool Invoke(const MyColor& from, renoir::Color* to)
    {
        to->Value = TO_RGBA(from);
        // or
        to->r = from.r();
        to->g = from.g();
        to->b = from.b();
        to->a = from.a();
        return true;
    }
};
}

If your engine uses custom string or primitive types, you must provide appropriate Property conversion implementations for them. Otherwise, conversions will fail gracefully (silently), which may result in parts of the UI not updating as expected.

For some of the operations, a fallback using CSS value as a string is also allowed. For example, when using data-bind-style-color="{{g_Player.color}} the color property will be converted to renoir::Color. In case this conversion fails, it will convert the property to a string and parse the color from that, using the CSS syntax for that value.

Complex expressions that do not involve JavaScript variables, method calls, or other dynamic constructs are evaluated directly in C++ by the SDK using the Property conversion functions (ToBool, ToNumber, ToString, etc.). For type-sensitive operations, where the SDK needs to know the underlying native type rather than a converted value, the type information is retrieved internally via Property::BindValue().

Expressions that cannot be evaluated entirely in C++ are compiled into JavaScript stub functions. These functions evaluate the expression in JavaScript and return the result to C++. In this case, the bound C++ properties are first converted to JavaScript values, and the result is then converted back to a C++ value. This requires several crossings of the C++ <-> JavaScript boundary (one for the function call and one for each model access), which might be quite expensive and should be avoided in performance-critical scenarios if possible.

Unregistering a model

Finally, you can unregister models from binding using the cohtml::View::UnregisterModel API. This removes the model by instance pointer. Unregistering will not remove any elements bound to the model - they’ll preserve their last state.

Unregistering a model on reload or loading a new URL

  • On reloading a view or navigating to a new URL the system will automatically unregister and remove all registered models, events and call handlers.

  • The cleared models will be not registered again automatically when the new URL because the new URL might need different models. The ViewListener has a callback OnReadyForBindings which would be invoked when the cohtml.js file is loaded and the view is ready to accept JavaScript bindings.

There are two major approaches to registering the models in the view

  1. Create and synchronize the models directly inside OnReadyForBindings.

     void MyApplication::InitModel()
     {
         m_View->CreateModel("g_TestPlayer", &g_Player);
         m_View->SynchronizeModels();
     }
    
     void OnReadyForBindings() override
     {
         InitModel();
     }
    
  2. Expose a C++ handler to be triggered from the UI when it wants the models to be registered.

     void OnReadyForBindings() override
     {
        m_View->RegisterForEvent("InitModel", cohtml::MakeHandler(this, &MyApplication::InitModel));
     }
    

Disabling default binding behavior

Cohtml provides a default version of CoherentBind which will error out if you don’t provide a template specialization of CoherentBind for your type. In some cases, you might want to implement the default by yourself (e.g. binding all types via the reflection system of your engine). To do that you need to declare a template specialization for cohtml::IsDefaultBindEnabled structure in the way shown below.

For example, let’s declare DisableDefaultBinding specialization for user-defined type Player:

namespace cohtml
{
    template<>
    struct DisableDefaultBinding<Player, Player> : TrueType
    {
    };
}

In the case you need to write a more generic version, you can use the second template parameter where you can use SFINAE and enable_if techniques. If you wish to disable default binding for classes and unions you can write something similar to this:

namespace cohtml
{
    template<typename T>
    struct DisableDefaultBinding<T, typename std::enable_if<std::is_class<T>::value || std::is_union<T>::value, T>::type> : TrueType
    {
    };
}

Events for model changes

In case you need to know when your data binding model was updated/synchronized (e.g. you want to run some JavaScript on model changes), you can implement your own events in a way similar to the following:

class MyGame
{
    // Class definition
    Player m_Player;
    bool m_NotifyPlayerUpdate;
}

void MyGame::UpdatePlayerModel()
{
    m_View->UpdateWholeModel(&m_Player);
    m_NotifyPlayerUpdate = true;
}

void MyGame::SynchronizeModels()
{
    if (m_NotifyPlayerUpdate)
    {
        m_View->TriggerEvent("BeforeModelSynchronize", m_Player);
        m_View->SynchronizeModels();
        m_View->TriggerEvent("AfterModelSynchronize", m_Player);
        m_NotifyPlayerUpdate = false;
    }
}

void MyGame::Advance()
{
    // Some game logic
    UpdatePlayerModel();
    // More game logic
    SynchronizeModels();
    ...
}
...
<script src="js/cohtml.js"></script>

engine.on('Ready', function() {
    engine.on('BeforeModelSynchronize', function(player) {
        /// Do your job here
    });

    engine.on('AfterModelSynchronize', function(player) {
        /// Do your job here
    });
    ...
});

Extending the Property interface

The Property interface type-erases access to a native property, allowing the SDK to interact with it without requiring knowledge of its concrete C++ type. You may need to implement a custom Property to support specific behaviors, such as dynamically-typed properties, binding values from another scripting layer (e.g. Lua), or integrating with an engine’s reflection system to avoid manual binding code.

For more details, see Extending the Property interface (C++ integration).