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;
    }
};
}

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 are compiled to JavaScript stub functions that evaluate the expression and return the result. This means that the properties of the bound C++ model are 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 property access of the model), which might be quite expensive. The fallback algorithm, described above, is used for values returned from complex expressions as well.

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
    });
    ...
});

Dynamically-typed object data binding

This section will show you how to create dynamically-typed object data binding, guiding you step by step with code snippets. At the end of the section, you will find the full example code.

Since the type of some models is not known in advance, you can set up data binding in a way that reads their values dynamically based on their type. This way you can create your UI at runtime and have the possibility to create it by using Lua bindings. To make this happen, we are going to use the C++ 17 type-safe union std::variant.

Here are the steps to create dynamically exposed objects using std::variant:

  1. Create a Model Class that will keep a collection of all the data for the given properties, so it can be modified from either C++ or Javascript.
struct DynamicModel
{
    std::variant<int, std::string> m_DamageData;
};
  1. Create a template class with a template specialization that extends cohtml::TypedProperty<ValueType*> and holds an address to the Model data. It acts as a middleman between C++ and JavaScript.
template <typename T>
class DynamicProperty;

template <typename Class, typename DataType>
class DynamicProperty<DataType Class::*> : public cohtml::TypedProperty<DataType*>
{
    typedef cohtml::TypedProperty<DataType*> BaseProperty;
    typedef DataType Class::* Pointer;
public:
    DynamicProperty(const char* name, Pointer data, const bool isReadOnly = false, const bool isRef = true)
        : BaseProperty(name, isRef)
        , m_Data(data)
        , m_IsReadOnlyData(isReadOnly) 
    {}
private:
    Pointer m_Data;
    // if we want to state that this shouldn't be modified
    // from the JavaScript side
    const bool m_IsReadOnlyData;
  1. Override the pure virtual function cohtml::Property::Clone() which will create a dynamic copy of the current template class and override the pure virtual function cohtml::TypedProperty::GetValue() which will return the current value for the cloned data object. These functions are used internally by Prysm.

  2. Create a template struct with template specialization, which handles the possibility for a value to be assigned to std::variant. This is required in order to have a more generic property and to work around the static_assert in std::variant for a missing type.

template <class T, class U>
struct IsOneOf;

template <class T, class... Ts>
struct IsOneOf<T, std::variant<Ts...>>
    : std::bool_constant<(std::is_same_v<T, Ts> || ...)> { };

template <typename Variant, typename BuiltInType, bool IsOneOf = IsOneOf<BuiltInType, Variant>::value>
struct Assign {};

template <typename Variant, typename BuiltInType>
struct Assign<Variant, BuiltInType, true>
{
    static bool Invoke(Variant& dt, const BuiltInType ct)
    {
        dt = ct;
        return true;
    }
};

template <typename Variant, typename BuiltInType>
struct Assign<Variant, BuiltInType, false>
{
    static bool Invoke(Variant& dt, const BuiltInType ct)
    {
        return false;
    }
};
  1. Override the pure virtual function cohtml::Property::Read() to read the current property’s name with cohtml::Binder::ReadProperty and pass the binder and object to cohtml::Property::ReadValue().

  2. Override the pure virtual function cohtml::Property::ReadValue() to handle different types of data and assign the next type from cohtml::Binder::PeekValueType() into std::variant and use std::visit to read it with cohtml::CoherentReadInternal(). This will expose the property’s name and the current value of the clone to the UI for reading.

  3. Override the pure virtual function cohtml::Property::Bind() to bind the property’s name with cohtml::Binder::PropertyName() and pass the binder and object to cohtml::Property::BindValue().

  4. Override the pure virtual function cohtml::Property::BindValue() to bind the active data type with cohtml::CoherentBindInternal() from std::variant using std::visit. This will expose the property’s name and the current value of the clone to the UI for binding.

  5. Create overrides of the virtual functions cohtml::Property::ToBoolean(), cohtml::Property::ToNumber(), cohtml::Property::ToString(), cohtml::Property::ToColor() using std::visit for compatibility in casting a type from JavaScript to other types.

  6. Create a factory method for convenient creation of objects from our property.

template <typename P>
DynamicProperty<P> CreateProperty(const char* name, P p)
{
    return DynamicProperty<P>(name, p);
}
  1. Create a CoherentBind(cohtml::Binder* binder, DynamicModel* model) function for the Model, that binds the properties that reside in the Model using cohtml::Binder::AddProperty() and the factory method we created, so the Model will get recognized by the UI.

The preceding example code assumes that the cohtml::View has been previously initialized, and the Model has been created using cohtml::View::CreateModel() in the cohtml::IViewListener::OnReadyForBindings() override. Here are the following files that you could include in your app. The files declare and define the DynamicModel and DynamicProperty classes, which you can use for your dynamically-typed data binding.

// ---------------------------------------------
// File: DynamicModel.h
// ---------------------------------------------
#pragma once

#include <cohtml/Binding/Binder.h>

struct DynamicModel
{
    std::variant<int, std::string> m_DamageData;
};

void CoherentBind(cohtml::Binder* binder, DynamicModel* model);
// ---------------------------------------------
// File: DynamicModel.cpp
// ---------------------------------------------
#include "DynamicModel.h"
#include "DynamicProperty.h"

void CoherentBind(cohtml::Binder* binder, DynamicModel* model)
{
    if (auto type = binder->RegisterType("DynamicModel", model))
    {
        binder->AddProperty(model, CreateProperty("damage", &DynamicModel::m_DamageData));
    }
}
// ---------------------------------------------
// File: DynamicProperty.h
// ---------------------------------------------
#pragma once

#include <string>
#include <variant>
#include <cohtml/View.h>
#include <cohtml/Binding/Binder.h>
#include <cohtml/Binding/Property.h>
#include <cohtml/Binding/String.h>
#include "Logger.h"


template <typename T>
class DynamicProperty;

template <typename Class, typename DataType>
class DynamicProperty<DataType Class::*> : public cohtml::TypedProperty<DataType*>
{
    typedef cohtml::TypedProperty<DataType*> BaseProperty;
    typedef DataType Class::* Pointer;
public:
    DynamicProperty(const char* name, Pointer data, const bool isReadOnly = false, const bool isRef = true)
        : BaseProperty(name, isRef)
        , m_Data(data)
        , m_IsReadOnlyData(isReadOnly) { }
public:
    void* Bind(cohtml::Binder* binder, void* object) const override;
    void* BindValue(cohtml::Binder* binder, void* object) const override;
    void* Read(cohtml::Binder* binder, void* object) const override;
    void* ReadValue(cohtml::Binder* binder, void* object) const override;
    cohtml::Property* Clone() const override;
    DataType* GetValue(void* objectModel) const override;
    bool ToBoolean(void* object, bool* boolean) const override;
    bool ToNumber(void* object, float* number) const override;
    bool ToString(void* object, char* buffer, size_t* length) const override;
    bool ToColor(void* object, renoir::Color* color) const override;
private:
    Pointer m_Data;
    // if we want to state that this shouldn't be modified
    // from the JavaScript side
    const bool m_IsReadOnlyData;
};

template <typename Class, typename DataType>
void* DynamicProperty<DataType Class::*>::Bind(cohtml::Binder* binder, void* object) const
{
    binder->PropertyName(BaseProperty::GetName());
    return DynamicProperty::BindValue(binder, object);
}

template <typename Class, typename DataType>
void* DynamicProperty<DataType Class::*>::BindValue(cohtml::Binder* binder, void* object) const
{
    std::visit([&](auto& arg)
    {
        CoherentBindInternal(binder, arg);
    }, *GetValue(object));

    return object;
}

template <typename Class, typename DataType>
void* DynamicProperty<DataType Class::*>::Read(cohtml::Binder* binder, void* object) const
{
    if (binder->ReadProperty(BaseProperty::GetName()))
    {
        ReadValue(binder, object);
    }
    return object;
}

template <typename Class, typename DataType>
void* DynamicProperty<DataType Class::*>::ReadValue(cohtml::Binder* binder, void* object) const
{
    if (m_IsReadOnlyData)
    {
        return object;
    }

    auto data = GetValue(object);

    switch (binder->PeekValueType())
    {
    case cohtml::VT_Boolean:
    {
        Assign<DataType, bool>::Invoke(*data, false);
        break;
    }
    case cohtml::VT_Number:
    {
        Assign<DataType, int>::Invoke(*data, 0);
        break;
    }
    case cohtml::VT_String:
    {
        Assign<DataType, std::string>::Invoke(*data, std::string(""));
        break;
    }
    case cohtml::VT_Null:
    case cohtml::VT_Object:
    case cohtml::VT_Array:
        assert(false && "This type is not supported");
        break;
    default:
        assert(false && "This shouldn't be reached");
        break;
    }

    // the type is set as active and we can read it
    std::visit([&](auto& arg)
    {
        CoherentReadInternal(binder, arg);
    }, *data);


    return object;
}

template <typename Class, typename DataType>
cohtml::Property* DynamicProperty<DataType Class::*>::Clone() const
{
      return new DynamicProperty(*this);
}

template <typename Class, typename DataType>
DataType* DynamicProperty<DataType Class::*>::GetValue(void* objectModel) const
{
    return &(static_cast<Class*>(objectModel)->*m_Data);
}

template <typename Class, typename DataType>
bool DynamicProperty<DataType Class::*>::ToBoolean(void* object, bool* boolean) const
{
    bool success = false;
    std::visit([&](auto& arg)
    {
        success = cohtml::CoherentToBoolean<DataType>::Invoke(arg, boolean);
    }, *GetValue(object));
    return success;
}

template <typename Class, typename DataType>
bool DynamicProperty<DataType Class::*>::ToNumber(void* object, float* number) const
{
    bool success = false;
    std::visit([&](auto& arg)
    {
        success = cohtml::CoherentToNumber<DataType>::Invoke(arg, number);
    }, *GetValue(object));
    return success;
}

template <typename Class, typename DataType>
bool DynamicProperty<DataType Class::*>::ToString(void* object, char* buffer, size_t* length) const
{
    bool success = false;
    std::visit([&](auto& arg)
    {
        success = cohtml::CoherentToString<DataType>::Invoke(arg, buffer, length);
    }, *GetValue(object));
    return success;
}

template <typename Class, typename DataType>
bool DynamicProperty<DataType Class::*>::ToColor(void* object, renoir::Color* color) const
{
    bool success = false;
    std::visit([&](auto& arg)
    {
        success = cohtml::CoherentToColor<DataType>::Invoke(arg, color);
    }, *GetValue(object));
    return success;
}

template <typename P>
DynamicProperty<P> CreateProperty(const char* name, P p)
{
    return DynamicProperty<P>(name, p);
}