Typed ValueTree structure

In an application where the same ValueTree’s are used in many different placed/views/components, how would you recommend using typed “wrappers” of the actual ValueTrees? I watched David Rowland’s great talk about ValueTrees (https://www.youtube.com/watch?v=3IaMjH5lBEY) and am planning to use the “typed wrapper” approach he describes.

For example consider the data model is a list of songs and a volume value and there’s a “CurrentSong” and “SongList” view. I see two ways to deal with multiple components using the same ValueTree

  1. Parse the full model into a typed class that has properties corresponding to the ValueTree’s structure, eg “AppModel” which has a list of Songs and a Volume value. Render the view based on the typed instances, so pass around the typed wrappers instead of value trees. Use a sort of facade pattern on the typed classes that listens to the actual change triggers on the ValueTree.

  2. Render the view from the ValueTree, and let the components themselves create the typed wrappers they need.

Rowland seems to be using 2., but I might very well be mistaken. He also seems to be mixing the view/ui components and typed wrappers into a single entity, there doesn’t seem to be a clear distinction between what is data and what is rendering that data (except of course the data is in the ValueTree).

It seems to me that 1. would be easier to design since there’s a clear distinction between data and view, and the ValueTree would be practically abstracted away from anything but the typed wrappers. There might be downsides to this approach that I’m not aware of.

What approach do you use or recommend?

1 Like

You may be able to achieve what you’re going for in approach 1 by using CachedValue to store a type-safe reference to a property inside a ValueTree. You can store the ValueTree inside your model class and expose its properties as public CachedValue members.

This approach does have some trade-offs – for example, if you have one or more ValueTree::Listener objects listening to the tree that the CachedValue references, the listener’s valueTreePropertyChanged() callback may occur before the CachedValue has been updated (can be worked around by using forceUpdateOfCachedValue()). There’s some really great information about this approach and some similar alternatives in this thread .

2 Likes

I use option 1. All of my ValueTrees are attached to one of two top level ValueTrees (persistent and runtime). These are passed into just about all of my classes (backend and gui) where the classes use the corresponding wrappers that they are interested in. I have a ValueTreeWrapper class which makes it super easy to spin up new wrappers. Here is the most brief of example to explain some…

MyApp::initiailize()
{
    // other set up stuff
    // 'wrapOrCreate' is used by the owner of the VT
    // it will look for the VT as either the one passed in, or a child of it
    // if not found it will create it
    guiProperties.wrapOrCreate (runtimePropertiesVT);
    layoutProperties.wrapOrCreate (persistentPropertiesVT);

    transport.init (persistentPropertiesVT, runtimePropertiesVT);

    mainWindow = std::make_unique<MyMainWindow> ("", persistentPropertiesVT, runtimePropertiesVT);
}

Transport::init (ValueTree persistentPropertiesVT, ValueTree runtimePropertiesVT)
{
    transportProperties.wrapOrCreate (runtimePropertiesVT);
}

MyMainWindow::MyMainWindow (String title, ValueTree persistentPropertiesVT, ValueTree runtimePropertiesVT)
{
    transportComponent.init (persistentPropertiesVT, runtimePropertiesVT)
}

TransportComponent::init (ValueTree persistentPropertiesVT, ValueTree runtimePropertiesVT)
{
    // `wrap` is used by clients of the data
    // it will look for the VT as either the one passed in, or a child of it
    // if it does not find it, it will return an invalid ValueTree
    transportProperties.wrap (runtimePropertiesVT);
    transportProperties.onStateChange = [this] (TransportState transportState) { updateTransport (transportState); };
    updateTransport (transportProperties.getState ());
}

TransportComponent::stopButtonClicked()
{
    transportProperties.setState (TransportState::stop);
}
2 Likes

That looks nice! Any chance you could share the wrapper class? :innocent:

Do you handle lists through the wrapper as well? What I thought about was using a child VT for each property that is a list and storing the items inside that child, instead of storing all items as direct children and filtering based on type. So eg. for a property like root.songs would be

<root projectName="foobar">
    <songs>
        <song/>
        ... 
    </songs>
   <authors>
       </author>
       ...
   </authors>
</root>

And then I supposed you could have a generic wrapper class that handles the “songs” element and creates it if empty etc, like you described

Great link, thanks! Was planning on using CachedValues, so very nice to know about potential timing issues.

I’d be glad to share the wrapper… there are some changes I want to make to it, but I can’t take the time while I am mid-project. :slight_smile:

For Lists I have the list owner valuetree wrapper, which has API’s to manage children, with old school style getNumChildren (where Children would be context specific, getNumSongs), and getChild(int childIndex), along with more modern style forEachSong(std::function<void(ValueTree song)> songCallback). Note the returned values are the ValueTree’s not the wrappers, as wrappers are not meant to be passed around, ie. they wrap locally.

Thank you so much, this is super useful! Why are the wrappers not meant to be passed around? If you select a song from the song list, and have a component that shows the selected song, would you then store an id of the song somewhere in the VT hierarchy or pass the selected VT of the song around?

Also, any reason for not using CachedValue's?

You can pass them around, it just doesn’t fit my design, ie. that it is a lightweight object that can be used anywhere. There can also be the callback issue, where you inadvertently replace a callback in the wrapper, ie. there are not callback lists, just the simple onThing type callback. But, there are a few times where I pass a reference to one, but usually I leave it to the consumer of the data to just wrap the valuetree.

as for CachedValues, I wouldn’t use them in the wrapper, since it is doing all the same stuff, but you could modify the wrapper to cache the values in the same way. there would be a some work involved in modifying the wrapper to make that easy to setup, but it would certainly be useful in the way CachedValue is, where you don’t incur the cost of ValueTree code to get the value.

Hi, any chance to see your wrapper, sounds really interesting!

I’m happy to share what I have in use:

namespace IDs
{
#define DECLARE_ID(name)  const juce::Identifier name(#name);
  DECLARE_ID(SONG);
  DECLARE_ID(associatedPrgNr);
  DECLARE_ID(songNumber);
  DECLARE_ID(songName);
  DECLARE_ID(isAttacaFromSongBefore);
  DECLARE_ID(isSongExtension);
#undef DECLARE_ID
}



class Song
  : private ValueTree::Listener
{
  public:
    class Listener
    {
      public:
        virtual                               ~Listener() noexcept = default;
        
        virtual void                          associatedPrgNrChanged(juce::TPrgNr);
        virtual void                          songNumberChanged(const juce::String&);
        virtual void                          songNameChanged(const juce::String&);
        virtual void                          isAttacaFromSongBeforeChanged(bool);
        virtual void                          isSongExtensionChanged(bool);
    };
    
                                              Song();
    explicit                                  Song(const ValueTree& vt);
                                              Song(const Song& other);
    
    Song&                                     operator=(const Song& inlay);
    void                                      swap(Song& inlay) noexcept;
    
    juce::TPrgNr                              getAssociatedPrgNr() const;
    juce::String                              getSongNumber() const;
    juce::String                              getSongName() const;
    bool                                      getIsAttacaFromSongBefore() const;
    bool                                      getIsSongExtension() const;
    
    void                                      setAssociatedPrgNr(juce::TPrgNr value, UndoManager* undo = nullptr);
    void                                      setSongNumber(const juce::String& value, UndoManager* undo = nullptr);
    void                                      setSongName(const juce::String& value, UndoManager* undo = nullptr);
    void                                      setIsAttacaFromSongBefore(bool value, UndoManager* undo = nullptr);
    void                                      setIsSongExtension(bool value, UndoManager* undo = nullptr);
    
    bool                                      isValid() const;
    void                                      deleteSelf(UndoManager* undo = nullptr);
    
    void                                      addListener(Listener& listener);
    void                                      removeListener(Listener& listener);
    
  private:
    void                                      valueTreePropertyChanged(ValueTree&, const Identifier& property) override;
    void                                      valueTreePropertyChanged_associatedPrgNr();
    void                                      valueTreePropertyChanged_songNumber();
    void                                      valueTreePropertyChanged_songName();
    void                                      valueTreePropertyChanged_isAttacaFromSongBefore();
    void                                      valueTreePropertyChanged_isSongExtension();
    
    void                                      referValues();
    
    
    ValueTree                                 valueTree;
    ListenerList<Listener>                    listeners;
    
    CachedValue<int>                          associatedPrgNr;
    CachedValue<juce::String>                 songNumber;
    CachedValue<juce::String>                 songName;
    CachedValue<bool>                         isAttacaFromSongBefore;
    CachedValue<bool>                         isSongExtension;
    
    
    friend class SongTable;
    JUCE_LEAK_DETECTOR(Song);
};

#include "Song.h"
#include "SongTable.h"
#include "ValueTreeUtils.h"

void Song::Listener::associatedPrgNrChanged(juce::TPrgNr)     {}
void Song::Listener::songNumberChanged(const juce::String&)   {}
void Song::Listener::songNameChanged(const juce::String&)     {}
void Song::Listener::isAttacaFromSongBeforeChanged(bool)      {}
void Song::Listener::isSongExtensionChanged(bool)             {}


Song::Song()
  : Song(ValueTree(IDs::SONG))
{}


Song::Song(const Song& other)
  : Song(other.valueTree)
{}


Song::Song(const ValueTree& vt)
  : valueTree(vt)
{
  jassert(valueTree.hasType(IDs::SONG));
  referValues();
  valueTree.addListener(this);
}


Song& Song::operator=(const Song& other)
{
  auto copy(other);
  swap(copy);
  return *this;
}


void Song::swap(Song& other) noexcept
{
  std::swap(valueTree, other.valueTree);
  referValues();
}


void Song::referValues()
{
  associatedPrgNr.referTo(valueTree,          IDs::associatedPrgNr,         nullptr, (int) juce::TProgram_Min);
  songNumber.referTo(valueTree,               IDs::songNumber,              nullptr, "");
  songName.referTo(valueTree,                 IDs::songName,                nullptr, "");
  isAttacaFromSongBefore.referTo(valueTree,   IDs::isAttacaFromSongBefore,  nullptr, false);
  isSongExtension.referTo(valueTree,          IDs::isSongExtension,         nullptr, false);
}


void Song::valueTreePropertyChanged(ValueTree&, const Identifier& property)
{
  if (property == IDs::associatedPrgNr)
    valueTreePropertyChanged_associatedPrgNr();
  else if (property == IDs::songNumber)
    valueTreePropertyChanged_songNumber();
  else if (property == IDs::songName)
    valueTreePropertyChanged_songName();
  else if (property == IDs::isAttacaFromSongBefore)
    valueTreePropertyChanged_isAttacaFromSongBefore();
  else if (property == IDs::isSongExtension)
    valueTreePropertyChanged_isSongExtension();
  else
    jassertfalse;
}

#define IMPL_Song_valueTreePropertyChanged(__field, __listenerFunc)                     \
  __field .forceUpdateOfCachedValue();                                                  \
  listeners.call([this](auto& listener) { listener. __listenerFunc ( __field ); });

void Song::valueTreePropertyChanged_associatedPrgNr()           { IMPL_Song_valueTreePropertyChanged(associatedPrgNr,          associatedPrgNrChanged); }
void Song::valueTreePropertyChanged_songNumber()                { IMPL_Song_valueTreePropertyChanged(songNumber,               songNumberChanged); }
void Song::valueTreePropertyChanged_songName()                  { IMPL_Song_valueTreePropertyChanged(songName,                 songNameChanged); }
void Song::valueTreePropertyChanged_isAttacaFromSongBefore()    { IMPL_Song_valueTreePropertyChanged(isAttacaFromSongBefore,   isAttacaFromSongBeforeChanged); }
void Song::valueTreePropertyChanged_isSongExtension()           { IMPL_Song_valueTreePropertyChanged(isSongExtension,          isSongExtensionChanged); }


void Song::setAssociatedPrgNr(juce::TPrgNr value, UndoManager* undoManager)
{
  associatedPrgNr.setValue((int) value, undoManager);
  sortValueTreeChild<int>(valueTree, [](auto vt) { return vt.getProperty(IDs::associatedPrgNr).operator int(); }, undoManager);
}


void Song::deleteSelf(UndoManager* undoManager)
{
  jassert(valueTree.getParent().isValid());
  valueTree.getParent().removeChild(valueTree, undoManager);
}


void Song::setSongNumber(const juce::String& value, UndoManager* undoManager) { songNumber.setValue(value,              undoManager); }
void Song::setSongName(const juce::String& value, UndoManager* undoManager)   { songName.setValue(value,                undoManager); }
void Song::setIsAttacaFromSongBefore(bool value, UndoManager* undoManager)    { isAttacaFromSongBefore.setValue(value,  undoManager); }
void Song::setIsSongExtension(bool value, UndoManager* undoManager)           { isSongExtension.setValue(value,         undoManager); }


juce::TPrgNr    Song::getAssociatedPrgNr()          const { return associatedPrgNr; }
juce::String    Song::getSongNumber()               const { return songNumber; }
juce::String    Song::getSongName()                 const { return songName; }
bool            Song::getIsAttacaFromSongBefore()   const { return isAttacaFromSongBefore; }
bool            Song::getIsSongExtension()          const { return isSongExtension; }
bool            Song::isValid()                     const { return valueTree.getParent().isValid(); }


void Song::addListener(Listener& listener)                 { listeners.add(&listener); }
void Song::removeListener(Listener& listener)              { listeners.remove(&listener); }



#pragma once

#include "JuceHeader.h"
#include "Song.h"

namespace IDs
{
#define DECLARE_ID(name)  const juce::Identifier name(#name);
  DECLARE_ID(SONG_TABLE);
#undef DECLARE_ID
}



class SongTable
  : private ValueTree::Listener
{
  public:
    class Listener
    {
      public:
        virtual                               ~Listener() noexcept = default;
        
        virtual void                          songAdded(const Song& song);
        virtual void                          songOrderChanged(int ixFrom, int ixTo);
        virtual void                          songRemoved(const Song& song, int index);
        virtual void                          songChanged(const Song& song);
    };
    
                                              SongTable();
    explicit                                  SongTable(const ValueTree& vt);
                                              SongTable(const SongTable& other);
    
    SongTable&                                operator=(const SongTable& other);
    void                                      swap(SongTable& other) noexcept;
    
    Song                                      createNewSong(UndoManager* undoManager = nullptr);
    void                                      deleteSong(const Song& inlay, UndoManager* undoManager = nullptr);
    size_t                                    getNumSongs() const;
    Song                                      getSong(size_t ix);
    Song                                      findSongByProgram(TPrgNr program);
    Song                                      findSongByNumber(const juce::String& numberPattern);
    int                                       indexOfSong(const Song& inlay) const;
    
    void                                      addListener(Listener& listener);
    void                                      removeListener(Listener& listener);
    
  private:
    void                                      valueTreePropertyChanged(ValueTree& vt, const Identifier& property) override;
    void                                      valueTreeChildOrderChanged(ValueTree&, int oldIndex, int newIndex) override;
    void                                      valueTreeChildAdded(ValueTree&, ValueTree& child) override;
    void                                      valueTreeChildRemoved(ValueTree&, ValueTree& child, int ixChild) override;
    
    Song                                      findSongByNumber_exact(const juce::String& number);
    
    
    ValueTree                                 valueTree;
    ListenerList<Listener>                    listeners;
    
    JUCE_LEAK_DETECTOR(SongTable);
};


void SongTable::Listener::songAdded(const Song&)          {}
void SongTable::Listener::songRemoved(const Song&, int)   {}
void SongTable::Listener::songOrderChanged(int, int)      {}
void SongTable::Listener::songChanged(const Song&)        {}


SongTable::SongTable()
  : SongTable(ValueTree(IDs::SONG_TABLE))
{}


SongTable::SongTable(const SongTable& other)
  : SongTable(other.valueTree)
{}


SongTable::SongTable(const ValueTree& vt)
  : valueTree(vt)
{
  jassert(valueTree.hasType(IDs::SONG_TABLE));
  
  valueTree.addListener(this);
}


SongTable& SongTable::operator=(const SongTable& other)
{
  auto copy(other);
  swap(copy);
  return *this;
}


void SongTable::swap(SongTable& other) noexcept
{
  std::swap(valueTree, other.valueTree);
}


Song SongTable::createNewSong(UndoManager* undoManager)
{
  Song song;
  
  if (valueTree.getNumChildren() != 0)
    song.setAssociatedPrgNr(jmin((juce::TPrgNr) (getSong(getNumSongs() - 1).getAssociatedPrgNr() + 1), juce::TProgram_Max));
  
  valueTree.addChild(song.valueTree, valueTree_appendChild, undoManager);
  return song;
}


Song SongTable::findSongByProgram(TPrgNr program)
{
  for (auto i = 0; i < valueTree.getNumChildren(); ++i)
    if (auto child = valueTree.getChild(i);  child.hasType(IDs::SONG) and child.getProperty(IDs::associatedPrgNr).operator int() == program)
      return Song(child);
  return Song();
}


Song SongTable::findSongByNumber(const juce::String& number)
{
 // omitted
}


Song SongTable::findSongByNumber_exact(const juce::String& number)
{
 // omitted
}


void SongTable::deleteSong(const Song& song, UndoManager* undoManager)
{
  jassert(song.valueTree.getParent() == valueTree);
  valueTree.removeChild(song.valueTree, undoManager);
}


void SongTable::valueTreeChildAdded(ValueTree&, ValueTree& child)
{
  jassert(child.hasType(IDs::SONG));
  listeners.call([song = Song(child)](auto& listener) { listener.songAdded(song); });
}


void SongTable::valueTreeChildRemoved(ValueTree&, ValueTree& child, int ixChild)
{
  jassert(child.hasType(IDs::SONG));
  listeners.call([song = Song(child), ixChild](auto& listener) { listener.songRemoved(song, ixChild); });
}


void SongTable::valueTreeChildOrderChanged(ValueTree&, int oldIndex, int newIndex)
{
  listeners.call([oldIndex, newIndex](auto& listener) { listener.songOrderChanged(oldIndex, newIndex); });
}


void SongTable::valueTreePropertyChanged(ValueTree& vt, const Identifier& property)
{
  if (vt.hasType(IDs::SONG))
    listeners.call([song = Song(vt)](auto& listener) { listener.songChanged(song); });
}


size_t    SongTable::getNumSongs()                    const { return valueTree.getNumChildren(); }
Song      SongTable::getSong(size_t ix)                     { return ix < getNumSongs() ? Song(valueTree.getChild((int) ix)) : Song(); }
int       SongTable::indexOfSong(const Song& song)    const { return valueTree.indexOf(song.valueTree); }


void SongTable::addListener(Listener& listener)      { listeners.add(&listener); }
void SongTable::removeListener(Listener& listener)   { listeners.remove(&listener); }

You pass those wrappers same way around as you would ValueTrees. I however recently discovered a major flaw when using ValueTree: when you subscribe to SongTable for changes and the SongTable is part of bigger ValueTree structure, you probably can’t no longer load your data by calling ::copyPropertiesAndChildren because that gets ride of the SongTable entry and readds a new one. Your Song list will then mostliekly refer to an unattached SongTable that is just floating around in the RAM.

Sure thing. I need to put it into its own repo, but for now you can access it from this project. You can see some usage patterns from the project as well. Here is a link to the code and the main repo.

@Rincewind makes a valid point about using ValueTree::copyPropertiesAndChildren being problamatic, so I tend to implement copy routines in the wrappers that have children.

I’m sure there is a lot of room for improvement in my code, but it does the job for me. :slight_smile:

1 Like

Thanks @Rincewind and @cpr2323, much appreciated.