Avalonia's ItemsControl renders a collection by creating template content for every item.
In Akbura, assign a collection to ItemsSource and declare the item template through the ItemsControl.ItemTemplate property element:
<ItemsControl ItemsSource={Vm.Items}>
<ItemsControl.ItemTemplate>
<TextBlock Text=${Binding Name} />
</ItemsControl.ItemTemplate>
</ItemsControl>
For every object in Vm.Items, Avalonia creates one TextBlock.
Akbura provides two ways to access the current item inside a template:
- A compiled binding such as
${Binding Name}. - A typed C# variable declared with
x.ItemName.
Both approaches can use a type inferred automatically from ItemsSource or provided explicitly through x.DataType.
Example model​
The examples on this page use the following types:
namespace Demo.Models;
public sealed class TaskItem
{
public int Id { get; init; }
public string Title { get; init; } = "";
public string Description { get; init; } = "";
public bool IsCompleted { get; init; }
}
namespace Demo.ViewModels;
public sealed class TasksViewModel
{
public IReadOnlyList<TaskItem> Items { get; init; } = [];
}
The view model can be injected or stored in state:
using Demo.ViewModels;
inject TasksViewModel Vm;
Basic item template​
The shortest form uses a compiled binding:
<ItemsControl ItemsSource={Vm.Items}>
<ItemsControl.ItemTemplate>
<TextBlock Text=${Binding Title} />
</ItemsControl.ItemTemplate>
</ItemsControl>
Inside ItemTemplate, the binding source is the current item rather than the component itself.
Therefore:
<TextBlock Text=${Binding Title} />
reads Title from the current TaskItem.
Conceptually, this template is applied once for every item:
Vm.Items[0] -> TextBlock
Vm.Items[1] -> TextBlock
Vm.Items[2] -> TextBlock
Automatic item type inference​
Akbura attempts to determine the item type from ItemsSource.
In this example:
<ItemsControl ItemsSource={Vm.Items}>
<ItemsControl.ItemTemplate>
<TextBlock Text=${Binding Title} />
</ItemsControl.ItemTemplate>
</ItemsControl>
Vm.Items has the type:
IReadOnlyList<TaskItem>
Because that type implements IEnumerable<TaskItem>, Akbura infers that the template data type is TaskItem.
The compiled binding is therefore checked as though its source were a TaskItem:
TaskItem item;
var value = item.Title;
This provides compile-time validation of the binding path.
For example, this is valid:
<TextBlock Text=${Binding Title} />
This produces a compiler diagnostic because TaskItem has no FullName property:
<TextBlock Text=${Binding FullName} />
Akbura can infer item types from:
- arrays such as
TaskItem[]; IEnumerable<T>;- types implementing
IEnumerable<T>; - typed C# expressions assigned to
ItemsSource; - typed binding expressions whose result type is known.
Compiled bindings​
The ${Binding ...} syntax accesses the current template item through an Avalonia binding.
<ItemsControl ItemsSource={Vm.Items}>
<ItemsControl.ItemTemplate>
<StackPanel>
<TextBlock Text=${Binding Title} />
<TextBlock Text=${Binding Description} />
</StackPanel>
</ItemsControl.ItemTemplate>
</ItemsControl>
Binding paths may contain nested properties:
<TextBlock Text=${Binding Author.DisplayName} />
They may also contain supported indexers:
<TextBlock Text=${Binding History[0].Title} />
The compiler resolves every path segment against the inferred or explicitly provided item type.
A compiled binding is useful when the value should continue to follow the item's property through Avalonia's binding system.
x.ItemName​
Use x.ItemName when the template needs a direct typed C# reference to the current item:
<ItemsControl ItemsSource={Vm.Items}>
<ItemsControl.ItemTemplate x.ItemName="item">
<TextBlock Text={item.Title} />
</ItemsControl.ItemTemplate>
</ItemsControl>
The value of x.ItemName becomes a local variable inside the template.
In this example, Akbura effectively introduces:
TaskItem item;
The variable is available to expressions inside the template:
<ItemsControl ItemsSource={Vm.Items}>
<ItemsControl.ItemTemplate x.ItemName="item">
<StackPanel>
<TextBlock Text={item.Title} />
<TextBlock
Text={item.IsCompleted
? "Completed"
: "Not completed"} />
</StackPanel>
</ItemsControl.ItemTemplate>
</ItemsControl>
Because item is a typed C# value, normal C# syntax can be used:
<TextBlock Text={item.Title.ToUpperInvariant()} />
<TextBlock Text={$"Task #{item.Id}: {item.Title}"} />
<Border IsVisible={!item.IsCompleted} />
Event handlers​
The item variable can also be captured by an event handler:
<ItemsControl ItemsSource={Vm.Items}>
<ItemsControl.ItemTemplate x.ItemName="item">
<Button Click={() => OpenTask(item.Id)}>
{item.Title}
</Button>
</ItemsControl.ItemTemplate>
</ItemsControl>
The expression is type-checked using the actual item type.
For example, if Id is an int, the compiler knows that:
OpenTask(item.Id)
passes an int.
Item name rules​
The value of x.ItemName must be a valid C# identifier.
Valid names include:
x.ItemName="item"
x.ItemName="task"
x.ItemName="user"
x.ItemName="currentItem"
Invalid names include:
x.ItemName="current-item"
x.ItemName="1item"
x.ItemName="current item"
x.ItemName is a compile-time directive. It does not set an Avalonia property and does not assign a name to the generated control.
x.ItemName and x.Name​
x.ItemName and x.Name serve different purposes.
<ItemsControl ItemsSource={Vm.Items}>
<ItemsControl.ItemTemplate x.ItemName="item">
<Button
x.Name="itemButton"
Content={item.Title} />
</ItemsControl.ItemTemplate>
</ItemsControl>
Here:
itemrefers to the current object fromItemsSource;itemButtonrefers to the generatedButton.
| Directive | Refers to |
|---|---|
x.ItemName |
The current template data item |
x.Name |
A control created by markup |
x.DataType |
The compile-time type of the template item |
x.DataType​
Use x.DataType to specify the template item type explicitly:
using Demo.Models;
<ItemsControl ItemsSource={Vm.Items}>
<ItemsControl.ItemTemplate
x.DataType="TaskItem">
<TextBlock Text=${Binding Title} />
</ItemsControl.ItemTemplate>
</ItemsControl>
x.DataType is especially useful when Akbura cannot infer the type from ItemsSource.
It may also be combined with x.ItemName:
using Demo.Models;
<ItemsControl ItemsSource={Vm.Items}>
<ItemsControl.ItemTemplate
x.DataType="TaskItem"
x.ItemName="item">
<TextBlock Text={item.Title} />
</ItemsControl.ItemTemplate>
</ItemsControl>
The declared item variable now has the explicit type:
TaskItem item;
Qualified type names​
A type imported through using can be written using its short name:
using Demo.Models;
<ItemsControl.ItemTemplate x.DataType="TaskItem">
A fully qualified type name may also be used:
<ItemsControl.ItemTemplate
x.DataType="Demo.Models.TaskItem">
The global namespace qualifier can be used when necessary:
<ItemsControl.ItemTemplate
x.DataType="global::Demo.Models.TaskItem">
When x.DataType is required​
Automatic inference works only when the compiler can obtain a concrete generic item type.
For example, inference works here:
state IReadOnlyList<TaskItem> tasks = [];
<ItemsControl ItemsSource={tasks}>
<ItemsControl.ItemTemplate x.ItemName="item">
<TextBlock Text={item.Title} />
</ItemsControl.ItemTemplate>
</ItemsControl>
The compiler can see IEnumerable<TaskItem> and infer TaskItem.
Inference may not work when ItemsSource is typed as object:
state object tasks = GetTasks();
<ItemsControl ItemsSource={tasks}>
<ItemsControl.ItemTemplate x.ItemName="item">
<TextBlock Text={item.Title} />
</ItemsControl.ItemTemplate>
</ItemsControl>
The static type of tasks does not expose IEnumerable<T>, so Akbura cannot determine the type of item.
Provide it explicitly:
using Demo.Models;
<ItemsControl ItemsSource={tasks}>
<ItemsControl.ItemTemplate
x.DataType="TaskItem"
x.ItemName="item">
<TextBlock Text={item.Title} />
</ItemsControl.ItemTemplate>
</ItemsControl>
The same applies to a non-generic collection whose element type is unknown at compile time.
INFO
Akbura uses the static compile-time type of ItemsSource.
The objects stored in the collection at runtime do not affect compile-time type inference.
Explicit type takes priority​
When x.DataType is present, it takes priority over automatic inference.
<ItemsControl ItemsSource={Vm.Items}>
<ItemsControl.ItemTemplate
x.DataType="TaskItem"
x.ItemName="item">
<TextBlock Text={item.Title} />
</ItemsControl.ItemTemplate>
</ItemsControl>
Even when Vm.Items already exposes IEnumerable<TaskItem>, the explicitly declared TaskItem type is used as the template data type.
This can make template contracts clearer, but an incorrect explicit type may cause bindings or expressions to be checked against the wrong type.
Choosing between bindings and x.ItemName​
Use a compiled binding for direct property access:
<TextBlock Text=${Binding Title} />
Use x.ItemName when you need a C# expression:
<TextBlock Text={item.Title.ToUpperInvariant()} />
Use x.ItemName for conditions:
<TextBlock
Text={item.IsCompleted ? "Completed" : "Pending"} />
Use x.ItemName in event handlers:
<Button Click={() => DeleteTask(item.Id)}>
Delete
</Button>
Both approaches may be used inside the same template:
<ItemsControl ItemsSource={Vm.Items}>
<ItemsControl.ItemTemplate x.ItemName="item">
<StackPanel>
<TextBlock Text=${Binding Title} />
<TextBlock
Text={$"Identifier: {item.Id}"} />
<Button Click={() => OpenTask(item)}>
Open
</Button>
</StackPanel>
</ItemsControl.ItemTemplate>
</ItemsControl>
Complex item layouts​
The template may contain a complete control tree:
<ItemsControl ItemsSource={Vm.Items}>
<ItemsControl.ItemTemplate x.ItemName="item">
<Border
Padding="12"
Margin="0,0,0,8">
<Grid ColumnDefinitions="*, Auto">
<StackPanel>
<TextBlock Text={item.Title} />
<TextBlock Text={item.Description} />
</StackPanel>
<TextBlock
Grid.Column="1"
Text={item.IsCompleted ? "Done" : "Pending"} />
</Grid>
</Border>
</ItemsControl.ItemTemplate>
</ItemsControl>
If one item requires several sibling controls, place them inside a panel such as:
StackPanel;Grid;DockPanel;- another suitable Avalonia control.
Bindings in ItemsSource​
ItemsSource may itself use an Avalonia binding:
<ItemsControl ItemsSource=${Binding Items}>
<ItemsControl.ItemTemplate>
<TextBlock Text=${Binding Title} />
</ItemsControl.ItemTemplate>
</ItemsControl>
When the result type of the outer binding is known, Akbura can use it to infer the template item type.
For example, if Items has the type:
IReadOnlyList<TaskItem>
the template data type is inferred as TaskItem.
When the outer binding does not expose a concrete generic collection type, provide x.DataType explicitly.
Nested ItemsControl​
Each item template has its own item scope.
Suppose a group contains a collection of tasks:
public sealed class TaskGroup
{
public string Name { get; init; } = "";
public IReadOnlyList<TaskItem> Tasks { get; init; } = [];
}
Nested collections can use separate item names:
<ItemsControl ItemsSource={Vm.Groups}>
<ItemsControl.ItemTemplate x.ItemName="group">
<StackPanel>
<TextBlock Text={group.Name} />
<ItemsControl ItemsSource={group.Tasks}>
<ItemsControl.ItemTemplate x.ItemName="task">
<TextBlock Text={task.Title} />
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ItemsControl.ItemTemplate>
</ItemsControl>
Inside the inner template:
taskrefers to the currentTaskItem;groupremains the outer template item.
Using distinct item names makes nested template expressions easier to understand.
Common errors​
The item variable is not found​
<ItemsControl ItemsSource={Vm.Items}>
<ItemsControl.ItemTemplate>
<TextBlock Text={item.Title} />
</ItemsControl.ItemTemplate>
</ItemsControl>
item was never declared.
Add x.ItemName:
<ItemsControl.ItemTemplate x.ItemName="item">
The compiler cannot determine the item type​
state object items = GetItems();
<ItemsControl ItemsSource={items}>
<ItemsControl.ItemTemplate x.ItemName="item">
<TextBlock Text={item.Title} />
</ItemsControl.ItemTemplate>
</ItemsControl>
The static type of items is object.
Add x.DataType:
<ItemsControl.ItemTemplate
x.DataType="TaskItem"
x.ItemName="item">
The compiled binding property is not found​
<TextBlock Text=${Binding FullName} />
If the item type has no FullName property, Akbura reports an invalid compiled binding path.
Use an existing property:
<TextBlock Text=${Binding Title} />
x.ItemName contains an invalid identifier​
<ItemsControl.ItemTemplate x.ItemName="current-item">
Use a valid C# identifier:
<ItemsControl.ItemTemplate x.ItemName="currentItem">
Complete example​
using Avalonia.Controls;
using Demo.Models;
using Demo.ViewModels;
inject TasksViewModel Vm;
void OpenTask(TaskItem task)
{
Console.WriteLine($"Opening task {task.Id}");
}
<ItemsControl ItemsSource={Vm.Items}>
<ItemsControl.ItemTemplate
x.DataType="TaskItem"
x.ItemName="task">
<Border
Padding="12"
Margin="0,0,0,8">
<Grid ColumnDefinitions="*, Auto">
<StackPanel>
<TextBlock Text=${Binding Title} />
<TextBlock
Text={task.Description} />
</StackPanel>
<Button
Grid.Column="1"
Click={() => OpenTask(task)}>
Open
</Button>
</Grid>
</Border>
</ItemsControl.ItemTemplate>
</ItemsControl>
In this example:
Vm.Itemssupplies the collection.x.DataType="TaskItem"explicitly declares the item type.x.ItemName="task"creates a typed C# variable.${Binding Title}reads the current item through a compiled binding.{task.Description}uses a direct C# expression.- The button event captures the current
task.