Skip to content

Instantly share code, notes, and snippets.

@smourier
Last active April 18, 2024 17:46
Show Gist options
  • Save smourier/d1961e2a8d18e762746cebe5948d36db to your computer and use it in GitHub Desktop.
Save smourier/d1961e2a8d18e762746cebe5948d36db to your computer and use it in GitHub Desktop.
Create a WinUI3 Xaml window in another thread
using Microsoft.UI.Xaml;
namespace WinUIAppFx
{
public partial class App : Application
{
private Window m_window;
public App()
{
InitializeComponent();
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
if (m_window != null) // WindowsXamlManager.InitializeForCurrentThread will call this too, show the main only once
return;
m_window = new MainWindow();
m_window.Activate();
}
}
}
<Window
x:Class="WinUIAppFx.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<Button Click="Button_Click">click</Button>
</Grid>
</Window>
using System;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Hosting;
[assembly: SupportedOSPlatform("windows10.0.17763.0")]
namespace WinUIAppFx
{
public sealed partial class MainWindow : Window
{
private MyOtherWindow _myOtherWindow;
public MainWindow()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
if (_myOtherWindow != null)
return;
Title = "On thread " + Environment.CurrentManagedThreadId;
var thread = new Thread(state =>
{
// create a DispatcherQueue on this new thread
var dq = DispatcherQueueController.CreateOnCurrentThread();
// initialize xaml in it
WindowsXamlManager.InitializeForCurrentThread();
// create a new window
_myOtherWindow = new MyOtherWindow(); // some other Xaml window you've created
_myOtherWindow.AppWindow.Show(true);
// run message pump
dq.DispatcherQueue.RunEventLoop();
});
thread.IsBackground = true; // will be destroyed when main window is closed, behavior can be changed
thread.Start();
// send some message to the second window to check it's handled from another thread
// note: real code should wait for _myOtherWindow to be fully initialized...
Task.Run(async () =>
{
for (var i = 0; i < 10; i++)
{
await Task.Delay(1000);
_myOtherWindow.DispatcherQueue.TryEnqueue(() =>
{
_myOtherWindow.Title = "#" + i + " on thread " + Environment.CurrentManagedThreadId;
});
}
});
}
}
}
@peter0302
Copy link

peter0302 commented Apr 18, 2024

Befpre dq.DispatcherQueue.RunEventLoop(); you also need:

SynchronizationContext.SetSynchronizationContext(new DispatcherQueueSynchronizationContext(dq.DispatcherQueue));

Otherwise any await on the second UI thread - for example an event handler - will resume on a worker thread. If any UI code is run after that it will throw an exception.

@smourier
Copy link
Author

I didn't need anything more than this when I tested it.

@peter0302
Copy link

Try putting await Task.Delay(1) before _myOtherWindow.Title = "#" + i + " on thread " + Environment.CurrentManagedThreadId;. I believe you'll get a thread exception. You'll start on the second UI thread but after Task.Delay you'll be on a background worker.

@smourier
Copy link
Author

Everything was done and works by design, there are multiple non-STA threads indeed and there may be a race condition especially if you remove the 1000 sec wait, this is why there is a "real code should wait for _myOtherWindow to be fully initialized..." comment.

@peter0302
Copy link

peter0302 commented Apr 18, 2024

Do you understand what SynchronizationContext is and its role in awaiting something on a UI thread? Trust me, you need my line or something like it, or else any UI code run on the 2nd window that includes async/await will break.

See also:
https://github.com/microsoft/microsoft-ui-xaml-specs/blob/master/winui3/DispatcherQueueUpdates.md

Or don't believe me, I'm fine either way.

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