Clearing custom components in TableListBox


#1

Greetings,

I am working with refreshComponentForCell in TableListBoxModel in an attempt to load custom components in various cells. These custom components are being stored in an OwnedArray of a class that keeps it’s own OwnedArray that may store these custom components.

The problem I’m running into is that when scrolling through the page, my refreshComponentForCell method checks to see if the cell should be empty, and whether it is or not, and if there’s a component where it shouldn’t be, it attempts to clear it out. Fairly simple when creating these components on the heap in the refreshComponentForCell method, since I can just delete them. But what I am struggling with is reusing components already allocated elsewhere (in my OwnedArray), since I would like to just point to them as the component and then just clear that out when not needed (so that they are not deleted). but it seems if I set existingComponentToBeUpdated to nullptr and return it, the model does not properly point to 0 any more - it seems the component stays and is reused.

Is there a better way of handling a table in which components that have already been allocated are used, and not necessarily deleted when no longer needed (specifically so when scrolling)?

Thanks


#2

with the ListBox class, the best thing to do is make dynamic components that aren’t owned (the class that calls refreshComponentForRow is who owns the component) but just reflect your data source.

Take a look here: ListBox Drag-To-Reorder [Solved] at how I store the data separately from the components that actually display the data.


#3

Unfortunately that’s not quite what I’m looking for, since my goal is to have my custom table class manage the life of all cell components. My ultimate goal is to have a method to add custom components to my OwnedArray and then reference them as needed for each cell. The custom table class will take care of their destruction.

So the TableListBoxModel simply won’t accept a nullptr as an empty cell? And there’s no way to keep it from destroying components to remove them from reused cells? That may end up causing some problems.


#4

when you need to modify your displayed data, modify the data in your data model, and then call TableListBox::updateContent() to have your TableListBox reflect the updated content. If you need empty rows, you need to figure out how to provide an empty view of your data, like a dummy component or something.

you’re running into problems because you’re not separating your data model from your view. the ListBox and TableListBox classes require you separate them and let the ListBox/TableListBox base class manage the lifetime of the component returned from refreshComponentForRow/Cell()


#5

I suppose it’s a bit of an ideological difference between what the class was written for, and what I’m trying to accomplish. In regards to the way you have explained it, the cell components ARE my data. I can go about using “dummy” components to reset the display, without deleting the actual component within the model, but it just seems messy when a null address should (at least the way I see it) suffice to clear said cell.

Again, a difference in what the class was probably designed for vs what I’m trying to accomplish (tying the model and the actual table together, since the model will never be used in a different table), but it seems that it would be so simple to accomplish the same thing by accepting a nullptr.

Thank you though for confirming my suspicion that I wouldn’t be able to do that!


#6

I went through the exact same problem with my drag-to-reorder code. once I came around to the idea of separating the data from the view, it worked very smoothly. I wish the documentation would have indicated that’s how we should be using the ListBox and TableListBox classes. it would have saved me some time.


#7

In an effort to avoid reinventing the wheel, I’m trying to work around the issue by attempting to reassign existingComponentToUpdate before deleting/returning. This works just fine for non-custom components, but for the custom components, it still refers to it. Am I missing why? Ultimately nullptr is being returned anyway.

Component* refreshComponentForCell(int rowNumber, int columnId, bool isRowSelected, Component* existingComponentToUpdate)
{
    CustomCellComponent* existingCustom = dynamic_cast<CustomCellComponent*>(existingComponentToUpdate);
    // Following if cell is supposed to be empty
    if(existingComponentToUpdate)
    {
        if(!existingCustom) { delete existingComponentToUpdate; }
        else
        {
            existingComponentToUpdate = new Component();
            delete existingComponentToUpdate;
        }
        existingComponentToUpdate = nullptr;
        return existingComponentToUpdate;
}

I know this isn’t necessarily a Juce fault (at least I don’t believe so), but I’m trying to figure out why a reassignment of existingComponentToUpdate won’t clear out the location of the previous custom component it was pointing to.


#8

you should do something like this:

    Component* refreshComponentForRow (int rowNumber,
                                       bool isRowSelected,
                                       Component *existingComponentToUpdate) override
    {
        ScopedPointer<ListBoxItem> item( dynamic_cast<ListBoxItem*>(existingComponentToUpdate) );
        //remember, 'items' is the data that we are generating customized views of in this function
        if( isPositiveAndBelow(rowNumber, (int)items.size() ) )
        {
            //this reassigns the ScopedPointer, which deletes the existingComponentToUpdate automatically.  
            //you could use std::unique_ptr here also.  
            //check out ScopedPointer<>::operator=() to see why this works
            item = new ListBoxItem(items[rowNumber].idNum, items[rowNumber].c, *this, rowNumber );
            // existingComponentToUpdate has been deleted at this point.
        }
        else 
        {
            //existingComponentToUpdate has NOT been deleted, but the rowNumber points
            //to data we don't have, so we should return a nullptr
            item.reset();
        }
        //return the pointer currently owned by the ScopedPointer
        //as the ScopedPointer is about to be destroyed, 
        //deleting the object it owns in the process
        //if we didn't call item.release(), that object would be deleted.
        //by returning the object returned from 'item.release()', that object is kept alive
        return item.release();
    }

#9

let’s break that down:

CustomCellComponent* existingCustom = dynamic_cast<CustomCellComponent*>(existingComponentToUpdate);

That may or may not have successfully cast, so existingCustom might be a nullptr.

    if(existingComponentToUpdate)

checking if you were passed an actual component…

        if(!existingCustom) { delete existingComponentToUpdate; }

so, if the cast failed and existingCustom is a nullptr, delete the component that was passed to us?

        else
        {
            existingComponentToUpdate = new Component();

the cast didn’t fail, so we have existingCustom holding the pointer to the Component that was passed in. and now you’re creating a new Component and replacing what existingComponentToUpdate points to. notice I said “points to”, not “owns”.

            delete existingComponentToUpdate;
        }

you just deleted the component you just created. why even bother creating it? both existingCustom and existingComponentToUpdate point to the same object if the dynamic_cast<> succeeded.

        existingComponentToUpdate = nullptr;
        return existingComponentToUpdate;

I guess you’re trying to follow the delete ptr; ptr = nullptr; pattern when manually deleting pointers to objects? All of these problems go away if you use a ScopedPointer<> or std::unique_ptr to have ownership of the existingComponentToUpdate.
You might hit up the discord channels (either https://discord.gg/FvpVvjN or https://discord.gg/SkAJA5d) and explain what you’re really trying to accomplish there to get a more real-time answer.


#10

This is a good thing, as I am also performing dynamic_casts to the other possible types of components that were possibly used in the table previously (namely label and a custom checkbox class). Basically, my goal is to delete the components if they aren’t a custom component, since they would have been previously allocated through a separate refreshComponentForCell call.

Yes, see above.

That’s sort of the main problem I’m having. If I point existingComponentToUpdate to a new (and different) component, and then return that component, why upon scrolling am I seeing the previous (and non-deleted) pointed-to component? This is disregarding the unnecessary delete operation (which was in place to avoid using an unnecessary component, and seemed to work with any of the other “deleted” component pointers).

I suppose this is a fair point in regards to the non-custom components, and I may look into that later on, but doesn’t help when the component I want to return is stored in an OwnedArray in my custom class. I wouldn’t want it to be destroyed upon going out of scope.

I truly do appreciate the continued help! Hopefully I’m not coming across as stubborn, I’m just trying to figure out a way to make this class, which evidently wasn’t designed for this, to accomplish the task I’m trying to do.


#11

i mean, you could roll your own if you don’t need everything it offers…

struct MyFakeListBox : public Component {
    OwnedArray<DataView> dataView;
    std::vector<Data> data;

//======================
    void refreshView() {
        dataView.clear();
        for( auto& item : data )
        {
            dataView.add( new DataView( item ) );
            addAndMakeVisible( dataView.getLast() );
        }
        resized();
    }

    void resized() override {
        int idealHeight = 50;
        int counter = 0;
        for( auto* view : dataView )
        {
            view->setBounds(0, counter * idealHeight, getWidth(), idealHeight );
            counter += 1; 
        }
    }
};

#12

I think, stepping a bit out from the implementation details, the problem is, that the ListBox/ListBoxModel is not really designed to be polymorph. You need to be very lucky, if the component, that just left the view is the type you need.

Option 1: create one custom component, that aggregates the functionality you need in a member and switches depending on the type in the model, you want to represent

Option 2: have your own cache of allocated components, so you don’t destroy the component but mark it as unused, and just return another one from the cache of the other type.

IMHO the second option is overkill, has a lot to take care of and is error prone for little benefit.

This is pseudo code:

class MyCellComponent : public Component
{
public:
    enum class DataType
    {
        checkbox,
        label
    };
    MyCellComponent()
    {
        addChildComponent (checkbox);
        addChildComponent (label);
    }
    void setType (DataType t)
    {
        checkbox.setVisible (t == DataType::checkbox);
        label.setVisible (t == DataType::label);
    }
    void resized() override
    {
        label.setBounds (getLocalBounds().reduced (1);
        combobox.setBounds (getLocalBounds().reduced (1);
    }
private:
    Label label;
    CheckBox checkbox;
};

Component * MyListBoxModel::refreshComponentForRow (int rowNumber, bool isRowSelected, Component *existingComponentToUpdate)
{
    auto* myCell = dynamic_cast<MyCellComponent*> (existingComponentToUpdate);
    if (myCell)
    {
        myCell->setType (/* whatever in model */);
        return myCell;
    }
    if (existingComponentToUpdate != nullptr)
        delete existingComponentToUpdate;

    existingComponentToUpdate = new MyCellComponent();
    existingComponentToUpdate->setType (/*whatever in model*/);
    return existingComponentToUpdate;
}

Good luck!