microsoft / microsoft/CsWinRT

UWP: static XAML events subscribed from a secondary CoreApplication view are invoked on the first subscriber's thread

Open
#2,524 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C#
Stars
665
Forks
134
Avg merge
1d 3h
Merged PRs (30d)
32

Description

Summary

In a UWP app on modern .NET (<UseUwp>true</UseUwp>), a handler for a static XAML event subscribed from a secondary CoreApplication view is invoked on the first view that subscribed, not on the view that subscribed it.

CompositionTarget.Rendering is the clearest case: both views' handlers are driven by a single registration, so the secondary view's frame callback arrives on the main view's thread. Under .NET Native the handler ran on the subscribing view's thread.

Repro
protected override async void OnLaunched(LaunchActivatedEventArgs e)
{
    _mainThreadId = Environment.CurrentManagedThreadId;
    Window.Current.Content = new Grid();
    Window.Current.Activate();

    CompositionTarget.Rendering += OnMainRendering;

    var newView = CoreApplication.CreateNewView();
    int viewId = 0;

    await newView.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
    {
        _secondaryThreadId = Environment.CurrentManagedThreadId;
        Window.Current.Content = new Grid();
        Window.Current.Activate();
        viewId = ApplicationView.GetForCurrentView().Id;

        CompositionTarget.Rendering += OnSecondaryRendering;   // subscribed on this view's thread
    });

    await ApplicationViewSwitcher.TryShowAsStandaloneAsync(viewId);
}

// each handler records Environment.CurrentManagedThreadId and increments a counter
Result
main thread 4 ticks 256
secondary thread 5 ticks 256
Rendering handlers ran on: main 4, secondary 4   <-- both on the main view's thread

Two independent view render loops would not stay in lockstep; the tick counts are exactly equal on every run (256/256, 258/258, 262/262), which is consistent with both delegates being invoked from one registration rather than one per view.

The ABI allows what the projection does not

Fetching the statics directly, on each view's thread:

RoGetActivationFactory("Windows.UI.Xaml.Media.CompositionTarget", IID_ICompositionTargetStatics)
  main      0x25B31BA1878
  secondary 0x25B31BA1878   (same object)

QueryInterface(IAgileObject) -> S_OK    (agile, so a call from either view runs on the calling thread)

So the statics object is a process-wide agile singleton, and add_Rendering invoked from the secondary view's thread would register against that view. The aggregation appears to happen above the ABI, in the per-statics event source cache: the second += does not produce a second add_Rendering, it appends the delegate to the registration the first view already made.

Consistent with that, the generated projection caches the statics in a plain static field (__objRef_global__Windows_UI_Xaml_Media_ICompositionTargetStatics), and Microsoft.Windows.UI.Xaml.dll contains no ThreadStatic/ThreadLocal anywhere.

Impact

Anything frame-driven in a secondary view runs on the wrong thread. Windows.UI.Composition objects tolerate it because they are agile, which masks the problem; XAML objects do not. In our app the visible symptom is that image animations in a secondary window freeze, because the handler ends up calling WriteableBitmap.Invalidate() on a bitmap created on the other view:

System.Runtime.InteropServices.COMException (0x8001010E)   // RPC_E_WRONG_THREAD
   at ABI.Windows.UI.Xaml.Media.Imaging.IWriteableBitmapMethods.Invalidate(IObjectReference _obj)
   at Windows.UI.Xaml.Media.Imaging.WriteableBitmap.Invalidate()

Any per-view static WinRT event is affected, not just this one.

Environment
  • Windows 11 10.0.26200
  • .NET SDK 10.0.302, net10.0-windows10.0.26100.0, UseUwp=true, x64, self-contained, no AOT
  • Microsoft.Windows.SDK.NET.Ref projection 10.0.26100.55
  • Microsoft.Windows.CsWinRT 2.2.0 referenced by the app

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by tracing the generated projection's plain static __objRef_global__Windows_UI_Xaml_Media_ICompositionTargetStatics cache and the static event add/remove path. Reproduce the two CoreApplication views and compare registration and callback threads; done means each view's handler is registered and invoked on its subscribing view's thread without regressing the reported behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
desktop, operating-systems
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.