Skip to content

Caret blink: RunAsync on an Infinite animation returns a faulted Task per instance ("Looping animations must not use the Run method") #49

Description

@ronnycohen

Description

RichTextBox starts its caret blink in the constructor path (InitializeAdornerElementsInitializeBlinkAnimation) with:

  • an Animation whose IterationCount = IterationCount.Infinite, and
  • blinkAnimation.RunAsync(_CaretRect); — fire-and-forget, the returned Task is neither awaited nor stored.

Avalonia refuses infinite animations on the Run/RunAsync path: instead of throwing, it returns a faulted taskTask.FromException(new InvalidOperationException("Looping animations must not use the Run method.")).

Consequences

  1. The caret never actually blinks (the animation never starts).
  2. Every new RichTextBox() produces a faulted, unobserved Task that later surfaces as TaskScheduler.UnobservedTaskException on the finalizer thread — with an empty stack trace (the exception is never thrown), which makes it very hard for consumers to trace back to this control.
  3. Consumers cannot observe or prevent it: the field is private and the task is discarded inside the constructor.

In our app (digital signage kiosk that periodically rebuilds screens containing several RichTextBox-based widgets) this generated ~940 telemetry events over 2 weeks across 4 devices before we could locate the source.

Repro

TaskScheduler.UnobservedTaskException += (s, e) => Console.WriteLine(e.Exception);
var rtb = new AvRichTextBox.RichTextBox();  // creates the faulted task
rtb = null;
GC.Collect(); GC.WaitForPendingFinalizers();
// -> AggregateException(InvalidOperationException("Looping animations must not use the Run method."))

Package: Simplecto.Avalonia.RichTextBox 1.9.3 (latest) · Avalonia 12.x (the RunAsync behavior is the same on 11.x). Still present on current main.

Suggested fix

Any of:

  • drive the blink through the styling/Apply path (which supports IterationCount.Infinite), or
  • run a finite animation restarted on completion, or
  • keep RunAsync but with a finite iteration count and observe the returned task.

Ideally only start blinking when the control actually has focus / the caret is visible — that would also avoid the cost for read-only usages.

Workaround (downstream)

We filter this exact message in our global UnobservedTaskException handler, which silences the noise but obviously doesn't restore the blink.

Thanks for the control — happy to test a fix.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions