Dynamic Layouts
DynamicLayoutNode evaluates a factory function once on first render, then re-evaluates only when Invalidate() is called. For content that switches based on a key (enum, step index, tab), use KeyedDynamicLayoutNode<TKey> which caches content by key and preserves child state across key changes.
When to Use
| Scenario | Use |
|---|---|
| Content switches based on a key (enum, int, string) | Layouts.KeyedDynamic<TKey>() — caches by key, preserves state |
| Observable-driven content updates | ReactiveProperty<T> + .AsLayout() or factory.AsDynamicLayout(trigger) |
| Advanced: custom factory with manual caching | Layouts.Dynamic() — low-level, you manage caching |
Basic Usage
KeyedDynamic (Recommended)
The natural way to write content that switches based on a key. No manual caching needed — child state (highlights, typed text, focus) is preserved across key changes:
public override ILayoutNode BuildLayout()
{
var currentTab = 0;
var layout = Layouts.KeyedDynamic(
() => currentTab,
tab => tab switch
{
0 => new TextNode("Welcome home"),
1 => new TextNode("Settings panel"),
_ => new TextNode("Unknown tab")
});
// Change tab and invalidate:
currentTab = 1;
layout.Invalidate();
return layout;
}Dynamic (Low-Level)
For cases where you need full control over the factory:
var dynamicNode = Layouts.Dynamic(() => BuildCurrentContent());
// ... later, when state changes:
dynamicNode.Invalidate();Choosing the Right Dynamic Node
| Need | Use |
|---|---|
| Content switches based on a key (enum, int, string) | Layouts.KeyedDynamic<TKey>() — caches by key, preserves state |
| Observable-driven content updates | observable.AsLayout() or factory.AsDynamicLayout(trigger) |
| Advanced: custom factory with manual caching | Layouts.Dynamic() — low-level, you manage caching |
With Invalidation Trigger
When your factory depends on state that changes over time, use Invalidate() or the AsDynamicLayout extension to trigger re-evaluation:
// Manual invalidation
var dynamicNode = Layouts.Dynamic(() => BuildCurrentContent());
// ... later, when state changes:
dynamicNode.Invalidate();
// Or subscribe an observable trigger
Func<ILayoutNode> factory = () => BuildCurrentContent();
var node = factory.AsDynamicLayout(someObservable.Select(_ => Unit.Default));Before / After Comparison
Before (manual Subject<Unit> merge trick):
// ViewModel
public ReactiveProperty<int> CurrentTab { get; } = new(0);
// Page
ViewModel.CurrentTab
.Select<int, ILayoutNode>(tab => tab switch
{
0 => BuildHomeTab(),
1 => BuildSettingsTab(),
_ => new EmptyNode()
})
.AsLayout()After (one-liner with Layouts.KeyedDynamic):
// No ViewModel property needed — page-local state
var tab = 0;
var layout = Layouts.KeyedDynamic(
() => tab,
t => t switch
{
0 => BuildHomeTab(),
1 => BuildSettingsTab(),
_ => new EmptyNode()
});
// Call layout.Invalidate() when tab changesHow It Works
- The factory is called once on first
Measure()/Render(), then only whenInvalidate()is called Invalidate()eagerly evaluates the factory so the new child is immediately available for tree traversal (e.g., focus propagation)- Reference equality detects child changes — returning the same instance avoids lifecycle transitions
- When the child changes: the old child is deactivated, the new child is activated (mirrors
ReactiveLayoutNodebehavior) KeyedDynamicLayoutNode<TKey>additionally caches content by key — navigating back to a previous key reuses the cached instanceGetChildNodes()returns the current child for focus tree traversal- Extends
LayoutNodeso fluent sizing (.Fill(),.Width(),.Height()) works
Fluent Sizing
Layouts.KeyedDynamic(() => currentTab, tab => BuildTab(tab))
.Fill() // Fill remaining height
.WidthFill() // Fill available widthTIP
For page-level state that doesn't need to be in the ViewModel, Layouts.KeyedDynamic() avoids the overhead of creating a ReactiveProperty<T> and observable pipeline. For state that should be in the ViewModel, use Reactive Properties instead.