ListView / TileView: Set List Items, entry lifecycle, and refresh¶
At a glance
Lives on: ListView and TileView (TileView uses the same item/entry model with a grid layout) ·
Set List Items: replaces the list's array of item UObject references — returns nothing ·
Visible entries: only enough pooled User Widgets for the visible area ·
Request Refresh: reconciles added/removed/visible items on the next tick — does not refresh fields inside an existing item ·
Regenerate All Entries: releases and reassigns every entry without destroying the pooled widgets ·
Official pages: Set List Items ·
Request Refresh ·
UListView
The one-minute version¶
- A ListView does not own one permanent row widget per item. It stores item object references, then creates only enough entry widgets for the visible rows. As you scroll, entries are released and reused for different items.
- Set List Items replaces the whole item source. Add Item, Remove Item, and Clear List Items mutate that source incrementally. Get List Items returns the items, not the visible widgets.
- The durable identity is the item object. An entry widget is a temporary view. Inventory count, selected equipment, quest state, and authoritative mutation do not belong in a recycled entry.
- Implement User Object List Entry on the entry widget. Whenever the list assigns an item, On List Item Object Set must read/cast that item and repaint every visual field. Never assume Construct means "new item."
- On Entry Released means the widget no longer represents its previous item. Unbind item delegates and clear cached item references there.
- Request Refresh is deferred and only reconciles item↔entry discrepancies.
Changing
Quantityinside an item that is already present does not itself create a discrepancy. Have the item broadcast a change that its current entry listens to, or use Regenerate All Entries as a heavier simple fallback.
Items and entries are different objects¶
Epic describes UListView as virtualized. If a list has 500 item objects but only 8 rows fit in the panel, the list may need roughly 8–10 entry widgets, not 500.
durable item source
Item_A Item_B Item_C ... Item_500
│ │ │
└────── ListView / TileView ──────┐
│ visible window
pooled entry widgets ▼
Entry_1 -> currently represents Item_42
Entry_2 -> currently represents Item_43
Entry_3 -> currently represents Item_44
...
Scroll down and Entry_1 may next represent Item_50. The widget instance can
stay alive while its item changes.
That is why this tempting setup is wrong:
Entry Widget Construct
-> create/store "my inventory item"
-> change inventory quantity from the widget
The entry can be released, reused, or off screen. Use the inventory/model owner as truth, pass a durable item UObject to the list, and let the entry display that object.
What Set List Items actually does¶
Set List Items(In List Items) replaces the ListView's item-object collection.
It does not copy the values inside those UObjects into new model instances.
The list stores references to the same objects you supplied.
The item source changes immediately from the UListView API's point of view; entry generation/release is reconciled by the underlying Slate list during its refresh. That means code later on the same execution chain can read the new item collection, while visible entry events may arrive on the refresh tick.
The item operations side by side¶
| Node | Item-source change | Useful rule |
|---|---|---|
| Set List Items | Replace the complete collection | Use when rebuilding from one authoritative snapshot |
| Add Item / Add Items | Append object references | Use for accepted incremental additions |
| Remove Item / Remove Items | Remove matching object references | Remove by the exact item identity the list holds |
| Clear List Items | Remove every item | An empty list is normal, not failure |
| Get List Items | Read all item references | These are not entry widgets; do not mutate a returned Array and expect the list source to change |
| Get Displayed Entry Widgets | Read only currently generated visible entries | Useful for inspection, not durable model iteration |
These mutators have no success Boolean. If your gameplay operation can be rejected (full inventory, stale ID, server denial), that result belongs to the inventory/model command before the UI list is changed.
Item identity, None, and duplicates¶
UListView is object-identity based. Selection, index lookup, entry assignment,
and removal refer to item UObject instances, not to your ItemId field.
- Do not send
Noneas a meaningful row. Filter invalid references first. - Do not rely on the same UObject appearing twice.
Get Index for Itemexplicitly returns the first match, so duplicate identity makes selection, removal, and entry lookup ambiguous even where an input path accepts it. - If two rows show the same item definition, create two durable item instances
with distinct runtime identity (for example, two inventory stacks), or use
one instance with
Quantity = 2according to the inventory model.
Use a stable gameplay ID inside each item for save/replication/reconciliation, while the local list uses the UObject reference for its runtime identity.
The entry lifecycle¶
The important lifecycle is assignment, not widget construction:
pool creates WBP_ItemEntry once
-> Widget Construct may run
-> list assigns Item_A
-> On List Item Object Set(Item_A)
-> entry paints Item_A and binds Item_A.OnChanged
entry scrolls out / item removed
-> On Entry Released
-> entry unbinds Item_A.OnChanged and clears CachedItem
same widget is reused
-> On List Item Object Set(Item_B)
-> entry paints Item_B from scratch
On List Item Object Set¶
This event is the entry's reliable payload handoff. A robust implementation:
- unbinds from any old cached item (safe even after a clean release);
- clears old transient state;
- Casts the supplied object to the expected item-model class;
- stores that typed reference;
- writes every label, icon, enabled state, visibility, tooltip, and accessibility value from the new item;
- binds once to the item's change notification; and
- applies selection/focus visuals from list state, not from a stale Boolean left on the widget.
If the Cast fails, render a deliberate safe fallback and log enough context to find the wrong item class. Do not leave the previous item's icon/text visible.
Generated, initialized, and released¶
The ListView exposes list-level generated/initialized events for code that needs to observe a row becoming active. The entry interface exposes the item-specific assignment and release events.
Use them at the right altitude:
- On List Item Object Set — entry paints/binds the assigned model.
- On Entry Initialized / generated — list owner performs rare coordination that genuinely needs both the item and active entry.
- On Entry Released — entry unbinds/clears everything tied to the old item.
- Widget Construct / Destruct — widget-instance lifetime, not row identity.
Do not bind the same item dispatcher in both Initialized and Object Set. A pooled entry can then double-bind and react twice.
Request Refresh: what it will and will not update¶
Request Refresh marks the list for work on the next tick. Its documented
scope is narrow: reconcile discrepancies between items and entries.
It handles situations such as:
- an item was added or removed from the source;
- a different item becomes visible after scrolling; or
- an active entry no longer has a corresponding item and must be released.
It does not inspect arbitrary properties inside every existing item:
Item_42.Quantity changes from 1 to 2
-> Item_42 is still the same item in the same list
-> there is no item↔entry discrepancy
-> Request Refresh alone does not promise a new "2" label
This is intentional. Polling every property on thousands of items would defeat virtualization.
The event-driven update pattern¶
Give the item model a dispatcher such as:
ItemModel.OnPresentationChanged
Inventory/model owner changes Quantity
-> update ItemModel.Quantity
-> ItemModel broadcasts OnPresentationChanged
visible entry receives the event
-> verify CachedItem is still valid
-> repaint from CachedItem
Off-screen items have no active entry to update. When one becomes visible,
On List Item Object Set paints its latest state.
The entry must unbind on release and before reassignment. Otherwise a recycled widget can keep reacting to Item_A while displaying Item_B.
Regenerate All Entries: the heavier fallback¶
Regenerate All Entries forces a full release-and-regenerate pass for the
current entries. Epic's contract is specific:
- entry widgets receive On Entry Released;
- object-list entries receive On List Item Object Set again;
- pooled widget instances are not destroyed; and
- Widget Destruct/Construct are therefore not the refresh signal.
This can repaint changed fields when adding a model notification system would be disproportionate for a small list. It is heavier than notifying only the affected visible entry, so do not call it every frame or after every keystroke in a large list.
Rule of thumb:
| Situation | Use |
|---|---|
| Membership/order changed | Set/Add/Remove/Clear; their list change leads to entry reconciliation |
| Existing item field changed and entries subscribe to it | Item dispatcher/field notification; no global rebuild |
| A few small lists need a simple one-shot repaint | Regenerate All Entries |
| You only need to reconcile pending item↔entry membership | Request Refresh |
Selection, focus, and scroll position¶
Selection belongs to item identity, not to the pooled entry. Ask the ListView for Selected Item(s), and handle On Item Selection Changed with the item object. Do not store "Selected" as authoritative state only inside the entry.
When replacing the whole source:
- selection cannot remain on an item reference that is no longer present;
- a replacement object with the same gameplay ID is still a different runtime item reference;
- sort/filter/replacement can move indices; and
- regeneration is not a guarantee that a remembered entry widget or index still represents the same item.
If preserving state matters, capture a stable gameplay ID and any needed scroll target before replacement, map that ID to the new item instance afterward, then explicitly set selection and request scroll into view. Do not promise an exact pixel scroll position across a different item order or different entry sizes.
For keyboard/gamepad use, keep focus on the ListView/entry path and restore it to a valid selected item when the old item disappears. The per-user focus rules are covered in Set Keyboard Focus / Set User Focus / Set Is Enabled.
When the list appears to fail¶
| Symptom | Likely cause | Fix |
|---|---|---|
| No rows appear | The ListView target is missing, item Array is empty/invalid, Entry Widget Class is unset, or the entry does not implement User Object List Entry | Validate target/items and configure a compatible entry class |
| Rows show the previous item's data | On List Item Object Set updates only some fields, or release/reassignment cleanup is missing | Repaint every field and clear stale state before assignment |
| Quantity changed but visible text did not | Existing item fields changed without notifying the entry | Broadcast from the item or regenerate deliberately |
| One row reacts multiple times | The pooled entry rebound without unbinding | Unbind old item on release and before every assignment |
| Selection points at the wrong row after rebuild | Code stored an index/entry widget instead of stable model identity | Reconcile by gameplay ID, then set the new item selection |
| Works for the first screen but breaks after scrolling | Construct was treated as the row setup event | Move item-specific setup to On List Item Object Set |
| Accessed None | A call targeted a missing ListView/item, or a released entry kept using its old model | Validate owner references and clear CachedItem on release |
The Set/Add/Remove/Clear/Refresh nodes do not run a Failed branch. Your gameplay/model layer must report rejected mutations; the UI layer should show the authoritative result.
Multiplayer: keep authority out of the entries¶
UMG exists locally. List item objects and entry widgets are not automatically replicated merely because the inventory Actor or Component replicates.
Use this boundary:
server InventoryComponent validates Add/Remove/Equip
-> authoritative inventory state replicates
-> owning client receives the new snapshot/change
-> local UI model reconciles item UObjects by stable ItemId
-> ListView source/visible entries update
entry button clicked
-> local UI sends an intent/request
-> server validates
-> replicated result drives the next UI state
Do not let an entry decrement server inventory truth optimistically and then hope a refresh repairs it. Local hover, focus, selection, animation, and formatting can remain presentation state; accepted inventory mutations remain with the authoritative gameplay owner.
Lookalikes — which one do I want?¶
| Tool | Use when | Main trap |
|---|---|---|
| ListView | A vertical/horizontal virtualized list of UObject item models | Item is not entry |
| TileView | The same item/entry model arranged as virtualized tiles | Tile layout does not change lifecycle |
| ScrollBox + manually created child widgets | The collection is small and every child genuinely needs to exist | No virtualization; you own creation/removal and cost |
| Uniform Grid / Wrap Box | Small fixed presentation without ListView selection/model machinery | Manual child lifecycle |
| Request Refresh | Item↔entry membership/visibility needs deferred reconciliation | Does not inspect fields inside existing items |
| Regenerate All Entries | A small list needs a full one-shot reassignment/repaint | Heavier and still does not Construct/Destruct pooled widgets |
| Item dispatcher / field notification | Existing model fields change repeatedly | Requires clean bind/unbind lifecycle |
The broader inventory architecture, manual-slot choice, and drag/drop request boundary live in UMG Inventory Slots, ListView/TileView, and Drag/Drop.
Going deeper¶
- Construct Object from Class — create local item-model UObjects with a truthful Outer and stored reference.
- Small Inventory Architecture — definitions, runtime entries, equipped Actors, UI, and SaveGame boundaries.
- Add, Remove, Stack, and Broadcast Inventory Changes — accepted-count results and event-driven UI refresh.
- Updating a Widget: Bindings vs Events — push changes instead of hiding gameplay work in UI polling.
- Engine source (requires engine access; we do not reproduce it):
UListViewBase::RequestRefreshandRegenerateAllEntriesinEngine/Source/Runtime/UMG/Private/Components/ListViewBase.cpp;UListView::HandleOnEntryInitializedInternalandOnItemsChangedinEngine/Source/Runtime/UMG/Private/Components/ListView.cpp;ITypedUMGListView::SetListItemsinEngine/Source/Runtime/UMG/Public/Components/TypedUMGListView.h; andIUserObjectListEntry::SetListItemObjectandIUserObjectListEntry::OnListItemObjectSetinEngine/Source/Runtime/UMG/Public/Blueprint/IUserObjectListEntry.h; andIUserListEntry::ReleaseEntryinEngine/Source/Runtime/UMG/Public/Blueprint/IUserListEntry.h, with its implementation inEngine/Source/Runtime/UMG/Private/Blueprint/IUserListEntry.cpp.