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.
std::string
, std::vector
, and similar. For the given examples above, you can simply include cohtml/Binding/String.h
and cohtml/Binding/Vector.h
.std::vector
, you will get a compiler error for unsupported type. You can, however, add these types as properties to your model.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
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(); }
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
.
std::variant
with primitive types and std::string
Here are the steps to create dynamically exposed objects using std::variant
:
- 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;
};
- 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;
Override the pure virtual function
cohtml::Property::Clone()
which will create a dynamic copy of the current template class and override the pure virtual functioncohtml::TypedProperty::GetValue()
which will return the current value for the cloned data object. These functions are used internally by Gameface.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 thestatic_assert
instd::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;
}
};
Override the pure virtual function
cohtml::Property::Read()
to read the current property’s name withcohtml::Binder::ReadProperty
and pass the binder and object tocohtml::Property::ReadValue()
.Override the pure virtual function
cohtml::Property::ReadValue()
to handle different types of data and assign the next type fromcohtml::Binder::PeekValueType()
intostd::variant
and usestd::visit
to read it withcohtml::CoherentReadInternal()
. This will expose the property’s name and the current value of the clone to the UI for reading.Override the pure virtual function
cohtml::Property::Bind()
to bind the property’s name withcohtml::Binder::PropertyName()
and pass the binder and object tocohtml::Property::BindValue()
.Override the pure virtual function
cohtml::Property::BindValue()
to bind the active data type withcohtml::CoherentBindInternal()
fromstd::variant
usingstd::visit
. This will expose the property’s name and the current value of the clone to the UI for binding.Create overrides of the virtual functions
cohtml::Property::ToBoolean()
,cohtml::Property::ToNumber()
,cohtml::Property::ToString()
,cohtml::Property::ToColor()
usingstd::visit
for compatibility in casting a type from JavaScript to other types.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);
}
- Create a
CoherentBind(cohtml::Binder* binder, DynamicModel* model)
function for the Model, that binds the properties that reside in the Model usingcohtml::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);
}
Property
class as it may lead to memory leaks.