Best practices to manage component life cycle according to value tree updates

Hi all,
I was wondering if someone could give me information on best practices to follow when creating/destroying components based on updates on the valuee tree.
For example, when a midiClip is created a node on the parent tree ( the track in this case ) is added and the valueTreeChildAdded() method is called while when it is removed the valureTreeChildRemoved() method is called.
Should I build/destroy my graphic objects in such methods ?
So I would like some advice on how to manage their lifecycle.
Thank you very much

This is a tricky problem, I’m not sure there is a one size fits all answer. You don’t want to delete when valureTreeChildRemoved() called, or if you are dragging a clip between tracks you component may get deleted mid drag. Pretty much everything in the engine is Selectable which means you can listen for selectableAboutToBeDeleted(), just be careful your UI doesn’t increase the reference count of any clips or then you’ll create a loop and then they’ll never get deleted. You may want to asynchronously update your UI after items are added or removed, but then be careful you have no dangling pointers.

I agree with you… but what about the undoManager ?
Il you don’t create/delete objs in valueTreeChildAdded()/valueTreeChildRemoved() how to deal with the undo/redo operations ?
For example, suppose that you have an EditComponent that has a listener on the edit’s state.
The ‘EditComponent’ has also an OwnedArray<Track> tracks;
When a new track is created a new node TRACK on the edit state is created, so :

class EditComponent : public Component , private ValueTreeAllEventListener{

public:
EditComponent(){}

protected :
void valueTreeChildAdded(ValueTree& parent,ValueTree& childAdded) override{
  Track* t = getTrackFromValueTree(childAdded);
  tracks.add(t);
  resized();
}

void valueTreeChildRemoved(ValueTree& parent,ValueTree& childRemoved,int order) override{
  String id = childRemoved.getProperty(IDs::id);
  for(auto * t : tracks){
    if(t->itemId.toString() == id){
      tracks.remove(t);
    }
  }
  resized();
}

private:

OwnedArray<Track> tracks;

};

In this way if you undo or redo the tracks array is consistent with the edit state ?
But as you said when a clip ( for example ) pass from one track to another there could be problems.

Maybe i can create the obj in the valueTreeChildAdded and remove them in the selectableAboutToBeDeleted() ?
In this case i need to add the listener to the object… and then remove it… for example if you have

class EditComponent : public Component , private ValueTreeAllEventListener, private SelectableListener{

public:
EditComponent(){}

protected :
void valueTreeChildAdded(ValueTree& parent,ValueTree& childAdded) override{
  Track* t = getTrackFromValueTree(childAdded);
   t->addSelectableListener(this);
  tracks.add(t);
  resized();
}

void valueTreeChildRemoved(ValueTree& parent,ValueTree& childRemoved,int order) override{
 
}

void selectableAboutToBeDeleted(Selectable* s) override{
Track* t = dynamic_cast<Track*>(s);
  if(t){
    for(auto* temp : tracks){
     if(temp == t)
       temp->removeSelectableListener(this);
       tracks.remove(temp);
    }
  }
}

private:

OwnedArray<Track> tracks;

};

Maybe it could work in this way ?

The way we do this in Waveform is to have a pool of clips and purge them periodically if they no longer belong on any tracks.

There’s two reasons for doing this, firstly, it solves the problem you mention here as we update asynchronously after clips have been added/removed, so if you’re dragging between tracks, the clip will belong to the new track by the time the update happens.

The second reason is that we can purge UI clips that are offscreen. This is a bit of an optimisation that probably isn’t required this day and age but it’s useful to know its possible.


This kind of question is coming up a lot recently and whether you’re synchronising UI or passing passing object representations between threads, the idea is the same. When something changes, you want to coalesce those changes in to a single update message. In the update message you can then say “does what I’m showing look like the model? If not, I’ll update so I do”.

This sounds long and complicated but it’s really not. There are all sorts of optimisations available doing it this way, pooling or ref-counting internal expensive state are some examples.

2 Likes

Thank you for your response,
I understand what you mean but I have implementation doubts :

  • How are the graphic objects created ? through the valueTreeChildAdded() ?
  • the “periodically” is meant through the use of a timer ? If yes, at what frequency ?

Ultimately, when the addClip() is called on a track, which operation must I perform to create the graphic object ? And when I delete it from the track how should I properly delete the existing object ?
Thanks again.

A very short example would be really appreciated

What I do is to have an array of component objects I can resize. These objects have a pointer to the data, which can be nullptr. When I add / remove childs to the value tree, I do create data objects. However, I do not create component objects, instead I call to a ChangeBroadcaster.

Then the parent of the components listen the ChangeListener. And it assign all its objects to the objects it already has. These component objects which are assigned to a new data, changes their content. If it needs more or it needs to delete, it does it here too (keeping a buffer). Unused components objects (data == nullptr) aren’t visible.

I do not use a Timer, I don’t like the idea of being constantly checking. Also, notice I’m not using Tracktion Engine, but maybe you could like this approach.

ok but in this way you will have an array with a very large size where so many elements in there has a nullptr… does it wort ?

Well, no, you may have an array with a very large size where zero elements (components) are nullptr, but the lastest ones may have a member variable pointing to a “data” which is nullptr.

It works in a similar way basic juce::Array does: when it needs to add an element and it doesn’t have remaining space, it will attach 8 spaces, but it will only use one of them, giving you the ilussion it only added the last element. It does this to avoid resize the data all the time.

In this way, you will have the easier of the two worlds: async heavy component object generation, and sync ValueTree generated/deleted small data objects for easier operation.

However, this async call may be extended to the “data generation” too, using the same mechanism, but usually for me it’s quite light.

Ok perfect,
i understood that you stored a reference to business object ( e.g a MidiClip ) in a component ( e.g MidiClipComponent ) and make null that reference when it no longer exists keeping the component alive.
If you make directly the component null it has more sense.
Thx.

I do not do the component null to avoid create&destroy them constantly. But yes, you got the idea. I attach another listener to the data inside each component, and so on…

Because juce::Component can’t be copied, containers of those are usually stored on the heap, using something like:
std::vector<std::unique_ptr<Component>> or OwnedArray<Component>.

So creating a new Component should be a very light operation, as all the container has to do is to move the pointers for the existing elements, without destroying them or copying their contents.

I believe that operation should be very fast even with thousands of existing Components.

Ok but how you decide to put the reference inside the component to null ?
I mean, you has an OwnedArray comps;
When you create the component ? It lives inside a class that has a listenere to the valueTree and so when a new node is added you can create and add a new Component to the array passing the reference to the create business object (e.g midiClip) ?.
Then supposing that you have created the comp… what type of listener do you attacch on it ? a valueTree listener ? a SelectableListener ?
Can you give me a short snipped of code… ?

Let me ilustrate with some pseudo-code:

You have a class which owns the ValueTree and generate / delete its childs. Lets call it DataContainer. This class have a “Data” class, which is related to the ValueTreeChild. Both, are ChangeBroadcaster

class DataContainer: public ValueTree::Listener, public ChangeBroadcaster
{
    void valueTreeChildAdded()
    {
         myChilds.add(new Data());
         sendChangeMessage();
    }
    void void valueTreeChildRemoved()
    {
         myChilds.remove(a target Data);
         sendChangeMessage();
    }

    class Data: public ChangeBroadcaster
    {
          setProperties(p1, p2)
          {
               property1 = p1;
               property2 = p2;
               sendChangeMessage();
          }
          int property1;
          int property2;
    }
    OwnedArray<Data> myChilds;
}

Now, you have a gui container for gui child, both classes should be Components / ChangeListeners. This is the GuiContainer:

class GuiContainer: public Component, public ChangeListener
{
  ~GuiContainer() {setData(nullptr);}

  void setData(DataContainer* container)
  {
      if (myContainer != container)
      {
           if (myContainer) myContainer.removeChangeListener(this);
           myContainer = container;
           if (myContainer) myContainer.addChangeListener(this);
           dataUpdated();
      }
  }

  void changeListenerCallback() {dataUpdated();}

  void dataUpdated()
  {
      // Here you read DataContainer and you create at least as many GuiChilds as you need
      if (myContainer.myChilds.size() > childs.size())
      {
           // add GuiChild to the OwnedArray<GuiChild> at least until it has childs.size() + 8 elements (up to you), to avoid constant resizing / generate / delete
      }
      else if (myContainer.myChilds.size() < childs.size() - 8)
      {
           // Remove childs here, but leave at least childs.size() + 8
      }

      // Now you have them created, you assing them a child
      for (int i=0; i!=myContainer->myChilds.size(); ++i)
      {
           childs.getUnchecked(i)->setData(myContainer->myChilds.getUnchecked(i));
           childs.setVisible(true);
      }
      // And you hide the unused components
      for (int i=myContainer->myChilds.size(); i!=childs.size(); ++i)
      {
          childs.getUnchecked(i)->setVisible(false);
      }

      // You would place the components bounds, or you would repaint here
  }

  DataContainer* myContainer = nullptr;
  OwnedArray<GuiChild> childs;
}

The Child gui should be something like ChildContainer. It may also have other childs, and since it’s quite similar to GuiContainer (in fact is the same idea), you may template them, I’m not going to repeat it all, but in this way:

class GuiChild: public Component, public ChangeListener
{
   void setData(DataContainer::Data* data)
   {
       // same technique as GuiComponent::setData
   }
   void changeListenerCallback() {dataUpdated();}

   void dataUpdated() {repaint();}
   DataContainer::Data* myData = nullptr;
}
1 Like

Ok perfect… thxs so much :grinning: :grinning: