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
OnReadyForBindingswhich 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
Prysm allows binding dynamically-typed objects as well. That can be useful if your models come from another scripting language, for example Python or Lua.
You can check the following two samples that explain how to setup such objects:
- ProtobufDataBind sample
- DynamicallyTypedDataBinding sample (using
std::variant). The sample also demonstrates how to implement avectorofstd::variantelements
Here is an overview of the steps to create dynamically exposed objects using std::variant, which are explained in greater details in the above-mentioned sample. We’ll keep the code snippets here as short as possible, because the APIs might change. The samples will be maintained up-to-date:
- Create a Model Class that will keep the dynamic property. If you also need a
vectorholding dynamic property, you need to wrap it in a struct, becausevector<std::variant<...>>is not supported directly.
using DynamicType = std::variant<int, std::string, DamageData>;
// If you need a vector of dynamically typed elements, you need to wrap them in a struct/class
struct DynamicWrapper
{
DynamicType m_Data;
};
struct DynamicModel
{
DynamicType m_DamageData; // dynamically typed property
std::vector<DynamicWrapper> m_DynTypedArray; // vector of dynamically typed elements
};
Create a template class with a template specialization that extends
cohtml::TypedProperty<ValueType*>and holds an address to the property data. It acts as a middleman between C++ and JavaScript.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 Prysm.Override the pure virtual functions
cohtml::Property::Read()andcohtml::Property::ReadValue()to handle different types of data and assign the next type fromcohtml::Binder::PeekValueType()intostd::variantOverride the pure virtual functions
cohtml::Property::Bind()andcohtml::Property::BindValue()to bind the active data type withcohtml::CoherentBindInternal()fromstd::variantusingstd::visitCreate overrides of the virtual functions
cohtml::Property::ToBoolean(),cohtml::Property::ToNumber(float),cohtml::Property::ToNumber(double),cohtml::Property::ToString(),cohtml::Property::ToColor()usingstd::visitfor compatibility in casting a type from JavaScript to other types when needed by the SDK.Create override of the virtual function
cohtml::Property::ToObject()to handle complex (user types) in thestd::variant.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
CoherentBind()functions for theDynamicModeland theDynamicWrappertypes that bind the dynamic property usingcohtml::Binder::AddProperty()and the factory method we created, so the data will get recognized by the UI.
Property class as it may lead to memory leaks.