Implementing Dynamic Data Binding from JSON
Hello,
I am involved in a project where the primary game logic is developed in Rust, and we look into possibly using NoesisGUI as a UI middleware.
Typically, NoesisGUI expects data models to be statically defined in C++ subclasses, with specific properties (like Name and Health for a Player). However, defining every possible data model from our Rust codebase in C++ would dramatically increase the interop complexity and maintenance overhead. We are exploring ways to dynamically generate these data models based on arbitrary JSON inputs from Rust to avoid this overhead.
Consider an example like this:
Input:
Outcome in XAML:
This could illustrate how I would like to dynamically bind incoming JSON data to UI components in NoesisGUI without pre-defining a C++ class for Player. In other words a system where UI components can dynamically adapt to the data structure defined in JSON.
Is something like this possible using dynamic reflection? If so, could you provide any reference or guidance how to start with implementation of such system?
Thank you for your help!
I am involved in a project where the primary game logic is developed in Rust, and we look into possibly using NoesisGUI as a UI middleware.
Typically, NoesisGUI expects data models to be statically defined in C++ subclasses, with specific properties (like Name and Health for a Player). However, defining every possible data model from our Rust codebase in C++ would dramatically increase the interop complexity and maintenance overhead. We are exploring ways to dynamically generate these data models based on arbitrary JSON inputs from Rust to avoid this overhead.
Consider an example like this:
Input:
Code: Select all
"Player": {
"Name": "John Doe",
"Health": 42
}
Code: Select all
<ResourceDictionary>
<Player x:Key="Player" Name="John Doe" Health="42"/>
</ResourceDictionary>
Is something like this possible using dynamic reflection? If so, could you provide any reference or guidance how to start with implementation of such system?
Thank you for your help!
Re: Implementing Dynamic Data Binding from JSON
It is possible to create new classes dynamically. This is what we are doing for C# and Blueprints in Unreal, we register new types in the reflection.
Let me explain this a bit more, unfortunately this is a part not clearly documented and the API is a bit obscure at some points (this is probably the oldest part of Noesis and we have plans to improve it).
In the document about reflection, you'll see there are a few macros in Noesis to define the types and properties. Those are backed up by some classes, the most important:
The CreateProperty is like a switch for the supported property types to create the correct implementation:
In your implementation you'll need one C++ proxy class to wrap all your view models. This class just needs to provide the correct TypeClass that corresponds to the view model by reimplementing the GetClassType method:
This way, when Noesis accesses the proxy through reflection you will be able to return the information stored in your VM. This is always done through the TypeProperty getters:
Please, let me know if you need more detail about this.
Let me explain this a bit more, unfortunately this is a part not clearly documented and the API is a bit obscure at some points (this is probably the oldest part of Noesis and we have plans to improve it).
In the document about reflection, you'll see there are a few macros in Noesis to define the types and properties. Those are backed up by some classes, the most important:
- TypeClass: defines a new type with properties. It has a base class (referenced as another TypeClass), and can implement interfaces (also referenced as a TypeClass).
- TypeProperty: defines a property of a specific type. There are several specializations to implement different kinds of properties (member variables, getter/setter accessors...). In this case you will need your own implementation to know how to access your data.
Code: Select all
TypeClass* type = new TypeClass(typeId, false);
Reflection::RegisterType(type);
TypeClassBuilder* typeClassBuilder = (TypeClassBuilder*)type;
typeClassBuilder->AddBase(typeData->baseType);
for (int i = 0; i < typeData->numProps; ++i)
{
const PropertyData& propData = typeData->propsData[i];
TypeProperty* property = CreateProperty(type, propData.type, Symbol(propData.name), i, propData.readOnly);
typeClassBuilder->AddProperty(property);
}
Code: Select all
TypeProperty* CreateProperty(const TypeClass* ownerType, PropertyType propType, Symbol propName, int index, bool readonly)
{
switch (propType)
{
case PropertyType_Bool:
return new TypePropertyProxy<bool>(ownerType, propName, index, readonly);
case ExtendPropertyType_Float:
return new TypePropertyProxy<float>(ownerType, propName, index, readonly);
case ExtendPropertyType_Double:
return new TypePropertyProxy<double>(ownerType, propName, index, readonly);
case ExtendPropertyType_Int:
return new TypePropertyProxy<int>(ownerType, propName, index, readonly);
...
}
}
Code: Select all
class VMProxy: public Noesis::BaseComponent
{
public:
VMProxy(void* obj, const Noesis::TypeClass* type): mVMObject(obj), mType(type) { }
const TypeClass* GetClassType() const override { return mType; }
void* mVMObject;
const Noesis::TypeClass* mType;
};
Code: Select all
class VMTypeProperty: public Noesis::TypeProperty { ... };
template<class T>
class VMTypePropertyImpl: public VMTypeProperty
{
Noesis::Ptr<Noesis::BaseComponent> GetComponent(const void* ptr) const override
{
VMProxy* obj = (VMProxy*)ptr;
// get from 'obj->mVMObject' the value of this property
return Noesis::Boxing::Box<T>(value);
}
Re: Implementing Dynamic Data Binding from JSON
Hey @jsantos,
This is very helpful. Thanks a lot for providing direction! So far following your advice I was able to come up with a code like this:
This seem to register the type defined by json input so for instance will register type `Player` with properties `Name` and `Health`. Although when I try to use it for simple data binding I got errors
Is this more or less how you imagine it should work? Any ideas what went wrong here? Again, thanks a lot for your help!
This is very helpful. Thanks a lot for providing direction! So far following your advice I was able to come up with a code like this:
Code: Select all
class JsonObject : public NoesisApp::NotifyPropertyChangedBase
{
public:
JsonObject(const json& data, const TypeClass* type) : _data(data), _type(type) {}
const TypeClass* GetClassType() const override { return _type; }
const json& GetJsonData() const { return _data; }
json& GetJsonData() { return _data; }
void SetJsonData(const json& new_data)
{
_data = new_data;
NotifyAll();
}
void NotifyAll()
{
auto num_props = _type->GetNumProperties();
for (unsigned i = 0; i < num_props; ++i)
{
const TypeProperty* prop = _type->GetProperty(i);
if (!prop) continue;
OnPropertyChanged(prop->GetName().Str());
}
}
private:
json _data;
const TypeClass* _type;
};
template <typename T> class TypePropertyProxy : public TypeProperty
{
public:
TypePropertyProxy(Symbol name, const Type* type) : TypeProperty(name, type) {}
virtual Ptr<BaseComponent> GetComponent(const void* ptr) const override
{
const JsonObject* obj = static_cast<const JsonObject*>(ptr);
if (obj)
{
const json& json_data = obj->GetJsonData();
if (json_data.contains(GetName().Str()))
{
T value = json_data[GetName().Str()].get<T>();
return Boxing::Box(value);
}
}
return nullptr;
}
virtual void SetComponent(void* ptr, BaseComponent* value) const override
{
JsonObject* obj = static_cast<JsonObject*>(ptr);
if (obj && !IsReadOnly())
{
json& json_data = obj->GetJsonData();
json_data[GetName().Str()] = Boxing::Unbox<T>(value);
}
}
virtual void* GetContent(const void* ptr) const
{
NS_UNUSED(ptr);
return nullptr;
}
};
TypeProperty* CreateProperty(const std::string& prop_name, json::value_t type)
{
auto prop_sym = Symbol(prop_name.c_str());
NS_LOG_INFO(" - Property: %s", prop_name.c_str());
switch (type)
{
case json::value_t::boolean: return new TypePropertyProxy<bool>(prop_sym, TypeOf<bool>());
case json::value_t::number_integer:
return new TypePropertyProxy<int>(prop_sym, TypeOf<int>());
case json::value_t::number_unsigned:
return new TypePropertyProxy<int>(prop_sym, TypeOf<int>());
case json::value_t::number_float:
return new TypePropertyProxy<float>(prop_sym, TypeOf<float>());
case json::value_t::string:
return new TypePropertyProxy<String>(prop_sym, TypeOf<String>());
case json::value_t::null:
case json::value_t::object:
case json::value_t::array:
case json::value_t::binary:
case json::value_t::discarded: break;
}
return nullptr;
}
const TypeClass* TypeFromJson(const json& json_data)
{
auto root = json_data.begin();
auto ty_name = root.key();
auto ty_sym = Noesis::Symbol(ty_name.c_str());
if (Reflection::IsTypeRegistered(ty_sym))
{
return static_cast<const TypeClass*>(Reflection::GetType(ty_sym));
}
NS_LOG_INFO("New type registered: %s", ty_name.c_str());
TypeClass* type = new TypeClass(ty_sym, false);
Reflection::RegisterType(type);
TypeClassBuilder* ty_builder = (TypeClassBuilder*)type;
// All values in the object are properties
json properties = root.value();
for (auto& prop : properties.items())
{
auto prop_name = prop.key();
auto prop_ty = prop.value().type();
TypeProperty* property = CreateProperty(prop_name, prop_ty);
if (property != nullptr)
{
ty_builder->AddProperty(property);
}
}
return type;
}
void NoesisApp::RegisterViewModel(rust::Str json_content)
{
std::string json_str(json_content.data(), json_content.size());
json json_data = ParseObject(json_str);
if (json_data.is_null()) return;
auto type_class = TypeFromJson(json_data);
if (!type_class)
{
NS_LOG_ERROR("Couldn't create `ClassType` from json: %s", json_str.c_str());
}
}
Code: Select all
{ "Player": {"Name": "John Doe", "Health": "42"} }
Code: Select all
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:noesis="clr-namespace:NoesisGUIExtensions;assembly=Noesis.GUI.Extensions"
mc:Ignorable="d"
d:DesignWidth="1280" d:DesignHeight="1720"
FontFamily="./#Roboto-Medium"
FontSize="10"
x:Name="control1"
>
<ResourceDictionary>
<Player x:Key="Player" Name="John Doe" Health="42"/>
</ResourceDictionary>
<Button x:Name="button1" Content="{Binding Source={StaticResource Player}, Path=Name}" Width="100" Height="50">
</Button>
</UserControl>
Code: Select all
ERROR > UserControl1.xaml(15,28): Cannot assign property to abstract class 'Player'
ERROR > UserControl1.xaml(15,44): Cannot assign property to abstract class 'Player'
ERROR > UserControl1.xaml(17,13): StaticResource 'Player' not found
Re: Implementing Dynamic Data Binding from JSON
I don't think you need the dictionary. Just set the JsonObject instance as the DataContext of the tree (setting the DataContext property) and bind it like this:
This should work. Please, let me know.
Code: Select all
<Button x:Name="button1" Content="{Binding Path=Name}" Width="100" Height="50">
<TextBlock Text="{Binding Health}"/>
Re: Implementing Dynamic Data Binding from JSON
Hey, if I set the JsonObject instance as the DataContext Noesis complains about incompatible types:
ERROR > Value cannot be assigned to the property 'UserControl.DataContext' (property has type 'BaseComponent', value has type 'Player')
So perhaps I'm missing the reflection bits that would allow Noesis::DynamicCast between created type and BaseComponent? Also, If I'm not using dictionary, how could I attach multiple ViewModels to the tree?
ERROR > Value cannot be assigned to the property 'UserControl.DataContext' (property has type 'BaseComponent', value has type 'Player')
So perhaps I'm missing the reflection bits that would allow Noesis::DynamicCast between created type and BaseComponent? Also, If I'm not using dictionary, how could I attach multiple ViewModels to the tree?
Re: Implementing Dynamic Data Binding from JSON
Got it fixed by adding base type on TypeBuilder:
and the binding works now (yay!), although I'm still not sure how to attach multiple view models to the tree root.
I would imagine if I set the JsonObject instance in the root resources, something like this should work?
Edit: Got it working like this:
Code: Select all
ty_builder->AddBase(TypeOf<NotifyPropertyChangedBase>());
I would imagine if I set the JsonObject instance in the root resources, something like this should work?
Code: Select all
<Button x:Name="button1" Content="{DynamicResource Player.Name}" Width="100" Height="50">
Code: Select all
<Button x:Name="button1" DataContext="{DynamicResource Player}" Content="{Binding Path=Name}">
Re: Implementing Dynamic Data Binding from JSON
A probably better way to implement this, is by implementing the IDictionaryIndexer and IListIndexer interfaces.
In this approach, you just set one DataContext (a root JsonDictionary) who is in charge of returning other sub-JSON entities (simple types using boxing, another JsonDictionary, a JsonArray, ...). This way is you have, for example, this JSON:
You can bind to it this way:
Code: Select all
class JsonDictionary: public BaseComponent, public IDictionaryIndexer
{
public:
bool TryGet(const char* key, Ptr<BaseComponent>& item) const override
{
// ...
}
bool TrySet(const char* key, BaseComponent* item) override
{
// ...
}
NS_IMPLEMENT_INTERFACE_FIXUP
NS_IMPLEMENT_INLINE_REFLECTION(JsonDictionary, BaseComponent)
{
NsImpl<IDictionaryIndexer>();
}
};
Code: Select all
class JsonArray: public BaseComponent, public IListIndexer
{
public:
bool TryGet(uint32_t index, Ptr<BaseComponent>& item) const override
{
// ...
}
bool TrySet(uint32_t index, BaseComponent* item) override
{
// ...
}
NS_IMPLEMENT_INTERFACE_FIXUP
NS_IMPLEMENT_INLINE_REFLECTION(JsonArray, BaseComponent)
{
NsImpl<IListIndexer>();
}
};
Code: Select all
{
"squadName": "Super hero squad",
"homeTown": "Metro City",
"formed": 2016,
"secretBase": "Super tower",
"active": true,
"members": [
{
"name": "Molecule Man",
"age": 29,
"secretIdentity": "Dan Jukes",
"powers": ["Radiation resistance", "Turning tiny", "Radiation blast"]
},
{
"name": "Madame Uppercut",
"age": 39,
"secretIdentity": "Jane Wilson",
"powers": [
"Million tonne punch",
"Damage resistance",
"Superhuman reflexes"
]
},
{
"name": "Eternal Flame",
"age": 1000000,
"secretIdentity": "Unknown",
"powers": [
"Immortality",
"Heat Immunity",
"Inferno",
"Teleportation",
"Interdimensional travel"
]
}
]
}
Code: Select all
<TextBlock Text="{Binding [members][1][powers][0]}"/>
Who is online
Users browsing this forum: Ahrefs [Bot] and 3 guests