AvaloniaUI/Avalonia · error · InvalidOperationException
Cannot call AsObservable on a to binding expression which is
Error message
Cannot call AsObservable on a to binding expression which is already instantiated on an AvaloniaObject.
What it means
A binding expression may serve either as an observable (ToObservable) or be instantiated on an AvaloniaObject, but not both. ToObservable() throws when the expression already has a non-ObservableSink, meaning it is attached to a target. This guards the mutually-exclusive usage noted in the remarks.
Source
Thrown at src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs:239
/// </exception>
/// <remarks>
/// This method is mostly here for unit testing and we may want to remove it in future. In
/// particular its usefulness is limited in that it preserves the semantics of binding
/// expressions as expected by unit tests, not necessarily the semantics that will be used
/// when the expression is used as an <see cref="IValueEntry"/> instantiated in a
/// <see cref="ValueStore"/>. Unit tests should be migrated to not test the behaviour of
/// binding expressions through an observable, and instead test the behaviour of the binding
/// when applied to an <see cref="AvaloniaObject"/>.
///
/// A binding expression may only act as an observable or as a binding expression targeting an
/// AvaloniaObject, not both.
/// </remarks>
internal IAvaloniaSubject<object?> ToObservable(AvaloniaObject? target = null)
{
if (_sink is ObservableSink s)
return s;
if (_sink is not null)
throw new InvalidOperationException(
"Cannot call AsObservable on a to binding expression which is already " +
"instantiated on an AvaloniaObject.");
var o = new ObservableSink(this);
_sink = o;
_target = target is not null ? new(target) : null;
return o;
}
/// <summary>
/// When overridden in a derived class, writes the specified value to the binding source if
/// possible.
/// </summary>
/// <param name="value">The value to write.</param>
/// <returns>
/// True if the value could be written to the binding source; otherwise false.
/// </returns>
internal virtual bool WriteValueToSource(object? value) => false;View on GitHub (pinned to 11c5427268)
Solutions
- Use a fresh binding expression instance for ToObservable, separate from the one applied to an AvaloniaObject.
- Rewrite the test to observe the bound AvaloniaObject's property changes (e.g. via `control.GetObservable(prop)`) instead of calling ToObservable on an attached expression.
- If you need observable semantics, do not attach the expression to a target first.
Example fix
// before (throws - same expr attached and observed): var expr = binding.CreateInstance(target, prop, anchor); ((UntypedBindingExpressionBase)expr).ToObservable(target); // after (separate instances): var forTarget = binding.CreateInstance(target, prop, anchor); target.Bind(prop, binding); // observe the target property directly: target.GetObservable(prop).Subscribe(...);
Defensive patterns
Strategy: validation
Validate before calling
// Do not mix: choose observable OR attached-to-target per instance.
if (expr is UntypedBindingExpressionBase b && /* already attached */) { /* use target observable */ } Type guard
static bool IsObservableOnly(UntypedBindingExpressionBase e) => e._sink is null or ObservableSink; // (conceptual; sink is private)
Prevention
- Never attach the same expression to a target and also call ToObservable.
- Observe bound targets via control.GetObservable(property).
- Use a fresh expression instance for observable tests.
When it happens
Trigger: Calling ToObservable()/AsObservable on a binding expression that has already been attached to an AvaloniaObject target via AttachCore (i.e. `_sink` is set to a sink that is not an ObservableSink).
Common situations: Legacy unit tests that apply a binding to a control and then also try to observe it as an IObservable. Migrating test code that mixed observable and binding-expression usage on the same instance.
Related errors
- BindingExpression has not been started.
- BindingExpression was already attached.
- BindingExpression was already attached to a different proper
- Cannot subscribe to IndexerDescriptor.
- Cannot create ElementName binding when NameScope is null
AI-assisted analysis of AvaloniaUI/Avalonia@11c5427268 (2026-08-13).
Data as JSON: /api/errors/ff31b507ddef9b03.
Report an issue: GitHub.