This document specifies a uniform, idiomatic, and extensible data templating model for XenoAtom.Terminal.UI.
The goal is to make it easy to:
This spec borrows proven ideas from WPF/Avalonia/SwiftUI (DataTemplates, content presenters, item containers), but adapts them to a retained-mode terminal UI with:
Visual.GetStyle<T>() / Visual.SetStyle<T>(...) / visual.Style(...)).Visual" mapping.State<T>/Binding<T> and update without manual RequestRender().DataPresenter<T>) to avoid boxing.Visual subtree (and optionally updates an existing subtree).Display: render a value for viewing.Editor: render a value for editing (typically requires a bindable source, e.g. State<T>/Binding<T>).DataTemplateRolepublic enum DataTemplateRole
{
Display = 0,
Editor = 1,
}
Notes:
Display by default.Editor.DataTemplateContextDataTemplateContext provides metadata to templates without forcing every control to invent its own signature.
public readonly record struct DataTemplateContext(
Visual Owner,
DataTemplateRole Role,
int Index,
DataTemplateItemState State);
[Flags]
public enum DataTemplateItemState
{
None = 0,
Selected = 1 << 0,
Hovered = 1 << 1,
Focused = 1 << 2,
Disabled = 1 << 3,
}
Guidance:
State; selection/hover rendering should primarily be the responsibility of the owning control
(similar to WPF’s ItemContainerStyle).Index MUST be -1 when the template is not item-based (e.g. a single DataPresenter<T>).DataTemplate<T> (recyclable contract)The core template representation is a struct so template slots are not themselves delegates (avoids method-resolution conflicts and keeps bindable properties simple). Internally it can still wrap delegates.
public readonly record struct DataTemplateValue<T>
{
public T GetValue();
}
public delegate Visual DataTemplateDisplayFactory<T>(DataTemplateValue<T> value, in DataTemplateContext context);
public delegate Visual DataTemplateEditorFactory<T>(Binding<T> binding, in DataTemplateContext context);
public delegate bool DataTemplateUpdater<T>(Visual visual, DataTemplateValue<T> value, in DataTemplateContext context);
public delegate void DataTemplateReleaser(Visual visual);
public readonly record struct DataTemplate<T>(
DataTemplateDisplayFactory<T>? Display,
DataTemplateEditorFactory<T>? Editor,
DataTemplateUpdater<T>? TryUpdate = null,
DataTemplateReleaser? Release = null)
{
public bool IsEmpty => Display is null && Editor is null;
}
Semantics:
Display builds a new Visual for display from a read-only value reference (DataTemplateValue<T>).Editor builds a new Visual for editing from a read/write binding (Binding<T>).TryUpdate enables recycling: update an existing visual instance to represent a different value.
true if the visual was updated successfully.false if the visual cannot be reused for that value (caller should fall back to Display).Release is called when a visual is removed from a recycling pool permanently (optional hook to dispose resources).Normative rules:
Display and Editor (when provided) MUST return a non-null Visual (otherwise throw at call site).TryUpdate MUST NOT attach/detach visuals directly; it should only update bindable properties/state on the provided visual subtree.
(The owning control manages parenting.)DataTemplates (environment-scoped registry)DataTemplates is an environment-scoped registry of templates, resolved via Visual.GetStyle<DataTemplates>().
DataTemplates SHOULD be immutable and replaced via Visual.Set(...) when changed.
To avoid "copy the world to override one entry", DataTemplates supports cheap overrides by chaining:
DataTemplates instance stores only the templates registered in that layer.Parent until a match is found.Parent points to the existing registry.public sealed record DataTemplates : IStyle<DataTemplates>
{
public static DataTemplates Default { get; }
public static StyleKey<DataTemplates> Key { get; }
// Optional: parent for overlay chaining.
public DataTemplates? Parent { get; init; }
// Register only adds/overrides in the current layer.
public DataTemplates Register<T>(DataTemplateRole role, DataTemplate<T> template);
// Resolve prefers the current layer and falls back to Parent.
public bool TryResolve<T>(DataTemplateRole role, out DataTemplate<T> template);
}
Key points:
Get<DataTemplates>() are automatically re-evaluated when the subtree’s DataTemplates value changes via Set.Why immutability works well with bindings:
Get<T>() calls. When you do container.Set(newRegistry), any descendants that read
Get<DataTemplates>() are invalidated and re-evaluated.For usability, the API SHOULD include a builder-style entry point to avoid repeatedly copying internal tables when registering multiple templates at once.
Example:
var templates = DataTemplates.Default.Derive(builder => builder
.Register<string>(
DataTemplateRole.Display,
new DataTemplate<string>(
Display: static (DataTemplateValue<string> v, in DataTemplateContext _) => new TextBlock(() => v.GetValue()),
Editor: null))
.Register<DateTime>(
DataTemplateRole.Display,
new DataTemplate<DateTime>(
Display: static (DataTemplateValue<DateTime> v, in DataTemplateContext _) => new TextBlock(() => v.GetValue().ToString("u")),
Editor: null))
);
root.Set(templates);
Proposed API:
public sealed record DataTemplates : IStyle<DataTemplates>
{
public static DataTemplates Default { get; }
public static StyleKey<DataTemplates> Key { get; }
public DataTemplates? Parent { get; init; }
// Convenience: single registration for simple cases.
public DataTemplates Register<T>(DataTemplateRole role, DataTemplate<T> template);
// Preferred for multiple registrations: the builder mutates a temporary table once.
public DataTemplates Derive(Func<DataTemplatesBuilder, DataTemplatesBuilder> configure);
}
public sealed class DataTemplatesBuilder
{
public DataTemplatesBuilder Register<T>(DataTemplateRole role, DataTemplate<T> template);
}
Notes:
Derive(...) returns a new DataTemplates instance whose Parent defaults to the receiver, so you don't need to pass
Parent = ... manually for common overlay cases.Register(...) can remain as a convenience method, but the documentation SHOULD recommend Derive(...) for any non-trivial
set of registrations.Resolution MUST support:
TryResolve<T>(...) (strongly typed; allocation-free).DataPresenter<T>DataPresenter<T> hosts a single value and resolves a template to render it.
It is generic to avoid boxing for value types and to keep binding paths type-safe.
public sealed class DataPresenter<T> : Visual
{
[Bindable] public partial T Value { get; set; }
[Bindable] public partial DataTemplateRole Role { get; set; }
// Optional override: if non-empty, bypasses registry resolution.
[Bindable] public partial DataTemplate<T> Template { get; set; }
}
Behavior:
Template is non-empty: use it.Get<DataTemplates>().Caching guidance:
Binding<T> and should react via bindings.Controls that render items should expose a template slot:
public sealed partial class Select<T> : Visual
{
[Bindable] public partial DataTemplate<T> ItemTemplate { get; set; }
}
Rules:
default/empty, meaning “use environment templates”.When a control needs a visual for a bindable value binding and role role:
var templates = Get<DataTemplates>();Visual, use it directly (identity).new TextBlock(() => binding.GetValue()?.ToString()).To keep DataPresenter<T> allocation-free:
TryResolve<T>) is sufficient.Notes:
TryResolve<T> is intentionally exact-match only. Because DataTemplate<T> is strongly typed, a template registered for
a base type or interface cannot be returned as DataTemplate<T> without introducing allocations (adapters).If a consumer wants heterogeneous items without boxing, they should use a reference-type base/interface for T.
null SHOULD be treated as a valid input:
null (role-specific), use it.new TextBlock(string.Empty).This section defines how templating supports large data sets with minimal allocations.
Virtualizing controls SHOULD:
template.Display.template.TryUpdate exists and returns true, reuse the visual.template.Release if provided.Notes:
Release is a finalizer-style hook for pooled visuals; it should be treated as “this instance will never be reused again”.For templates to be safely recyclable:
Update(...)) that are registered once and never cleared.State<T>/Binding<T> inside the visual subtree.Recommended pattern for maximum reuse:
State<T> per realized item visual.State<T> (and possibly to selection/hover state).state.Value = newItem.This pattern is a strong differentiator for a binding-driven terminal UI: you get React-like reuse without a diff engine.
Pools SHOULD be bounded (configurable per control style/options) to avoid unbounded memory usage when scrolling through huge lists.
Template resolution MUST be a tracked read:
ItemTemplate), not private fields.DataTemplates registry via Get<DataTemplates>() so changes to the registry can invalidate dependent visuals.Controls should rebuild item visuals when:
Controls should NOT rebuild visuals merely because a State<T> value changes; the visual subtree should update through bindings.
The default theme should ship with templates that make “drop data in UI” productive.
Recommended defaults for DataTemplateRole.Display:
string -> new TextBlock(() => binding.GetValue())string? -> new TextBlock(() => binding.GetValue() ?? string.Empty)bool -> new TextBlock(() => binding.GetValue() ? "True" : "False")new TextBlock(() => binding.GetValue().ToString())Visual -> identity (already a visual)Recommended guidance:
value.GetValue() (for display) or binding.GetValue() (for editors) inside a lambda so changes are tracked automatically.Editor templates should generally exist for types that have built-in editor controls:
string? -> new TextBox().Text(binding)int -> new NumberBox<int>().Value(binding)bool -> new Switch().IsOn(binding) (or CheckBox)This enables a future property grid/forms experience without adding a new framework layer.
new Select<MyModel>()
.Items(models)
.ItemTemplate(new DataTemplate<MyModel>(
Display: (DataTemplateValue<MyModel> value, in DataTemplateContext _) =>
{
var m = value.GetValue();
return new HStack(
new TextBlock(m.Name),
new TextBlock(() => $"#{m.Id}").Style(TextBlockStyle.Muted))
.Spacing(2);
},
Editor: null));
var templates = new DataTemplates { Parent = DataTemplates.Default }
.Register<string>(
DataTemplateRole.Display,
new DataTemplate<string>(
Display: static (DataTemplateValue<string> value, in DataTemplateContext _) => new TextBlock(() => $"> {value.GetValue()}"),
Editor: null));
new VStack(
new Select<string>().Items(["One", "Two", "Three"]),
new ListBox<string>().Items(["A", "B", "C"])
)
.Set(templates);
DataPresenter<T> for “just show this value”var name = new State<string?>("Alex");
new VStack(
name.PresentAs(DataTemplateRole.Display),
name.PresentAs(DataTemplateRole.Editor)
).Spacing(1);
This repository is pre-1.0; breaking changes are acceptable if they improve usability and consistency. The focus is on simplifying user code and making the framework more coherent, while keeping performance excellent. Tests, samples, and documentation MUST be updated accordingly.
<T>) that adopt the modelSelect<T>Current implementation:
Items : BindableList<T>ItemTemplate : DataTemplate<T>ItemTemplate is empty: resolve DataTemplateRole.Display from DataTemplates.Benefits:
TryUpdate path.OptionList<T>Current implementation:
Items : BindableList<T>ItemTemplate : DataTemplate<T>SelectedIndex remains.SelectionList<T>Current implementation:
Items : BindableList<T>ItemTemplate : DataTemplate<T>SelectedIndex + Checked : BindableList<bool> (kept in sync with Items)ListBox<T>Current implementation:
Items : BindableList<T>ItemTemplate : DataTemplate<T>ListBox<Visual> (the default registry includes Visual identity).TreeView -> TreeView<TNode> (or TreeView<T>)Current:
Roots : BindableList<TreeNode> where TreeNode contains Header : Visual and Data : object?.Proposed:
TreeView<T> with Roots : BindableList<T> + ChildrenSelector : Func<T, IEnumerable<T>>TreeView<TNode> where TNode is a node model interface/class with Children.NodeTemplate : DataTemplate<TNode> for the header/content.Migration strategy:
TreeNode API initially; introduce the generic version as additive.The V1 primitives above are chosen to unlock:
IDataTemplateSelector similar to WPF DataTemplateSelector.Editor templates for Binding<T> properties..ItemTemplate(...) extensions and diagnostics for missing templates.Data templating types SHOULD live in a dedicated namespace to keep the public API discoverable:
XenoAtom.Terminal.UI.Templating
DataTemplateRoleDataTemplateContextDataTemplateItemStateDataTemplate<T> and related delegatesDataTemplates and DataTemplatesBuilderXenoAtom.Terminal.UI.Controls
DataPresenter<T> (as a visual/control)ItemTemplate properties (e.g. Select<T>.ItemTemplate)File layout (suggested):
src/XenoAtom.Terminal.UI/Templating/
DataTemplateRole.csDataTemplateContext.csDataTemplate{T}.csDataTemplates.csDataTemplatesBuilder.cssrc/XenoAtom.Terminal.UI/Controls/
DataPresenter{T}.cs