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
Summary
In a UWP app on modern .NET (
<UseUwp>true</UseUwp>), a handler for a static XAML event subscribed from a secondaryCoreApplicationview is invoked on the first view that subscribed, not on the view that subscribed it.CompositionTarget.Renderingis 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
Result
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:
So the statics object is a process-wide agile singleton, and
add_Renderinginvoked 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 secondadd_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), andMicrosoft.Windows.UI.Xaml.dllcontains noThreadStatic/ThreadLocalanywhere.Impact
Anything frame-driven in a secondary view runs on the wrong thread.
Windows.UI.Compositionobjects 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 callingWriteableBitmap.Invalidate()on a bitmap created on the other view:Any per-view static WinRT event is affected, not just this one.
Environment
net10.0-windows10.0.26100.0,UseUwp=true, x64, self-contained, no AOT