Collection Parameters

Collection Parameters​

A default param IList or param IList<T> is an owned collection with a writable source setter. The component creates an ObservableCollection<object> or ObservableCollection<T> for itself. Assigning another collection copies its items into that backing and connects notifications; it never replaces the backing object. Each component instance owns a separate list, including when no value is supplied.

Declaring a collection parameter​

Save this component as ItemsPanel.akbura:

using Avalonia.Controls;
using System.Collections.Generic;

param IList<int> Data;

<StackPanel>
    <TextBlock Text={$"Count: {Data.Count}"} />
    $foreach (var item in Data)
    {
        <TextBlock Text={$"Item: {item}, index: {@index}"} />
    }
</StackPanel>

The public CLR property still has the declared IList<int> type. Its getter always returns the same backing. The setter takes a source of that declared type. The associated Avalonia property is a writable direct property, not a readonly collection property and not a styled property containing a shared collection default.

Optional parameter​

No empty list has to be passed merely to construct the component:

<ItemsPanel />

Data starts empty. Required non-collection parameters are still required. The collection is optional in both semantic diagnostics and runtime initialization. A declared initializer is evaluated once for each backing instance and then supplied through the same source connection; its collection object is not shared accidentally.

Observable state source​

using System.Collections.ObjectModel;

state ObservableCollection<int> items = [1, 2, 3];

<ItemsPanel Data={items} />

Adding/removing items in items updates the component backing. Editing the backing updates a writable notifying source as well. Assigning a new source unsubscribes from the old one, reconciles the contents and subscribes to the new source. Reassigning the exact same source on a later component update is a no-op: it does not clear, enumerate or subscribe again. Assigning the backing to itself is also a no-op; it does not accidentally clear the list or discard its existing connection.

Static resource source​

Numbers may resolve either to an IList<int> implementation or to a non-generic enumerable such as ArrayList. Akbura validates every item and copies the references into the component-owned typed backing collection:

<ItemsPanel Data=${StaticResource Numbers} />

Static resource lookup and collection observation are separate. Replacing the resource entry is not a dynamic resource update, but INotifyCollectionChanged events from the resolved collection continue to update the owned list. Use DynamicResource when replacing the resource entry itself must reconnect the parameter.

A plain ArrayList has no change notifications, so it is snapshotted once. If application code mutates that same source instance, refresh it explicitly:

ItemsPanel.DataProperty.RefreshSource(itemsPanel);

The public Data property remains IList<int>; assigning an untyped source from C# uses the descriptor API instead of weakening that property:

ItemsPanel.DataProperty.SetSource(itemsPanel, legacyArrayList);

DataContext binding​

Here Items may be an IList<int>, System.Collections.IList, or ArrayList property on the inherited DataContext:

<ItemsPanel Data=${Binding Items} />

The Avalonia binding delivers a source reference to the parameter setter. If the DataContext implements INotifyPropertyChanged, replacing its Items property reconnects the parameter. Reference binding defaults to OneWay: the component must not overwrite the ViewModel property with its own backing list.

This does not prevent two-way item synchronization. A writable notifying list can receive Add, Remove, Replace, Move and Clear operations from the component while its ViewModel property continues to reference the original list. No Mode=TwoWay is needed for this content synchronization.

Non-generic and concrete observable declarations​

A non-generic parameter keeps mixed items in ObservableCollection<object>:

using System.Collections;
using Avalonia.Controls;

param IList Data;

<TextBlock Text={$"Count: {Data.Count}"} />

The concrete standard observable type uses the same source-setter behavior:

using System.Collections.ObjectModel;
using Avalonia.Controls;

param ObservableCollection<int> Data;

<TextBlock Text={$"Count: {Data.Count}"} />

The implementation also preserves existing support for ICollection<T> backed by an observable list. A public concrete ObservableCollection<T> subclass with a public parameterless constructor can be the backing type itself.

A concrete List<T>, an arbitrary custom IList<T> class or a custom derived interface cannot be replaced with ObservableCollection<T> while preserving its CLR type. Such declarations are not silently rewritten or unsafely cast by this feature. Prefer param IList<T> and pass that custom list as its source. Legacy concrete List<T> parameter generation is unchanged. Explicit param bind and param out retain their existing reference-parameter semantics; the automatic owned-list contract described here applies to unmodified param declarations.

Synchronization contract​

Source object Source to backing Backing to source
Writable IList<T> or IList implementing INotifyCollectionChanged Event deltas Yes
Readonly notifying list, including ReadOnlyObservableCollection<T> Event deltas No
Fixed-size notifying list Event deltas No
Non-notifying list or array Snapshot when the source reference changes No
Enumerable without a writable indexed-list contract Snapshot; notifying sources refresh on events No
null Disconnect and empty the owned list No

Both generic ICollection<T>.IsReadOnly and nongeneric IList.IsReadOnly / IsFixedSize are considered when present. Arrays cannot support insertion/removal merely because their individual elements can be assigned.

A readonly source does not make the owned backing readonly. Local edits stay local; the next source notification makes the backing match the authoritative source again. For a non-notifying source, use a new list reference to publish a new snapshot. The runtime connection also exposes Refresh() for explicit refresh by custom hosts; ordinary component updates do not poll a non-notifying list of unchanged identity.

Ordered deltas use indexes, not Remove(value). Duplicate values and equal-but-distinct reference objects remain separate occurrences. A standard single-item Move remains a Move on the owned observable collection. Range notifications are translated into indexed operations. Reset, unusable indexes and a divergent readonly projection use a fresh source snapshot, with linear positional reconciliation instead of unconditional Clear plus Add. Reset, an unaddressable notification and explicit refresh relay replacement notifications even for unchanged references: an object may have changed without changing identity. Replacing the source with an equivalent snapshot does not produce those forced replacements. Normal indexed Add/Remove/Move events leave unrelated items untouched.

New source items are validated before the old connection is removed. A heterogeneous nongeneric source containing incompatible elements is rejected. This bridge is not a transaction system for arbitrary user collections: a source method or an external collection-event handler can throw after mutating the source. Such failures are not reported as successful synchronization. A source altered by another listener during a reverse write is reconciled after the owned CollectionChanged event unwinds. Indexed source-to-backing deltas do not enumerate the complete source. Reverse writes currently validate a source snapshot afterwards; that validation is linear in the source size, even when the outgoing mutation itself was one indexed operation.

Foreach and component lifetime​

$foreach already observes the source object's INotifyCollectionChanged contract; it does not require a concrete ObservableCollection<T>. The parameter bridge checks that same interface, including generic-only custom lists. The loop subscribes to the stable owned list and receives its deltas without a new visual/logical wrapper. An unrelated parameter update does not force the bridge to enumerate or reset its source.

The bridge schedules a component update after collection notifications have been delivered. This also refreshes expressions such as Data.Count outside the loop. It does not guarantee that every loop body is skipped: @index, environment reads, break, keys and body dependencies still follow the normal foreach rules.

The source subscription is removed on component detachment and replaced on source assignment. On reattachment, the current source is authoritative and refreshed; local edits made while detached are not replayed back into it. A weak source listener prevents an application-scoped resource from retaining a discarded component. Collection events and mutation must occur on the owning UI thread; the bridge does not enumerate a list concurrently on a worker thread.

Compatible Hot Reload updates retain the direct-property identity, owned list and source connection. The writable-source descriptor has a new contract key so it is not confused with an older readonly collection descriptor. Changing/removing the parameter contract is not treated as a compatible source update.

Collection content​

Content collections and collection property elements still use the generated Add helper and the same owned backing. Clearing/resetting collection content now synchronizes logical children rather than throwing the old "Resetting component content" exception. Avoid configuring the same destination simultaneously from source assignment and markup children unless appending those children into a writable bound source is intentional.

See Foreach Markup for keys, indexes, early exits and rendering behavior.