Incorporating ValueTree in a code snippet

Hi all,

I am new to C++ and JUCE. I am building a new application that might get large, so I would like to adopt a good architecture from the beginning to limit future refactoring. My main concern, for now, is to separate the different view classes from the model classes, but I am not sure how to achieve this with the JUCE framework. I think at the center of the architecture there will be some ValueTrees, but I cannot understand how they will be integrated despite having looked at many examples.

Let’s consider the relatively simple case where I have a Playlist containing a list of Track. I create two classes to represent them, and they contain the model (i.e., logic and state) of my program. Then, I also created their respective views to let the user display and interact with them. Later, multiple views for each class in the model might exist:

class Playlist{
	Array<Track> tracks; // State
	void addTrack(){ // Logic
		tracks.add(new Track());
	}
} 
class Track{
	double length; // State
	void changePlaybackRate(double playbackRate){ // Logic
		length /= playbackRate;
	}
}

class PlaylistView: public Component, ValueTree::Listener{
	Playlist playlist; // Reference to the model in the view
	Button addButton; // UI element
	PlaylistView(Playlist p): playlist(p){
		playlist.addListener(this); // Somehow listen to changes in the state of playlist
		addAndMakeVisible(addButton);
		addButton.onClick = [this] { playlist.addTrack(); }; // interact with playlist
	}

	void valueTreePropertyChanged(…){ // When playlist state changes
		for(Track track: playlist.tracks) // Create child components to display each track
			addAndMakeVisible(new TrackView(track)); 
	}
}

class TrackView: public Component, ValueTree::Listener{
	Track track; // Reference to the model in the view
	Label label; // UI element
	TrackView(Track t): track(t){
		track.addListener(this); // Somehow listen to track state
	}

	void valueTreePropertyChanged(…){ // When track state changes
		label.setText(track.length); 
	}
}

I have two questions from this example:

  • Would you consider this architecture to be adequate?
  • How could I change my state in Playlist and Track to be able to listen to it in the views? I tried to change the type of Playlist.tracks to a ValueTree, but is it a good idea to store Track instances in it?

Thank you for your feedback!

It looks to me like you’re on the right track but are still a bit confused about how ValueTrees work. Each of your classes should have a ValueTree instance as a member variable, so that it can access the properties from that node of the tree. You’ll need to pass this ValueTree in through the constructor. You should also probably use CachedValue to access the properties. (I know that CachedValue is a bit confusing when you’re starting out, but trust me it’s worth investing time into getting to know it).

It looks like you’re also a bit confused about how to be adding and removing new elements in your structure. ( addAndMakeVisible(new TrackView(track)); is not a good idea by the way–it’s a memory leek). You should be using the valueTreeChildAdded() and valueTreeChildRemoved() listeners for this. You should set it up so that every time a new node is added to your ValueTree, a new node appears in your own structure.

1 Like

In case you haven’t seen it, this JUCE ADC talk by @dave96 is an invaluable resource when getting started with the ValueTree class.

3 Likes

Yes, this video is what made me want to try ValueTrees in the first place! I watched it a second time since I asked my question :slight_smile:

With my second watch I understood a couple of things:

  • You can build wrappers around the ValueTrees to hold the logic of the application (i.e., what is changing the state). Those are my Playlist and Track classes in the example above.
  • The ValueTree will not store the wrapper classes themselves, but only the state of the application. This means that you need to mirror the ValueTree structure with your class structure as @LiamG suggested:

You should set it up so that every time a new node is added to your ValueTree, a new node appears in your own structure.

  • This mirroring can be achieved automatically with a custom class like ValueTreeObjectList<Wrapper> given in the video (at 25 minutes).

I see a big difference in the examples @dave96 gave in the video compared to what I want to achieve, though. In his examples, his wrapper classes contain both the logic and the view implementation. In my code, I would like to decouple them. But this seems to complexify my architecture quite a bit, and I am not sure what is the best way to achieve that. Do you have any suggestions?

I haven’t been using ValueTreeObjectList, but have been employing more or less the same techniques in my application. I don’t like the use of raw pointers in ValueTreeObjectList; in my would be better for the child array to be std::vector<unique_ptr<ObjectType>> (or shared_ptr or juce::ReferenceCountedObjectPtr).

(Which type of smart pointer to use here is an important question, and you’ll need to think carefully about happens when an object is deleted.)

Like you said, separating the data model from the GUI is a good idea. It does add some complexity, but it will probably make the code much more robust. In my case, I have GUI element classes which take in their constructors a shared_ptr to the wrapped object of the specific type that it is modeling. That way, the GUI element is completely isolated from the ValueTree. I found that once I reached this level of organization, my code became much more reliable and easy to maintain.

1 Like

I haven’t used smart pointers so far, I will do some research accordingly, Thank you for the heads-up.

This is what I want to achieve! If I understood correctly, the GUI class should still listen to the changes in the ValueTree inside the wrapper class it received to update itself. Then reading the state, or sending the user’s commands, would use the accessors methods from the wrapper (using CachedValues).

I will try to reproduce your method.
Thank you very much!

I would recommend that the GUI class does not listen to the ValueTree, and in fact has nothing to do with the ValueTree. Instead, you can create you own custom listener class for the wrapper class, for the GUI class to register with. Your own custom listener methods can of course be triggered by ValueTree listeners though. So you might want to do something like this:


void valueTreePropertyChanged(ValueTree& vt, Identifier prop)
{
     if (prop == volume)
          listeners.call([vt](TrackListener& l){ l.volumeChanged(vt[volume]); });

     else if (prop == volume)
          listeners.call([vt](TrackListener& l){ l.panningChanged(vt[panning]); });

     // else if...
}

In the case I’ve described this might seem a bit redundant, but I’ve found this to be a great method for handling complicated callbacks, and it scales really well. The advantage is that you can have much more specialized and descriptive listener methods, without having to faff around with ValueTree properties.