Blazor Hybrid (MAUI) Application - Window Customization at Runtime (when Targeting Windows)

Humberto Cruz-Oyola Jr 21 Reputation points
2022-07-05T15:33:53.87+00:00

Quick Background: I am writing this from the standpoint of a Blazor web developer who wants to use an RCL to create the UI for a cross-platform application. This question, however, is specific to running it on Windows.

The Issue

After a couple of weeks of research and trying various recommendations and things that I thought might work, I am running into a lot of issues with customizing the main application window when targeting Windows. The primary thing I am trying to do is to change the background color of the top bar when running the application in Windows. I've successfully managed to change the background color of the TitleBar (using the Windows Platform-specific App.xaml.cs), however, this only works on Windows 11 since the TitleBar property is null otherwise. I am looking for a way to either add my own TitleBar. I've tried a few things, including using the ConfigureLifecycleEvents in MauiProgram.cs, but I don't really understand how to use the SetTitleBar method; as well as adding the <DataTemplate x:Key="MauiAppTitleBarContainerTemplate"> which requires me to understand how to manipulate it (add draggable area, set the background for the custom title bar, as well as the TitleBar.ButtonBackgroundColor or what the correct thing to do is for the control buttons for the window); and have played around with the ExtendsContentIntoTitleBar and wrapping the main page in a NavigationPage, which just gives me a big empty space between the TitleBar and the BlazorWebView.

I would really love some resources for fully customizing the TitleBar, primarily focused on changing the TitleBar background color (and button background color) at runtime. Below is the setup of the Platform-specific (Window) App.xaml.cs that works with Win 11, but not Win 10.

// Platforms/Windows/App.xaml.cs  
  
public partial class App : MauiWinUIApplication  
{  
    public App()  
    {  
        InitializeComponent();  
  
        WindowHandler.Mapper.AppendToMapping(nameof(IWindow), (handler, view) =>  
        {  
            var width = 1420;  
            var height = 838;  
  
            var nativeWindow = handler.PlatformView;  
            nativeWindow.Activate();  
  
            var hWnd = WindowNative.GetWindowHandle(nativeWindow);  
            var windowId = Win32Interop.GetWindowIdFromWindow(hWnd);  
            var appWindow = AppWindow.GetFromWindowId(windowId);  
  
            var preferencesState = handler.MauiContext.Services.GetService<PreferencesState>();  
            var themeManager = handler.MauiContext.Services.GetService<IThemeManager>();  
  
            // The TitleBar is null when not Windows 11 (well...at least on Windows 10 machines)  
            if(appWindow.TitleBar is not null)  
            {  
                var activeTheme = themeManager[preferencesState.ActiveTheme];  
                var color = ((Color)activeTheme ["BarBackgroundColor"]).ToWinUiColor();  
  
                appWindow.TitleBar.BackgroundColor = color;  
                appWindow.TitleBar.ButtonBackgroundColor = color; // Changes the background color of the control buttons  
                // This forces the icon area to refresh as well (so the background color applies without needing to click off of the app then back on)  
                appWindow.TitleBar.IconShowOptions = IconShowOptions.ShowIconAndSystemMenu;  
            }  
  
            // Size and position the window at the center of the current display  
            var screenWidth = DeviceDisplay.MainDisplayInfo.Width;  
            var screenHeight = DeviceDisplay.MainDisplayInfo.Height;  
  
               appWindow.MoveAndResize(  
                new Windows.Graphics.RectInt32(  
                    // Move the top left corner so that the window is centered on the main display  
                    (int)(screenWidth / 2 - width / 2),  
                    (int)(screenHeight / 2 - height / 2),  
                    // Set the height and width  
                    width, height  
                )  
            );  
        });  
    }  
  
    protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();  
}  

Below is more information about how the app is setup, but it can be skipped if the above makes sense. I will have a code sample in GitHub shortly (I will be remaking it on my stream today). I'll update the question with the repo once I am finished.

More Information - The Setup

The UI portion of the application--a Razor Class Library (RCL)--has a theme switcher with multiple themes (not just light and dark). I have a shared (Dependency Injected) Preferences object that holds the value of the current theme. It implements INotifyPropertyChanged so changes to the currently selected theme can be watched.

In the RCL, the CSS contains a :root element with all of the CSS variables used for coloring the elements, along with separate rules to change the values of the CSS variables based on the class on the body element. When the theme changes, a class is added to the body element, so those rules are applied and the color/styling changes. I think this is a pretty standard approach for theming in a web application, although there could be smarter approaches (swapping out a CSS file in the header, for example).

For the Maui application: I have separate Fonts.xaml, Colors.xaml, and Styles.xaml resource dictionaries that are merged in App.xaml. I have the themes defined as classes that implement ITheme, which just lists the properties required for a theme class, along with an enum that has an extension method for getting the associated ITheme implementation. The enum is the same on that is used for controling the theme switching in the Blazor components. I also have one ThemeResourceDictionary.xaml that will be the ResourceDictionary wrapper for a given ITheme:

ThemeResourceDictionary.xaml.cs:

public partial class ThemeResourceDictionary : ResourceDictionary, ITheme  
{  
    public ThemeResourceDictionary(ITheme theme)  
    {  
        var myProps = GetType().GetProperties().Where(x => x.PropertyType == typeof(Color));  
        foreach (var curProp in theme.GetType().GetProperties().Where(x => x.PropertyType == typeof(Color)))  
        {  
            Add(curProp.Name, curProp.GetValue(theme));  
            var myProp = myProps.Single(x => x.Name == curProp.Name);  
            myProp.SetValue(this, curProp.GetValue(theme));  
        }  
  
        InitializeComponent();  
    }  
  
    public Color PageBackgroundColor { get; private set; }  
    public Color PrimaryTextColor { get; private set; }  
    public Color BarBackgroundColor { get; private set; }  
    public Color BarTextColor { get; private set; }  
    public Color ButtonBackgroundColor { get; private set; }  
}  
  

I also have a ThemeManager.cs that is dependency injected that just keeps track of all of the themes that have been added (the add is shown in the App.xaml.cs below.) In the main App.xaml file, I removed all of the contents and populated the merged dictionaries in the code-behind.

App.xaml.cs

public partial class App : Application, IDisposable  
{  
    private readonly IThemeManager _themeManager;  
    private readonly PreferencesState _preferencesState;  
  
    public App(IThemeManager themeManager, PreferencesState preferencesState)  
    {  
        _themeManager = themeManager;  
        _preferencesState = preferencesState;  
  
        // Add the colors first  
        Resources.MergedDictionaries.Add(themeManager.Colors);  
        // Then the active theme  
        ApplyTheme(_preferencesState.ActiveTheme);  
        // Then the styles to make sure all of the resources are available by this time  
        Resources.MergedDictionaries.Add(new Resources.Styles.Styles());  
  
        InitializeComponent();  
  
        MainPage = new MainPage();  
  
        _preferencesState.PropertyChanged += HandlePreferenceChanged;  
    }  
  
    private void HandlePreferenceChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)  
    {  
        if (e.PropertyName == nameof(PreferencesState.ActiveTheme))  
        {  
            ApplyTheme(_preferencesState.ActiveTheme);  
        }  
    }  
  
    protected override Window CreateWindow(IActivationState activationState)  
    {  
        var window = base.CreateWindow(activationState);  
        window.Title = "Theming Example";  
  
        return window;  
    }  
  
    private void ApplyTheme(ColorTheme theme)  
    {  
        var resDict = Current.Resources.MergedDictionaries;  
        var curTheme = resDict.GetActiveThemeResource();  
        if (curTheme is not null && theme.Is(curTheme))  
        {  
            // The theme is not changing  
            return;  
        }  
  
        // Remove the current theme and add the new theme resources  
        if (curTheme is not null) { resDict.Remove(curTheme); }  
        var updateTheme = _themeManager[theme];  
        resDict.Add(updateTheme);  
#if WINDOWS  
        // Apply the colors to the app window for Windows  
        WindowHelpers.ApplyColorTheme(updateTheme);  
#endif  
    }  
  
    public void Dispose()  
    {  
        _preferencesState.PropertyChanged -= HandlePreferenceChanged;  
    }  
}  

The WindowHelpers is specific to Windows Platform and will perform the same logic, that is: get the AppWindow and change the TitleBar.BackgroundColor and TitleBar.ButtonBackgroundColor to the active theme dictionary (from the ThemeManager) if TitleBar is not null.

Blazor
Blazor
A free and open-source web framework that enables developers to create web apps using C# and HTML being developed by Microsoft.
1,662 questions
.NET MAUI
.NET MAUI
A Microsoft open-source framework for building native device applications spanning mobile, tablet, and desktop.
3,902 questions
Windows 11
Windows 11
A Microsoft operating system designed for productivity, creativity, and ease of use.
10,668 questions
{count} votes

Accepted answer
  1. Castorix31 86,971 Reputation points
    2022-07-06T12:56:01.577+00:00

    It is not a bug on Windows 10
    It is documented at : Title bar customization

    ("Title bar customization APIs are currently supported on Windows 11 only. ...")

    On Windows 10, it is done with ExtendsContentIntoTitleBar/SetTitleBar + a Manifest
    (I had uploaded a basic sample with WinUI 3 at WinUI3_CustomCaption )

    1 person found this answer helpful.

0 additional answers

Sort by: Most helpful

Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.