Template, Items, and Visual-Tree Migration
This guide covers the breaking visual and collection changes in the template-first release. The normative language rules remain in xaml-language.md; the complete type-by-type classification is in control-template-migration-manifest.md.
Choose the Owning Type
Control remains the base styleable visual/layout node. Use a foundational element when the type's
job is to render, arrange, or project content directly:
- primitives:
Border, text, image, shape, drawing, and effect elements; - panels:
CanvasPanel,OverlayPanel,StackPanel,WrapPanel,FlexPanel,GridPanel, andViewbox; - presenters:
ContentPresenter,ItemsPresenter, andScrollPresenter.
These types do not have templates. A semantic widget derives from TemplatedControl, retains its
state, input, focus, selection, and accessibility behavior, and gets all replaceable chrome from a
ControlTemplate. Do not move widget behavior into a Border, presenter, or template code-behind.
Replace Custom-Drawn Chrome
Move outer chrome into a typed template and leave interaction on the semantic owner:
<ControlTemplate x:Key="ToolbarButtonTemplate" TargetType="Button">
<Border Classes="chrome" Padding="10,6" CornerRadius="4">
<ContentPresenter x:Name="PART_ContentPresenter"
Content="{Binding Content, RelativeSource=TemplatedParent}"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center" />
</Border>
</ControlTemplate>
<Button Template="{StaticResource ToolbarButtonTemplate}" Content="Save" />
Use the documented named parts and minimum required part types. GetTemplateChild replaces casts or
child-index assumptions about a widget's former internal visual tree. Different templates may have
unrelated structure, so application code must not traverse from a semantic owner by fixed child
positions.
Selectors use the visual tree. Ordinary descendant and child combinators stop at a template or data
-template boundary. Cross exactly one control-template boundary with >>:
<Style Selector="Button.primary >> Border.chrome" />
<Style Selector="ListBoxItem:selected >> Border.selection" />
Logical content remains owned by its semantic control even while a presenter hosts it visually.
Use Parent for logical ownership, VisualParent for rendered ancestry, and a typed
RelativeSource FindAncestor binding when the relationship is visual.
Replace Row Factories
A data item now requires an explicit DataTemplate; there is no implicit item-type lookup or
runtime DataTemplateSelector:
<DataTemplate x:Key="ServerRowTemplate" x:DataType="local:ServerRowModel">
<local:ServerRow />
</DataTemplate>
<ItemsControl ItemsSource="{Binding Servers}"
ItemTemplate="{StaticResource ServerRowTemplate}" />
Convert a C# row factory that only assembled visuals into a keyed or inline DataTemplate. Keep an
ordinary separate x:Class row only when row-local event handling or recyclable local state is
required. Event attributes are not legal directly inside DataTemplate; handlers on a referenced
row control resolve against that row's code-behind, not the outer view.
Observable sources use one identity slot per occurrence. Duplicate object references are distinct. Add/remove/move notifications preserve unaffected slot identities, replacement creates a new slot, and reset rebuilds. Notifications must arrive on the attached UI thread.
Rows are poolable when their template state can be deactivated, rebound, and activated. A custom
row with code-behind opts in through IDataTemplateRecyclingState; a custom item container uses
IRecyclableItemContainer. Otherwise it remains correct but is not pooled.
Adopt Virtualization Deliberately
Select an explicit items panel:
VirtualizingStackPanelsupports vertical and horizontal variable item extents;VirtualizingGridPanelsupports uniform fixed or estimated cells;- non-virtualizing panels realize every source slot.
Virtualized realization is bounded by the visible range, overscan, and pinned focus/capture/edit
interactions. Set a positive estimated item extent when item sizes are not yet known. Preserve an
application identity across reset/source replacement with ItemKeySelector; otherwise Forma clamps
the prior raw offset.
DataGrid virtualizes rows and recycles all cells in a row together. Columns are explicit and are
not virtualized; at most DataGrid.MaximumSupportedVisibleColumns (256) may be visible. Keep wide,
unbounded spreadsheet scenarios outside this release's contract.
Migrate Tables and Trees
Use DataGridMode.Flat for tables and DataGridMode.Hierarchical for expandable tree tables.
Declare DataGridTextColumn, DataGridCheckBoxColumn, DataGridTemplateColumn, or
DataGridExpanderColumn explicitly with typed bindings. Forma does not reflect item properties or
auto-generate columns.
Sorting requires a typed SortBinding or comparer. Filtering belongs to DataGridSource<T> (or an
application-owned filtered source) and runs only when RefreshFilter is called. Hierarchical
sources declare typed children and optional has-children/expanded accessors; ancestor-preserving
filtering may inspect the full tree during refresh. Warm frames do not sort or filter.
Row selection preserves source-occurrence paths. Cell selection uses immutable CellIndex values.
Choose DataGridSelectionUnit.Row or Cell and the inherited single/multi selection mode
explicitly. Expansion, sorting, filtering, selection, and scroll anchors remain semantic owner
state when a control template changes.
The retained Tree remains available for compatibility. Prefer DataGrid for new typed,
data-bound, template-first flat or hierarchical tables.
Package and Release Boundary
Reference exactly one runtime peer and its private build package. Add the matching
Forma.Xaml.HotReload package only to a Debug development host. Core templates, selectors, items,
selection, DataGrid, static-font text, and virtualization do not require DynamicText or hot
reload.
Release, trimmed, and NativeAOT output uses injected typed IL only. Do not ship source XAML, the
compiler, XamlX, Cecil, SRE, or the hot-reload package. Add Forma.DynamicText only when runtime font
loading, shaping, or rasterization is required; SpriteFontAdapter remains the core-only path.
Verification Checklist
- Replace fixed child traversal with namescope or named-part lookup.
- Verify required parts after every explicit/theme template replacement.
- Test pointer, keyboard, focus, selection, and accessibility behavior independently of template structure.
- Test observable add/remove/move/replace/reset with duplicate occurrences.
- Assert realized, recycled, and pinned counts under large-source scrolling.
- Build Debug and Release for MonoGame and FNA; confirm Release excludes hot reload and source XAML.
- Publish the matching core-only trim/NativeAOT consumer before adding optional companions.