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

  1. Use a fresh binding expression instance for ToObservable, separate from the one applied to an AvaloniaObject.
  2. 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.
  3. 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

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


AI-assisted analysis of AvaloniaUI/Avalonia@11c5427268 (2026-08-13). Data as JSON: /api/errors/ff31b507ddef9b03. Report an issue: GitHub.