Binding Templates
This page describes the ready-made templates your DataBinding inherits from. Each template automatically implements UI loading, add/remove event handling, and data synchronization — you only need to define a few primitive methods.
For a general overview of DataBinding and its role in the pipeline, see Data Binding.
Which Template To Choose
flowchart TD
A["What data structure\nbacks your inventory?"] --> B{"Dynamic list?"}
B -->|Yes| C["ListInventoryDataBinding\nBackpack, chest, loot"]
B -->|No| D{"Slots with numeric index?"}
D -->|Yes| E["SlotIndexedInventoryDataBinding\nHotbar, slot array"]
D -->|No| F{"Need to persist\nshape/anchor/orientation?"}
F -->|Yes| G["PlacementInventoryDataBinding\nShaped items, grid"]
F -->|No| H["MappedSlotInventoryDataBinding\nEquipment, named slots"]
| Template | Data structure | Sync pattern | When to use |
|---|---|---|---|
ListInventoryDataBinding |
Dynamic list | Per-adapter (each adapter separately) | Backpack, chest, loot, merchant |
SlotIndexedInventoryDataBinding |
Array/dict by index | Per-slot, with adapter list | Hotbar, equipment slot array |
MappedSlotInventoryDataBinding |
Named properties | Per-slot + per-slot validation | Character equipment (head, body, weapon) |
PlacementInventoryDataBinding |
Placements with anchor/orientation | Per-placement, with every item instance | Shaped items, grid inventories |
ListInventoryDataBinding
For inventories where data is stored as a list: List<T>, array, database collection.
What to implement
public class BackpackBinding : ListInventoryDataBinding<ItemSO, ItemSOAdapter>
{
[SerializeField] private List<ItemSO> _items;
// 1. Where to read data from when loading UI
protected override IReadOnlyList<ItemSO> GetItems() => _items;
// 2. How to create an adapter from a data element
protected override ItemSOAdapter CreateAdapter(ItemSO item) => new(item);
// 3. How to add to data (called for EACH adapter in the stack)
protected override void AddToData(ItemSOAdapter adapter) => _items.Add(adapter.Data);
// 4. How to remove from data (called for EACH adapter in the stack)
protected override void RemoveFromData(ItemSOAdapter adapter) => _items.Remove(adapter.Data);
}
How it works automatically
On load (ReloadUI): iterates GetItems(), creates an adapter for each element, adds to UI.
On item added (drag & drop): iterates all adapters in the stack and calls AddToData for each:
On item removed: same pattern, RemoveFromData for each adapter.
Why per-adapter?
Each adapter in a stack can hold unique runtime data (serial number, purchase timestamp). Per-adapter processing guarantees the exact instances are added to and removed from your data.
SlotIndexedInventoryDataBinding
For inventories where data is tied to slots by numeric index: fixed-size array, int → Item dictionary.
What to implement
public class HotbarBinding : SlotIndexedInventoryDataBinding<ItemSO, ItemSOAdapter>
{
[SerializeField] private ItemSO[] _slots = new ItemSO[8];
// 1. Which slots are occupied (skip empties)
protected override IEnumerable<(int index, IReadOnlyList<ItemSO> items)> GetOccupiedSlots()
{
for (int i = 0; i < _slots.Length; i++)
if (_slots[i] != null)
yield return (i, new[] { _slots[i] });
}
// 2. How to create an adapter from a data element
protected override ItemSOAdapter CreateAdapter(ItemSO item) => new(item);
// 3. How to write data to a slot (full adapter list for the stack)
protected override void AddToSlotData(int index, IReadOnlyList<ItemSOAdapter> adapters)
=> _slots[index] = adapters[0].Data;
// 4. How to clear slot data (full adapter list for the stack)
protected override void RemoveFromSlotData(int index, IReadOnlyList<ItemSOAdapter> adapters)
=> _slots[index] = null;
}
How it works automatically
On load: iterates GetOccupiedSlots(), creates an adapter for every item in items, builds a stack, and adds it to UI at the slot index.
On add/remove: called once for the entire stack and passes all typed adapters from that stack:
This preserves per-instance data in a stack. If three items look the same but carry different runtime data, the binding receives all three adapters.
Difference from List template
| List | SlotIndexed | |
|---|---|---|
| Sync | Per adapter | Single call per slot with adapter list |
| Slot identity | None | Numeric index |
| Size | Dynamic | Usually fixed |
MappedSlotInventoryDataBinding
For inventories where each slot is a separate named property with its own read, write, and validation logic. Supports both single items and stacks — both slot types can be mixed in the same CreateBindingMap().
What to implement
public class EquipmentBinding : MappedSlotInventoryDataBinding<ItemModel, ItemModelAdapter>
{
[SerializeField] private UniversalSlot _weaponSlot;
[SerializeField] private UniversalSlot _armorSlot;
[SerializeField] private UniversalSlot _potionSlot;
[SerializeField] private CharacterData _data;
// 1. Declarative map: slot → how to read, write, clear, validate
protected override Dictionary<BaseSlot, SlotBinding<ItemModel, ItemModelAdapter>> CreateBindingMap() => new()
{
// Single item (simple constructor)
[_weaponSlot] = new(
get: () => _data.Weapon,
set: adapter => _data.Weapon = adapter.Model,
clear: () => _data.Weapon = null,
canDrop: adapter => adapter.Model.Type == ItemType.Weapon
? RuleResult.Success()
: RuleResult.Failure("Weapons only")),
[_armorSlot] = new(
get: () => _data.Armor,
set: adapter => _data.Armor = adapter.Model,
clear: () => _data.Armor = null),
// Stacking slot (list constructor)
[_potionSlot] = new(
getAll: () => _data.Potions,
add: adapters => _data.AddPotions(adapters),
remove: adapters => _data.RemovePotions(adapters),
clear: () => _data.ClearPotions(),
canDrop: adapter => adapter.Model.Type == ItemType.Potion
? RuleResult.Success()
: RuleResult.Failure("Potions only")),
};
// 2. How to create an adapter from a data element (for ReloadUI)
protected override ItemModelAdapter CreateAdapter(ItemModel item) => new(item);
}
How it works automatically
On load: iterates all entries in BindingMap, calls GetAll() for each slot. For each item in the list, creates a separate adapter and assembles them into an ItemStack via ItemStack.TryCreate().
On item added: filters adapters of type TAdapter from the stack, finds the binding for the target slot, calls Add(adapter_list).
On item removed: similarly filters adapters, finds the binding for the source slot, calls Remove(adapter_list).
On CanDrop/CanStartDrag check: automatically calls CanDrop(adapter) / CanStartDrag(adapter) for the target/source slot using PrimaryAdapter, if a validator is provided.
Two SlotBinding constructors
Simple — for single items (one item per slot):
new SlotBinding<TData, TAdapter>(
get: () => ..., // read current value (TData)
set: adapter => ..., // write via adapter (TAdapter)
clear: () => ..., // clear
canDrop: adapter => ... // optional: validation on drop (TAdapter)
)
Internally get/set/clear are wrapped into the list-based API: GetAll returns a single-element array, Add calls set(adapters[0]), Remove calls clear().
Stacking — for multiple identical items in a slot:
new SlotBinding<TData, TAdapter>(
getAll: () => ..., // all items in the slot (IReadOnlyList<TData>)
add: adapters => ..., // add adapters (IReadOnlyList<TAdapter>)
remove: adapters => ..., // remove adapters (IReadOnlyList<TAdapter>)
clear: () => ..., // full clear
canDrop: adapter => ... // optional: validation on drop (TAdapter)
)
add/remove receive the concrete adapter instances from the stack. Like ListInventoryDataBinding, this lets you work with individual instance data without an intermediate ExtractData step.
Both types in one BindingMap
Both constructors produce the same SlotBinding<TData, TAdapter>. They can be freely mixed in a single dictionary — the base class always works through the unified list-based API.
canDrop and canStartDrag are optional in both constructors. If not set, the slot accepts anything that passes other rules.
Difference from SlotIndexed template
| SlotIndexed | MappedSlot | |
|---|---|---|
| Slot identity | Numeric index | BaseSlot object reference |
| Slot data | Same pattern for all | Individual get/set/clear per slot |
| Validation | Shared via CanDrop override |
Individual canDrop / canStartDrag per slot |
| Stacks | Via slot adapter list | Via list-based API (each adapter individually) |
| Slot count | Can be many | Usually < 10 |
PlacementInventoryDataBinding
For inventories that need to persist not only the slot, but also item placement: anchor, orientation, covered cells, and item shape.
This is usually used for shaped items, grid inventories, and any system where an item can occupy several cells.
What to implement
public class ShapedItemsBinding
: PlacementInventoryDataBinding<ItemModel, ItemModelAdapter>
{
[SerializeField] private List<MyPlacementModel> _placements;
protected override IEnumerable<PlacementData<ItemModel>> GetPlacements()
{
foreach (var placement in _placements)
yield return new PlacementData<ItemModel>(
placement.Items,
placement.AnchorIndex,
placement.Orientation);
}
protected override ItemModelAdapter CreateAdapter(ItemModel item) => new(item);
protected override ItemModel ExtractData(ItemModelAdapter adapter) => adapter.Model;
protected override void AddPlacementData(
PlacementCommitContext<ItemModel, ItemModelAdapter> context)
{
_placements.Add(new MyPlacementModel(
context.Data,
context.AnchorIndex,
context.Orientation));
}
protected override void RemovePlacementData(
PlacementCommitContext<ItemModel, ItemModelAdapter> context)
{
RemovePlacementAt(context.AnchorIndex);
}
}
What changed in the current API
PlacementData<TData> now stores Items, the full list of item instances in the placement.
PlacementCommitContext<TData, TAdapter> now exposes:
Data— data for every adapter in the transferred stackAdapters— all typed adapters from the transferred stackPrimaryData/Adapter— the first item, only as a convenience for single-item cases
Use context.Data when a stack can contain different instances. Use PrimaryData only when the slot really stores one item or your items are fully homogeneous.
Base class: InventoryDataBindingBase
All templates inherit from InventoryDataBindingBase. You normally don't inherit from it directly, but it's useful to know which virtual methods are available for override:
| Method | Default | When to override |
|---|---|---|
CanStartDrag(context, entry) |
Success |
Block taking items from this inventory |
CanDrop(context, entry) |
Success |
Add drop checks (MappedSlot overrides this automatically) |
CanSwap(args) |
Success |
Extra swap validation |
OnSwapCompleted(args) |
No-op | React to completed swap |
OnDropCompletedFrom(context) |
No-op | React after a drop completes where this inventory was the source |
OnDropCompletedTo(context) |
No-op | React after a drop completes where this inventory was the target |
CreateItemConverter() |
null |
Item conversion between inventories with different formats |
Occupied-slot drop handling is no longer a virtual method on InventoryDataBindingBase. Implement IPreRuleOccupiedSlotDropHandler or IPostRuleOccupiedSlotDropHandler on your binding instead. See Optional Interfaces.
Helper methods are also available:
ReloadUI()— full resync of UI with dataClearUI()— clear all slotsAddToUIQuiet(adapter, count, slotIndex)— add item without generating eventsBeginSync()— start sync scope (suppresses events, prevents feedback loops)
Deferred Processing: OnDropCompletedFrom / OnDropCompletedTo
OnItemAddedToUI and OnItemRemovedFromUI are called per allocation during a transfer. If a transfer splits across multiple target slots (batch transfer), you'll receive multiple calls for a single drag operation.
When this is a problem, use OnDropCompletedFrom / OnDropCompletedTo. They are called once after the entire drop operation completes.
Example: Craft Result Slot
Recipe 3×A → 6×B. Slot contains 384×B. Player drags into an inventory with max stack 64.
- The transfer spreads across 6 slots:
64+64+64+64+64+64 = 384 OnItemRemovedFromUIfires 6 times withCount = 6464 / 6 = 10(integer division) — remainder lost on each call
Solution — accumulate removed items and consume ingredients once:
public class CraftResultDataBinding : InventoryDataBindingBase
{
private int _pendingRemovedItems;
protected override void OnItemRemovedFromUI(InventoryItemEventContext context)
{
// Only accumulate, don't consume yet
_pendingRemovedItems += context.Stack.Count;
}
protected override void OnDropCompletedFrom(DragContext context)
{
// Called once after the entire transfer
int craftsConsumed = _pendingRemovedItems / resultCount;
_pendingRemovedItems = 0;
manager.ConsumeCraftIngredients(craftsConsumed);
}
}
DragAmountStep
The transfer respects the source inventory's DragAmountStep: the total number of transferred items is rounded down to a multiple of the step. This guarantees that the sum of all placements is divisible by the step, even if individual placements are not.