ItemTemplateControl is MeshWeaver's declarative for-each renderer. Give it a collection and a view template, and it stamps out one copy of that template per item — reactively, keeping the DOM in sync as the collection changes. 1. Static Collection T[] / IEnumerable<T> 2. Observable Stream IObservable<IEnumerable<T>> 3. Template.Bind Nested parent + children 4. Workspace Stream GetStream<T>() TypeSource BindMany ItemTemplateControl TemplateBuilderVisitor Compile expression tree ItemTemplate.razor Render one copy per item

Four data-supply patterns converge into BindManyItemTemplateControl, which compiles the expression tree and stamps one rendered copy per item.

Usage Patterns

There are four ways to supply data to BindMany, depending on where your collection lives.

1. Static Collection

The simplest case: you already have an in-memory array or list.

var items = new[] { new User("Alice"), new User("Bob") };
var control = items.BindMany(item => Controls.Label(item.Name));

MeshWeaver's TemplateBuilderVisitor analyses the expression tree at build time and rewrites property accessors — item.Name becomes a JsonPointerReference("name") binding. The resulting template is entirely data-driven and renders correctly on the client without any runtime reflection.

2. Observable Stream

For collections that change over time, pass an IObservable<IEnumerable<T>>. The client re-renders automatically whenever the stream emits.

IObservable<IEnumerable<AccessAssignment>> assignmentStream = ...;

var control = assignmentStream.BindMany("assignments", a =>
    Controls.Stack
        .WithOrientation(Orientation.Horizontal)
        .WithView(Controls.Label(a.DisplayLabel))
        .WithView(Controls.Switch(a.IsActive))
);

The id parameter ("assignments") names the data stream inside the layout host's store. New emissions from the observable propagate to the client as JSON Patch operations — only the changed elements update.

3. Nested Inside Template.Bind

When you need to bind both a parent object and a child collection within it, combine Template.Bind with Template.BindMany:

return Template.Bind(
    filterEntity,
    x => Template.BindMany(x.Items, y => Controls.CheckBox(y.Value)),
    "myFilter"
);

This creates an ItemTemplateControl whose DataContext points to the parent object's stream, and whose Data property is a JsonPointerReference into the items sub-path of that context. The parent and children share a single coherent data tree.

4. Workspace Stream

When your data is already registered as a workspace type (via WithTypeSource on a DataSource), retrieve it through the workspace and bind with BindMany:

var stream = host.Workspace.GetStream<MyType>()
    ?.Select(items => items?.AsEnumerable() ?? Enumerable.Empty<MyType>())
    ?? Observable.Return(Enumerable.Empty<MyType>());

var control = stream.BindMany("my_stream_id", item =>
    Controls.Stack.WithView(Controls.Label(item.Name))
);

Tip: Unlike the plain observable pattern above, you do not call SetData manually here. The workspace's TypeSource handles loading from the backing store on initialization and syncing changes whenever a DataChangeRequest arrives. The whole persistence and change-tracking pipeline is automatic.

How Data Binding Works

When BindMany compiles the expression tree, TemplateBuilderVisitor replaces every property accessor with a JsonPointerReference:

Expression:  item => Controls.Label(item.DisplayLabel)
Compiled to: Controls.Label(new JsonPointerReference("displayLabel"))

On the Blazor client, ItemTemplate.razor iterates over the bound collection and renders one copy of the View template per item. Each item receives a scoped DataContext path:

/data/"assignments"/0    -- first item
/data/"assignments"/1    -- second item

The template's JsonPointerReference("displayLabel") resolves relative to each item's DataContext, producing paths like /data/"assignments"/0/displayLabel. Pointer resolution is entirely client-side — no round-trip per item.

Data Flow

graph LR A[Observable Stream] -->|Push data| B[Layout Host Data Store] B -->|JSON Patch| C[Client Workspace] C -->|DataBind| D[ItemTemplate.razor] D -->|For each item| E[Rendered View Copy]
Step What happens
1 The server-side observable pushes data into the layout host via SetData
2 Changes propagate to the client as JSON Patch operations
3 ItemTemplate.razor subscribes to the data at DataContext via DataBind
4 For each element in the array, a copy of View is rendered with an item-scoped DataContext

Live Demo

The cell below builds a small static collection and renders it with BindMany, so you can see the template expansion in action:

record UserRow(string Name, string Role);

var rows = new[]
{
    new UserRow("Alice",   "Admin"),
    new UserRow("Bob",     "Editor"),
    new UserRow("Charlie", "Viewer"),
};

// The property is passed DIRECTLY as a control argument — Controls.Label(u.Name). BindMany
// rewrites the expression tree, replacing each property getter with a BINDING; a property read
// inside an interpolated string ($"{u.Name}") is consumed by string.Format before the rewriter
// can see it, so the item would render empty. Label, not Text: Controls.Text is an editable
// TextFieldControl whose value lives in an input, not as visible text.
MeshWeaver.Layout.Controls.Stack
    .WithView(MeshWeaver.Layout.Controls.Title("Users", 4))
    .WithView(rows.BindMany(u => MeshWeaver.Layout.Controls.Label(u.Name)))

Reference

Resource Path
Template.BindMany methods src/MeshWeaver.Layout/Template.cs
ItemTemplateControl record src/MeshWeaver.Layout/ItemTemplateControl.cs
Blazor renderer src/MeshWeaver.Blazor/Components/ItemTemplate.razor
Layout test examples test/MeshWeaver.Layout.Test/LayoutTest.cs — see TestItemTemplate, TestDataBoundCheckboxes
Reconnecting…
The server was updated. Reloading the page to pick up the latest version.