microsoft / microsoft-ui-xaml

WinUI: a modern UI framework with a rich set of controls and styles to build dynamic and high-performing Windows applications.
MIT License
6.38k stars 683 forks source link

Proposal: Support setting/changing Application.Current.RequestedTheme after app constructor (rather than throw exception) #4474

Open billhenn opened 3 years ago

billhenn commented 3 years ago

Describe the bug Setting Application.Current.RequestedTheme at run-time to alter the app's light/dark theme throws this exception:

System.NotSupportedException
  HResult=0x80131515
  Message=Specified method is not supported.
  Source=WinRT.Runtime
  StackTrace:
   at WinRT.ExceptionHelpers.ThrowExceptionForHR(Int32 hr)
   at ABI.Microsoft.UI.Xaml.IApplication.global::Microsoft.UI.Xaml.IApplication.set_RequestedTheme(ApplicationTheme value)
   at Microsoft.UI.Xaml.Application.set_RequestedTheme(ApplicationTheme value)
   at TestApp.RootWindow.OnThemeMenuFlyoutItemClick(Object sender, RoutedEventArgs e) in S:\Code\TestApp\Views\RootWindow.xaml.cs:line 71
   at ABI.Microsoft.UI.Xaml.RoutedEventHandler.<>c__DisplayClass10_0.<Do_Abi_Invoke>b__0(RoutedEventHandler invoke)
   at WinRT.ComWrappersSupport.MarshalDelegateInvoke[T](IntPtr thisPtr, Action`1 invoke)
   at ABI.Microsoft.UI.Xaml.RoutedEventHandler.Do_Abi_Invoke(IntPtr thisPtr, IntPtr sender, IntPtr e)

Setting the RequestedTheme property on the root Grid in my Window succeeds. But I want to change it app-wide, not only on this window.

Steps to reproduce the bug

Use this code to reproduce:

Application.Current.RequestedTheme = ApplicationTheme.Dark;

Expected behavior

No exception and the app theme should toggle to the selected theme.

Version Info

NuGet package version: [Microsoft.WinUI 3.0.0-preview4.210210.4]

Windows app type: UWP Win32
Yes
Windows 10 version Saw the problem?
Insider Build (xxxxx)
October 2020 Update (19042)
May 2020 Update (19041) Yes
November 2019 Update (18363)
May 2019 Update (18362)
October 2018 Update (17763)
April 2018 Update (17134)
Fall Creators Update (16299)
Creators Update (15063)
Device form factor Saw the problem?
Desktop Yes
Xbox
Surface Hub
IoT
ranjeshj commented 3 years ago

@llongley @codendone as FYI

codendone commented 3 years ago

This is currently by design. It is a little hidden down in the Remarks section of the documentation:

The theme can only be set when the app is started, not while it's running. Attempting to set RequestedTheme while the app is running throws an exception (NotSupportedException for Microsoft .NET code). ...

You can change specific theme values at run-time after Application.RequestedTheme is applied, if you use the FrameworkElement.RequestedTheme property and sets values on specific elements in the UI.

The error message could certainly be better -- a problem we have for all errors right now. This error will be much easier to understand when the code is finally open source, since you'd then be able to see this code comment at the source of the exception:

    // RequestedTheme cannot be set after app.xaml has been loaded.
billhenn commented 3 years ago

@codendone Thanks, I apologize I didn't see that remark before. The more descriptive error message would certainly help prevent confusion on this, and as you said, error messages really need to be more detailed across the board.

I would really ask that you improve this to support app-wide theme changes without having to restart the app. All popular modern apps support instant live theme changes between variations of light and dark themes without having to restart, and it's kind of an expected app capability at this point. Not having support for that in a "modern" UI platform is very disappointing. Thanks for listening!

codendone commented 3 years ago

I believe this restriction exists simply because way back in Windows 8 when the RequestedTheme support was implemented there weren't available resources to also support dynamic changes. Adding that support could be done in the future, so I'm marking this as a future feature proposal.

billhenn commented 3 years ago

Thank you @codendone, much appreciated.

jtorjo commented 2 years ago

Guys, can you please please please add just a tad of description in the exception, when you throw it? I wanted to bang my head against the wall on this one (Until i figured out i should set that in the constructor). The (insanely generic) exception is beyond annoying.

ajsuydam commented 4 months ago

A potentially dumb question: if runtime switching isn't supported, how does task manager do it? Or notepad? Both support runtime switching, and they're both using WinUI 3.

MartyIX commented 3 months ago

The proposal: 💯


This is a sort of workaround for the missing feature. One can call SetTheme("Light", window.Content) to enforce theme changes for all ComboBoxes (simplification). However, it modifies only visible ComboBoxes as VisualTreeHelper (doc) is used.

Not sure how to change all ComboBoxes in the window (being visible in the UI tree or not).

public static void SetTheme(string appTheme, Microsoft.UI.Xaml.DependencyObject obj)
{
    Microsoft.UI.Xaml.ElementTheme elementTheme = appTheme switch
    {
        "Light" => Microsoft.UI.Xaml.ElementTheme.Light,
        "Dark" => Microsoft.UI.Xaml.ElementTheme.Dark,
        "System" => Microsoft.UI.Xaml.ElementTheme.Default,
        _ => throw new NotSupportedException($"Invalid application theme value '{appTheme}'."),
    };

    foreach (Microsoft.UI.Xaml.Controls.ComboBox comboBox in FindDescendants<Microsoft.UI.Xaml.Controls.ComboBox>(obj))
    {
        comboBox.RequestedTheme = elementTheme;
    }
}

public static IEnumerable<T> FindDescendants<T>(Microsoft.UI.Xaml.DependencyObject dobj)
    where T : Microsoft.UI.Xaml.DependencyObject
{
    int count = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetChildrenCount(dobj);
    for (int i = 0; i < count; i++)
    {
        Microsoft.UI.Xaml.DependencyObject element = Microsoft.UI.Xaml.Media.VisualTreeHelper.GetChild(dobj, i);
        if (element is T t)
            yield return t;

        foreach (T descendant in FindDescendants<T>(element))
            yield return descendant;
    }
}
MartyIX commented 2 months ago

This is a more complete code one can use: https://github.com/dotnet/maui/issues/21042#issuecomment-2325140606

Lightczx commented 1 day ago

place this line of code inside your App's OnLaunched method to set m_isRequestedThemeSettable to true

*(bool*)(((IWinRTObject)this).NativeObject.As<IUnknownVftbl>(IApplicationIID).ThisPtr + 0x118U) = true;

IApplicationIID is defined like below.

private static ref readonly Guid IApplicationIID
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    get => ref MemoryMarshal.AsRef<Guid>([231, 244, 168, 6, 70, 17, 175, 85, 130, 13, 235, 213, 86, 67, 176, 33]);
}

(Tested and works in WASDK 1.6.3)


Additional Context: https://github.com/microsoft/WindowsAppSDK/discussions/4710#discussioncomment-10741712

ghost1372 commented 1 day ago

place this line of code inside your App's OnLaunched method to set m_isRequestedThemeSettable to true

(bool)(((IWinRTObject)this).NativeObject.As(IApplicationIID).ThisPtr + 0x118U) = true; (Tested and works in WASDK 1.6.3)

Additional Context: microsoft/WindowsAppSDK#4710 (comment)

what is IApplicationIID ? Image

Lightczx commented 1 day ago

what is IApplicationIID ?

@ghost1372 My bad, you can find through looking the code decompiled by VS.

There is a Make___objRef_global__Microsoft_UI_Xaml_IApplication method in Application

Image

And finally get this

Image

ghost1372 commented 18 hours ago

what is IApplicationIID ?

@ghost1372 My bad, you can find through looking the code decompiled by VS.

There is a Make___objRef_global__Microsoft_UI_Xaml_IApplication method in Application

Image

And finally get this

Image

thank you now its working fine

complete code:

unsafe
{
    *(bool*)(((IWinRTObject)this).NativeObject.As<IUnknownVftbl>(IID).ThisPtr + 0x118U) = true;
}

public static ref readonly Guid IID
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    get
    {
        return ref Unsafe.As<byte, Guid>(ref MemoryMarshal.GetReference((ReadOnlySpan<byte>)new byte[16]
        {
            231, 244, 168, 6, 70, 17, 175, 85, 130, 13,
            235, 213, 86, 67, 176, 33
        }));
    }
}