tandr
Topic Author
Posts: 4
Joined: 27 Mar 2024, 08:33

Implementing Dynamic Data Binding from JSON

29 Apr 2024, 18:53

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:
  "Player": {
    "Name": "John Doe",
    "Health": 42
  }
Outcome in XAML:
<ResourceDictionary>
    <Player x:Key="Player" Name="John Doe" Health="42"/>
</ResourceDictionary>
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!
 
User avatar
jsantos
Site Admin
Posts: 3939
Joined: 20 Jan 2012, 17:18
Contact:

Re: Implementing Dynamic Data Binding from JSON

01 May 2024, 11:21

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:
  • 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.
In C# it looks something like this. When a new C# type is going to be registered in C++, we send a bunch of information: type name, base class, list of properties (name+type)... and we do the following:
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);
}
The CreateProperty is like a switch for the supported property types to create the correct implementation:
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);
            ...
    }
}
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:
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;
};
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:
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);
  }
Please, let me know if you need more detail about this.
 
tandr
Topic Author
Posts: 4
Joined: 27 Mar 2024, 08:33

Re: Implementing Dynamic Data Binding from JSON

07 May 2024, 18:40

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:
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());
    }
}

This seem to register the type defined by json input so for instance
{ "Player": {"Name": "John Doe", "Health": "42"} }
will register type `Player` with properties `Name` and `Health`. Although when I try to use it for simple data binding I got errors
<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>
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    
Is this more or less how you imagine it should work? Any ideas what went wrong here? Again, thanks a lot for your help!
 
User avatar
jsantos
Site Admin
Posts: 3939
Joined: 20 Jan 2012, 17:18
Contact:

Re: Implementing Dynamic Data Binding from JSON

07 May 2024, 19:43

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:
<Button x:Name="button1" Content="{Binding Path=Name}" Width="100" Height="50">
<TextBlock Text="{Binding Health}"/>
This should work. Please, let me know.
 
tandr
Topic Author
Posts: 4
Joined: 27 Mar 2024, 08:33

Re: Implementing Dynamic Data Binding from JSON

08 May 2024, 09:27

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?
 
tandr
Topic Author
Posts: 4
Joined: 27 Mar 2024, 08:33

Re: Implementing Dynamic Data Binding from JSON

08 May 2024, 11:17

Got it fixed by adding base type on TypeBuilder:
    ty_builder->AddBase(TypeOf<NotifyPropertyChangedBase>()); 
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?
<Button x:Name="button1" Content="{DynamicResource Player.Name}" Width="100" Height="50">
Edit: Got it working like this:
<Button x:Name="button1" DataContext="{DynamicResource Player}" Content="{Binding Path=Name}"> 
 
User avatar
jsantos
Site Admin
Posts: 3939
Joined: 20 Jan 2012, 17:18
Contact:

Re: Implementing Dynamic Data Binding from JSON

11 May 2024, 13:28

A probably better way to implement this, is by implementing the IDictionaryIndexer and IListIndexer interfaces.
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>();

    }
};
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>();
    }
};
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:
{
  "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"
      ]
    }
  ]
}
You can bind to it this way:
<TextBlock Text="{Binding [members][1][powers][0]}"/>

Who is online

Users browsing this forum: Ahrefs [Bot], Bing [Bot], Google [Bot], mgb and 2 guests