AvaloniaUI/Avalonia · error · InvalidOperationException

Transform container stack is non-empty

Error message

Transform container stack is non-empty

What it means

ImmediateDrawingContext.Dispose throws InvalidOperationException("Transform container stack is non-empty") when _transformContainers still has entries after all states are popped. Every PushTransformContainer must be matched by disposing its PushedState before the context is disposed.

Source

Thrown at src/Avalonia.Base/Media/ImmediateDrawingContext.cs:362

                throw new ObjectDisposedException(nameof(DrawingContext));
            _transformContainers.Push(new TransformContainer(CurrentTransform, _currentContainerTransform));
            _currentContainerTransform = CurrentTransform * _currentContainerTransform;
            _currentTransform = Matrix.Identity;
            return new PushedState(this, PushedState.PushedStateType.MatrixContainer);
        }

        /// <summary>
        /// Disposes of any resources held by the <see cref="DrawingContext"/>.
        /// </summary>
        public void Dispose()
        {
            if (_states is null || _transformContainers is null)
                throw new ObjectDisposedException(nameof(DrawingContext));
            while (_states.Count != 0)
                _states.Peek().Dispose();
            StateStackPool.ReturnAndSetNull(ref _states);
            if (_transformContainers.Count != 0)
                throw new InvalidOperationException("Transform container stack is non-empty");
            TransformStackPool.ReturnAndSetNull(ref _transformContainers);
            if (_ownsImpl)
                PlatformImpl.Dispose();
        }

        private static bool PenIsVisible(IPen? pen)
        {
            return pen?.Brush != null && pen.Thickness > 0;
        }

        public object? TryGetFeature(Type type) => PlatformImpl.GetFeature(type);
    }
}

View on GitHub (pinned to 11c5427268)

Solutions

  1. Always wrap PushTransformContainer in a using/using-declaration.
  2. Ensure every push has a matching pop on all code paths including exceptions.
  3. Keep transform-container pushes lexically scoped.

Example fix

// before
ctx.PushTransformContainer(); // result discarded
ctx.Dispose(); // throws: stack non-empty

// after
using (ctx.PushTransformContainer()) { /* draw */ }
ctx.Dispose();
Defensive patterns

Strategy: validation

Validate before calling

// Always pair PushTransformContainer with a using.
using (ctx.PushTransformContainer()) { /* draw */ }
// Now safe to dispose the context.

Prevention

When it happens

Trigger: Calling PushTransformContainer and disposing the context without disposing the returned PushedState — a leaked transform-container push leaves an entry on the stack.

Common situations: Forgetting to using/dispose a PushTransformContainer result; an exception skipping the dispose; conditional branches that push but do not pop.

Related errors


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