.NET MAUI Blazor windows

.NET MAUI Blazor windows

The .NET MAUI documentation details the creation of windows and states that .NET MAUI apps support having multiple windows, but what it omits is explaining how to access this functionality in the context of a .NET MAUI Blazor app.

Fundamentally, a .NET MAUI Blazor app is just a normal MAUI app with a single page (MainPage.xaml) that has a BlazorWebView which then actually hosts the Razor content. That means that any newly created window will still have the same MainPage.xaml as its content and it is up to the Blazor-side to present the correct content.

.NET MAUI app startup

First, a little background about how a .NET MAUI Blazor application starts (in the boilerplate project):

  1. Program.cs

    The entry point is CreateMauiApp() which references App by using it as a type argument in the generic method UseMauiApp().

  2. App.xaml(.cs) (derives from Microsoft.Maui.Controls.Application)

    Sets its MainPage property (inherited from its base class) to a new instance of MainPage. According to the .NET MAUI documentation on windows, setting the MainPage property of an Application will cause a Window to be created.

  3. MainPage.xaml(.cs)

    Contains a BlazorWebView that references the static wwwroot/index.html as the HTML boilerplate/skeleton and adds the Main type as the only RootComponent of the BlazorWebView.

  4. Main.razor(.cs)

    Contains a Router component that, according to the Blazor routing and navigation documentation, scans the specified AppAssembly to "gather route information for the app's components that have a RouteAttribute".

So we need to provide a way for Razor components to create a new MAUI window with the possibility to specify an URI that references a specific page within the AppAssembly of the Router.

Implementation

The following details the implementation from the bottom up in terms of the startup described above:

Main.razor.cs (create)

Firstly, we need to provide a way to specify a page other that the index (i.e. the page that has the / route) in the Main razor component. So, we add a parameter to specify the URI of the page we want to view and navigate to that URI right after initializing the component:

+public partial class Main : ComponentBase
+{
+    [Parameter]
+    public string PageUri { get; set; } = string.Empty;
+
+    protected override void OnInitialized()
+    {
+        if (!string.IsNullOrEmpty(PageUri))
+            Nav.NavigateTo(PageUri);
+    }
+}

MainPage.xaml

Remove the <BlazorWebView.RootComponents> element from the XAML completly. We will populate this collection in the code-behind instead:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:RecEx.App"
             x:Class="RecEx.App.MainPage"
             BackgroundColor="{DynamicResource PageBackgroundColor}">

    <BlazorWebView x:Name="blazorWebView" HostPage="wwwroot/index.html">
-        <BlazorWebView.RootComponents>
-            <RootComponent Selector="#app" ComponentType="{x:Type local:Main}" Parameters="{Binding Path=RootComponentParameters}" />
-        </BlazorWebView.RootComponents>
    </BlazorWebView>

</ContentPage>

MainPage.xaml.cs (create)

The items in the RootComponents collection provide a way to pass parameters to the specified component in the form of a IDictionary<string, object>. So we add an optional argument to the constructor to expose this to callers:

+public partial class MainPage : ContentPage
+{
+    public MainPage(IDictionary<string, object> rootComponentParameters = null)
+    {
+        InitializeComponent();
+
+        blazorWebView.RootComponents.Add(new()
+        {
+            ComponentType = typeof(Main),
+            Parameters = rootComponentParameters,
+            Selector = "#app"
+        });
+    }
+}

App.xaml.cs

Finally, we add a static class that provides extension methods to open the window:

public partial class App : Application
{
    public App()
    {
        InitializeComponent();

        MainPage = new MainPage();
    }
}

+public static class ApplicationExtensions
+{
+    public static Window OpenWindowFromPage(this Application app, Page page)
+    {
+        var windowObj = new Window(page);
+        windowObj.Parent = app.Windows[0];
+        app.OpenWindow(windowObj);
+        return windowObj;
+    }
+    public static void OpenInNewWindow(this Application app, string pageUri)
+    {
+        var windowPage = new MainPage(new Dictionary<string, object>()
+        {
+            {nameof(Main.PageUri), pageUri }
+        });
+        app.OpenWindowFromPage(windowPage);
+    }
+
+    public static void OpenInNewWindow(this ComponentBase component, string pageUri) =>
+        Application.Current.OpenInNewWindow(pageUri);
+}

Usage

To open a Razor page in a new MAUI window, simply call the OpenInNewWindow extension method from any component:

@page "/"

<h1>.NET MAUI Blazor window demo</h1>
<button type="button" @onclick="() => OpenCounterWindow()">Open counter window</button>

@code {
    protected void OpenCounterWindow()
    {
        this.OpenInNewWindow("/counter");
    }
}

Limitations/caveats

  1. There does not seem to be any support for modal windows (yet?). I've experimented with the Parent property on Window, but to no avail.
  2. In my tests, I could always briefly see the content of the Index.razor page because the Router loads it by default. In addition to being a cosmetic issue, it also means that any code on the Index page is executed before the specified page is loaded.