dotnet / wpf

WPF is a .NET Core UI framework for building Windows desktop applications.
MIT License
7.1k stars 1.17k forks source link

WPF NET 9.0 (latest preview)-based app that uses the built-in Fluent theme crashes when Theme is changed #9906

Open danielkornev opened 1 month ago

danielkornev commented 1 month ago

Description

This problem appears on two kinds of OS:

Here's the error I've got in the beginning on both machines:

Monday, October 7, 2024, 10/7/2024 12:48:28 PM
Exception [System.InvalidOperationException]: The calling thread cannot access this object because a different thread owns it.
System.InvalidOperationException: The calling thread cannot access this object because a different thread owns it.
   at System.Windows.Threading.Dispatcher.<VerifyAccess>g__ThrowVerifyAccess|7_0()
   at System.Windows.ThemeManager.OnSystemThemeChanged()
   at System.Windows.SystemResources.SystemThemeFilterMessage(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.Dispatcher.LegacyInvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
   at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)

Sadly there was no way to figure out why this error happened. My app showed this message and then crashed. App uses Microsoft.VisualBasic library to get a Single Instance behavior, so catching the exception would get us to app.Run method.

After several experiments, the app now crashes w/o even this error after the theme of the apps in System Settings is changed to Light/Dark.

Reproduction Steps

  1. Create a WPF 9.0 app that uses Visual Basic assembly's Application to get a Single Instance Application
  2. Use Fluent theme as a ResourceDictionary across your app
  3. Switch theme of all apps in System Settings from Light to Dark or vice versa

Expected behavior

Theme changes where expected

Actual behavior

App crashes (initially with an error message and now silently)

Regression?

there were no problems prior to this moment

Known Workarounds

  1. don't use Fluent theme?
  2. Added these lines in SingleInstanceApplication's OnStartup override event handler:
#pragma warning disable WPF0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
                this.ThemeMode = ThemeMode.Light;
#pragma warning restore WPF0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

This seems to keep app working.

Impact

App cannot be given to the end users. It crashes every time user changes an app color in System Settings on Windows 11, and it crashes (in general) every time the theme changes on Windows 10/11.

Configuration

.NET 9 (preview, latest) Windows 10: 24H2, amd64 Windows 11: 24H2 Canary, arm64

Other information

No response

danielkornev commented 1 month ago

Updated workaround:

Added these lines in SingleInstanceApplication's OnStartup override event handler:

#pragma warning disable WPF0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
                this.ThemeMode = ThemeMode.Light;
#pragma warning restore WPF0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
h3xds1nz commented 1 month ago

Quick glance, this isn't invoked on the dispatcher, while all other calls from SystemThemeFilterMessage always are.

https://github.com/dotnet/wpf/blob/278c355919ad7496717b370ae9185857d26cf179/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/ThemeManager.cs#L38

dipeshmsft commented 1 month ago

Hey @danielkornev, can you give a sample repro. I am afraid, I have not worked with Visual Basic a lot and may have missed somethings.

danielkornev commented 1 month ago

I’ll try to,

But not right now. Got a workaround that seems to work (requiring the system to have only one theme set), it kinda works. Have a lot more stuff to build for a client ☹

Sorry!

dipeshmsft commented 1 month ago

Okay, no issues. Just wanted one clarification on the above message. Switching theme while the application is running is also causing the application to crash ?

danielkornev commented 1 month ago

Precisely.

My understanding is, once the event that changes the theme is fired (you can trigger it by changing all apps color in System Settings), app crashes.

On Windows 10, it seems like this event is also triggered when Spotlight changes the background image.

Get Outlook for Androidhttps://aka.ms/AAb9ysg


batzen commented 1 month ago

If i am not mistaken the code in ThemeManager also ignores the fact that a Window might run on a different dispatcher thread. Has anyone tried that yet?

batzen commented 1 month ago

ThemeManager also seems to ignore the fact that Application.Current might be null. Has anyone tried that yet?

dipeshmsft commented 1 month ago

ThemeManager also seems to ignore the fact that Application.Current might be null. Has anyone tried that yet?

Regarding this, I have tried to check everywhere is Application.Current is null or not before performing application wide. In this issue, https://github.com/dotnet/wpf/issues/9373 they have tried using Fluent without creating an Application instance, is that what you are asking about ?

batzen commented 1 month ago

@dipeshmsft That's one part. But I guess the ThemeManager causes a crash as soon as the Windows theme changes as it's trying to access Application.Current in that case.

h3xds1nz commented 1 month ago

If i am not mistaken the code in ThemeManager also ignores the fact that a Window might run on a different dispatcher thread. Has anyone tried that yet?

Yep, whole ThemeManager does not count on that, which doesn't correspond with how the rest is written.

dipeshmsft commented 1 month ago

That's one part. But I guess the ThemeManager causes a crash as soon as the Windows theme changes as it's trying to access Application.Current in that case.

Can you send me a sample repro for this issue?

I will try to get it in before the GA happens

batzen commented 1 month ago

@dipeshmsft Just do this somewhere in your project, without having an application object, and then change some windows setting. When having an application object the theme for the threaded window simply doesn't change, which i consider another bug.

    private static void StartNewThreadedWindow()
    {
        var thread = new Thread(StartOnThread);
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
    }

    private static void StartOnThread(object? obj)
    {
        var window = new Window
        {
            ThemeMode = ThemeMode.System
        };
        window.Show();
        Dispatcher.Run();
    }

Usage of Application.Current seem to be guarded by calls to IsFluentThemeEnabled which checks for Application.Current != null. So i was wrong with that assumption.

danielkornev commented 2 weeks ago

One more issue:

If you are in the situation like mine, and you want to use a custom title area, when theme switches, color of the icons for minimize, maximize/restore, and close buttons (on the right side of the app's custom title area) change to white/invisible (no idea). Curiously, mouse over still registers, custom snap layouts appear, and you can minimize/maximize/close window, but you have to switch visibility off/on in Hot Reload to make icons visible again.

Go figure.

Upd 1: maybe its because of this?

private void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
{
    Dispatcher.Invoke(() =>
    {
        if (SystemParameters.HighContrast)
        {
            MinimizeButton.Visibility = Visibility.Visible;
            MaximizeButton.Visibility = Visibility.Visible;
            CloseButton.Visibility = Visibility.Visible;
        }
        else
        {
            MinimizeButton.Visibility = Visibility.Collapsed;
            MaximizeButton.Visibility = Visibility.Collapsed;
            CloseButton.Visibility = Visibility.Collapsed;
        }
    });
}

but if so, then why mouse over still works?

Upd 2: yep, this is precisely the reason. Once I've commented out this above, buttons stopped from disappearing when theme changes (and by theme I mean even a background change). Go figure!

dipeshmsft commented 2 weeks ago

@danielkornev, this is actually a known thing. And seeing the code you have added, I am assuming you are using the WPF Gallery application.

When we use WindowChrome, we can extend the WPF's content into the non-client area of a window, allowing us to use the whole area of the window. However, by setting the CaptionHeight property of the WindowChrome, we allow a rectangular area at the top to be treated as a NON_CLIENT area and receives WM_NCHITTEST messages, irrespective of the Window's background or other elements drawn in that area (until specified otherwise using WindowChrome.IsHitTestVisibleInChrome property).

When we set UserAeroCaptionButtons the area which generally behaves as caption buttons, keep doing that, and that's why you are seeing the snap layout and other cursor movements being registered.

Now, regarding the button's not being visibile - in my understanding, Window's background is painted over the window's CompositionTarget, and anything drawn by the OS (like the default title bar buttons) is painted over by Window's background and other elements. In this case, when you said even on background change the buttons are getting invisible, I think the reason for this is that the window's background is opaque.

The code that you are referring above, it was added to show custom title bar buttons when we are in HighContrast mode. In this mode, the background of Window in opaque and we are not able to see the button's drawn by OS.

I hope I have not misunderstood your queries. I am curious about the hot-reload scenario though. I haven't explored the working on CompositionTarget, WindowChrome with Hot-Reload specifically.

danielkornev commented 2 weeks ago

My point is that if you want to force specific Fluent theme (e.g., Light), you can avoid using this code from WPF Gallery app and then window management buttons will look ok:

private void SystemEvents_UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
{
    Dispatcher.Invoke(() =>
    {
        if (SystemParameters.HighContrast)
        {
            MinimizeButton.Visibility = Visibility.Visible;
            MaximizeButton.Visibility = Visibility.Visible;
            CloseButton.Visibility = Visibility.Visible;
        }
        else
        {
            MinimizeButton.Visibility = Visibility.Collapsed;
            MaximizeButton.Visibility = Visibility.Collapsed;
            CloseButton.Visibility = Visibility.Collapsed;
        }
    });
}
dipeshmsft commented 1 week ago

The code above is to deal with HighContrast theme's only, also when backdrop is disabled using the switch. There are some other scenario's where the code may not work ( like when transparency effects are switched off ). I am taking a look at those scenarios.