Skip to content
Haiku OSDeep Dive Published Updated 8 min readViews unavailable

Haiku BListView: Item Lifetime, Selection, and Stable Model Updates

Build robust Haiku BListView screens by managing item ownership, selection notifications, stable model identity, scroll integration, and safe updates.

BListView is a row-oriented Interface Kit view for presenting and selecting a list of BListItem objects. It handles row layout, keyboard and pointer selection, scrolling integration, and message invocation. It does not turn each item into a child BView, and it does not take ownership of the item objects. Those two facts shape the design of every reliable list: keep row data small, manage item lifetime explicitly, and treat selection indexes as positions rather than durable record identifiers.

Haiku’s implementation source explicitly notes that BListView does not free its items itself. Adding a pointer to the list therefore does not transfer ownership. The application must ensure each item remains alive while present, remove it before deletion, and release it when the list is cleared or destroyed. This is easy to miss because many container APIs own inserted objects; BListView is not one of them.

Model rows as data, not nested views

A BListItem provides row-specific data and drawing behavior. BStringItem is appropriate for simple labels; a custom item can retain a stable record key and draw additional columns or status. Use a BListView for compact, row-based content. If every row needs a complex view hierarchy, independent focusable controls, or responsive child layouts, a vertical group of views may be a better model than forcing child BView instances into list items.

Keep the authoritative records in an application model. A BListItem can hold a stable ID and presentation fields, but the row index is only its current position. Sorting, inserting, removing, or filtering changes indexes. A delayed operation should carry a record ID or object reference that the model validates, not an index captured before the list changed.

class RecordItem : public BStringItem {
public:
    RecordItem(int64 id, const char* label)
        : BStringItem(label), fID(id) {}

    int64 ID() const { return fID; }

private:
    int64 fID;
};

This item extends the standard drawing behavior without creating child views. The application can use ID() to resolve the selected row back to its current model object. It should still verify that the ID exists and that the record is in the expected state before opening, deleting, or modifying it; the UI can be stale if the underlying model changed asynchronously.

Make item lifetime explicit

AddItem() returns whether insertion succeeded. Keep ownership outside BListView; if insertion fails, the caller still owns the object and must dispose of it. If insertion succeeds, do not delete the item while the list still references it. RemoveItem(index) returns the detached BListItem*, which allows a clear handoff to application code.

RecordItem* item = new RecordItem(record.id, record.label.String());
if (!listView->AddItem(item)) {
    delete item; // insertion failed; the list never stored this pointer
}

// Later, remove by current row and then release the detached object.
BListItem* detached = listView->RemoveItem(row);
if (detached != NULL)
    delete detached;

The view’s destructor does not delete rows, and MakeEmpty() clears its internal list without becoming a general-purpose object owner. If you store raw item pointers in a model-side container, remove each row and delete the item from one well-defined owner. Do not keep a stale item pointer after deletion, and do not free an item before its row has been detached.

For bulk updates, build new items before mutating the visible list, check every insertion result, and define rollback behavior if one allocation or insertion fails. RemoveItems() is useful for contiguous ranges but does not return the removed item pointers. If the application owns row objects that need individual destruction or reuse, remove them individually or maintain a parallel ownership collection that can safely identify the same objects before clearing.

Separate selection-change and invocation messages

BListView inherits BInvoker. SetSelectionMessage() configures notification for a selection change, while SetInvocationMessage() configures the message used when the current selection is invoked. Use the first for updating previews, enabling actions, or syncing a status area. Use invocation for a committed action such as opening the selected record. These are different user intents; treating every selection movement as activation can launch actions while a user is merely navigating with arrow keys.

The list’s invocation message includes an index field for the selected row. In multiple-selection mode, it adds an index value for each selected row. That payload still identifies positions at the moment of invocation, not stable model IDs. Resolve those row positions immediately against the current list and copy the stable IDs into the queued application operation.

enum {
    kSelectionChanged = 'slch',
    kOpenSelected = 'open'
};

listView->SetSelectionMessage(new BMessage(kSelectionChanged));
listView->SetInvocationMessage(new BMessage(kOpenSelected));
listView->SetTarget(window);

SetSelectionMessage() replaces and deletes the previous selection message, so pass a heap-allocated BMessage when constructing the list’s owned configuration. BInvoker::SetTarget() determines which handler receives invocations; configure it deliberately and verify it after the view is attached to its window. Handle messages in the target looper and avoid editing the list from an unrelated worker thread.

When a subclass needs a synchronous hook for application state, override SelectionChanged() and call the base implementation if future framework behavior or a further subclass may depend on it. A message is useful when another handler should receive the event; the hook is useful when the list subclass itself owns the behavior. Avoid performing long I/O in either path. Capture the current selection and schedule expensive work separately.

Enumerate multiple selection by selection ordinal

The argument to CurrentSelection(index) is the ordinal within the selected set, not the row number you want to inspect. For example, CurrentSelection(0) returns the first selected row, CurrentSelection(1) the next selected row, and -1 ends the enumeration. IsItemSelected(row) tests a row index directly. Confusing these two coordinate systems often produces loops that skip selected rows or repeatedly inspect the same one.

for (int32 selectedOrdinal = 0; ; ++selectedOrdinal) {
    const int32 row = listView->CurrentSelection(selectedOrdinal);
    if (row < 0)
        break;

    RecordItem* item = dynamic_cast<RecordItem*>(listView->ItemAt(row));
    if (item == NULL)
        continue;

    const int64 stableID = item->ID();
    // Resolve stableID in the application model before acting on it.
}

The dynamic cast in the example assumes that this list contains only RecordItem instances. If multiple item subclasses are valid, handle each type explicitly or use a shared application-defined base class. Do not dereference a cast result without checking it.

Selection is mutable UI state. Select(index, extend) and Select(start, end, extend) update it, and DeselectAll() clears it. The extend argument controls whether prior selection is retained; it does not mean “select all rows.” When programmatically restoring selection, validate every row against the current item count and ensure the list type supports the intended multiple-selection behavior.

Reconcile changes without confusing position and identity

The built-in SortItems() operation changes ordering and the current implementation clears selection before sorting. If preserving selection matters, record stable IDs first, sort or rebuild the view, then find the new rows for those IDs and reselect them. Do not preserve raw row indexes: after a sort, row 4 may represent a different record.

For asynchronous refreshes, attach a model generation number to the refresh result. Before replacing visible rows, verify that the response is still current. If the user selected a record while a request was in flight, restore that record by stable ID only if it remains present. If it was deleted or filtered out, clear selection and update dependent controls rather than leaving a dangling preview.

Avoid doing a sequence of deletes and inserts while a selection message handler interprets each intermediate state as a user decision. Use a scoped update flag or a small reconciliation routine that distinguishes application-driven rebuilds from user interaction. The list exposes SelectionChanged() as a hook and separate selection notification messages; whichever path is used should respect that distinction.

Scroll and draw with the list’s layout contract

Put long lists inside a BScrollView and let the list participate as the scroll target. ScrollToSelection() is preferable to manually estimating the row’s pixel position after keyboard navigation. When rows have variable heights, call the list’s item APIs and let BListView recalculate positions; do not compute a row’s y-coordinate as index * constantHeight unless every item is deliberately fixed to that height.

Custom BListItem::DrawItem() implementations should draw within the supplied frame and respect the completion/highlight state. Use Update() to calculate the item’s required height from the current font and available width. If data changes without replacing the row, update the item’s model fields and invalidate the affected row through the list. Do not rely on invalidating only the scroll-view parent to cause each list item to redraw itself.

Keep item drawing cheap. The list may redraw rows during scrolling, selection, resizing, or exposure. Do not perform file access, network requests, model mutations, or expensive image decoding in DrawItem(). Precompute or cache presentation data and invalidate only when the underlying row changes.

Acceptance checks for production lists

Test insertion failure, removal of the first/middle/last row, deleting a selected row, emptying the list, sorting, switching between single and multiple selection, and a refresh that removes the currently selected model object. Confirm that each detached BListItem is freed exactly once and that destroying the view does not leak the caller-owned items.

Test mouse selection, keyboard navigation, selection-change messages, invocation, and repeated index fields with multiple selected rows. Include variable-height items, narrow and wide windows, long content, and a scroll-to-selection check. If the list supports asynchronous updates, deliberately complete an old refresh after a newer one and verify that stale data is rejected.

The safe mental model is that BListView owns presentation mechanics, but the application owns row objects and durable records. Row indexes are temporary coordinates, selection notifications are not activation, and sorting can invalidate both assumptions. Give every item an explicit owner, keep a stable ID in the model, and make each update a deliberate reconciliation between the model and the visible list.

Related:

Sources:

Comments