dynamic children¶
this article assumes you have read arranging children: a container's Children = { ... } list, and the flex properties that lay it out.
that literal list works when you know the children at author time. when the children come from data, a list whose length and contents change at runtime, you build them in a loop instead.
the fixed list, and its limit¶
the collection-initializer form lists children literally:
new Container
{
Children =
{
new Text( "members" ),
new Text( "alice" ),
},
}
mixing init properties with bare loop code in one brace block is a compile error (CS0747), so a raw foreach cannot live inside that list. two forms handle a data-driven list instead: Each.Of runs the loop inline, inside the initializer; Children.AddRange runs it after construction, on a container you already hold a reference to.
Each.Of: inline, in the initializer¶
Each.Of is a loop element you drop directly into the brace list, alongside literal children:
static readonly string[] Names = { "alice", "bob", "carol" };
new Container
{
FlexDirection = FlexDirection.Column,
Gap = 8,
Children =
{
new Text( "members" ),
Each.Of( Names, ( i, name ) => new Text( name ) ),
},
}
it runs the builder once per item and auto-keys each result the same way AddRange does, covered next. reach for it when the loop is the only dynamic part of an otherwise-literal list; reach for AddRange once the container is built separately from its children, which is the more common shape once a builder gets non-trivial.
AddRange: one child per item¶
build the container first, then call AddRange on its Children. it takes your list and a builder function, runs the builder once per item with the index and the item, and adds each returned blob as a child. the builder can return any blob kind, not just Container: a list of Text rows or Polygon markers works the same way.
static readonly string[] Names = { "alice", "bob", "carol" };
protected override Container Build()
{
var list = new Container { FlexDirection = FlexDirection.Column, Gap = 8 };
list.Children.AddRange( Names, ( i, name ) => new Container
{
Padding = 8,
Children = { new Text( name ) },
} );
return list;
}
for a small item, an inline lambda like this is fine. when the item needs its own layout, animation, or state, move the construction into a static helper and call that from the lambda instead.
delegating to a static builder¶
the keystroke visualizer (Code/Demos/KeystrokeVisualizerUI.cs) does this to keep Build() readable. it has a column container and a queue of entries, and passes a helper delegate:
column.Children.AddRange( _queue, ( i, e ) =>
EntryChip.Build( i, e.Label, _now - e.SpawnTime, _now - e.LastInputTime, anim ) );
EntryChip.Build takes the index and the entry data and returns a fully configured Container. the caller never sees the internals of a chip. the pattern scales to any item that is complex enough to deserve its own type. composition covers this presenter shape in full.
keys and identity¶
goo matches children across rebuilds by their Key. when a child has no key, identity falls back to position, which is fragile: insert or remove an item in the middle of the list and the panels below it shift identity, which can restart an animation or move focus to the wrong row.
AddRange and Each.Of both handle the common case for you. when your builder returns a container with no Key, they assign an index key (_idx:0, _idx:1, and so on), which is enough for a list you only ever append to or truncate.
for a list that reorders, or where items are inserted and removed in the middle, give each child a stable key drawn from the data. a key you set yourself wins over the index default, so identity follows the item rather than its slot. both AddRange and Each.Of take an optional keyOf argument for this:
list.Children.AddRange( people, p => p.Id, ( i, p ) => new Container
{
Children = { new Text( p.Name ) },
} );
a key set directly on the built blob wins over keyOf too:
list.Children.AddRange( people, ( i, p ) => new Container
{
Key = p.Id, // stable identity, survives reorder
Children = { new Text( p.Name ) },
} );
stable also means unique. draw the key from an id, never from a display value that can repeat: two rows both keyed row-ruby give the reconciler one identity for two children, so it warns and falls back to positional matching for that list, the same degraded mode as mixing keyed and unkeyed siblings. if your data has no natural id, mint one when the item enters the list (a counter field is enough) and carry it with the item.
a child only when a condition holds¶
a nullable child skips silently, so a conditional child is a plain ternary, inline in the initializer:
Children =
{
ListBody(),
_showHint ? HintBanner() : null,
},
only the taken branch is evaluated: HintBanner() never runs when _showHint is false. after construction, the same thing is an ordinary if:
if ( _showHint ) list.Children.Add( HintBanner() );
see also¶
- arranging children - the literal
Children = { ... }list, and the flex properties that lay it out. - composition - delegating to a static builder like
EntryChip.Build, in full. - cells - keyed child lists are the environment most cells are mounted into.
- container reference - the container each builder returns.
- build method - when
Build()re-runs and the child list is rematched.