diff --git a/README.md b/README.md
index de33735..cbf457b 100644
--- a/README.md
+++ b/README.md
@@ -119,8 +119,8 @@ It is also possible to configure these values through `appsettings.json` like so
}
```
> [!NOTE]
-> The `Window` object itself is also made available inside of the DI container by injecting `BlazorDesktopWindow`, so you can access all properties on it by using the inject Razor keyword or requesting it through the constructor of a class added as a service.
-> The `BlazorDesktopWindow` inherits from the WPF `Window` class, as such you use WPF apis to manipulate it. WPF documentation for the Window class can be found [here](https://learn.microsoft.com/en-us/dotnet/api/system.windows.window?view=windowsdesktop-9.0).
+> The main window can be accessed through the `IWindowManager` service available in the DI container. Use `IWindowManager.MainWindow` to get a handle to it.
+> The underlying `BlazorDesktopWindow` inherits from the WPF `Window` class, as such you use WPF apis to manipulate it. WPF documentation for the Window class can be found [here](https://learn.microsoft.com/en-us/dotnet/api/system.windows.window?view=windowsdesktop-9.0).
> Examples of usage can be found below.
## Custom Window Chrome & Draggable Regions
@@ -153,14 +153,14 @@ Using the base template, if you were to edit `MainLayout.razor` and add a `-webk
```
The top bar becomes draggable, applying the `-webkit-app-region: drag;` property to anything will make it able to be used to drag the window.
-In terms of handling things such as the close button, you can inject the Window into any page and interact from it there.
+In terms of handling things such as the close button, you can inject `IWindowManager` into any page and interact with the main window from there.
Here is an example changing `MainLayout.razor`:
```razor
-@using BlazorDesktop.Wpf
+@using BlazorDesktop.Services
@inherits LayoutComponentBase
-@inject BlazorDesktopWindow window
+@inject IWindowManager WindowManager
@@ -181,7 +181,7 @@ Here is an example changing `MainLayout.razor`:
@code {
void CloseWindow()
{
- window.Close();
+ WindowManager.MainWindow.NativeWindow?.Close();
}
}
```
@@ -190,23 +190,128 @@ To support fullscreen mode, you should also hide your custom window chrome when
## Changing Window Properties During Startup
It is possible to customize window startup behaviors for Blazor Desktop apps. As an example base setup you could do the following:
-Using the base template, if you were to edit `MainLayout.razor` and inject the `BlazorDesktopWindow` you can have the window be maximized on launch using Blazor's `OnInitialized` lifecycle method:
+Using the base template, if you were to edit `MainLayout.razor` and inject `IWindowManager` you can have the window be maximized on launch using Blazor's `OnInitialized` lifecycle method:
```razor
-@using BlazorDesktop.Wpf
+@using BlazorDesktop.Services
@using System.Windows
@inherits LayoutComponentBase
-@inject BlazorDesktopWindow window
+@inject IWindowManager WindowManager
...
@code {
protected override void OnInitialized()
{
- window.WindowState = WindowState.Maximized;
+ if (WindowManager.MainWindow.NativeWindow is { } window)
+ {
+ window.WindowState = WindowState.Maximized;
+ }
}
}
```
+## Multi-Window Support
+Blazor Desktop supports opening multiple windows, each with its own independent Blazor component tree. There are two ways to create child windows: a **service-based API** for programmatic control and a **component-based API** for declarative Razor usage.
+
+### Service-based API
+Inject `IWindowManager` and call `OpenAsync` to open a new window programmatically:
+
+```razor
+@using BlazorDesktop.Services
+@inject IWindowManager WindowManager
+
+
+
+@code {
+ private async Task OpenSettings()
+ {
+ var handle = await WindowManager.OpenAsync(options =>
+ {
+ options.Title = "Settings";
+ options.Width = 600;
+ options.Height = 400;
+ });
+
+ // React when the window is closed (user clicks X or closed programmatically)
+ handle.Closed += (sender, args) =>
+ {
+ // Handle cleanup
+ };
+ }
+}
+```
+
+You can also close a window programmatically:
+```csharp
+await WindowManager.CloseAsync(handle);
+```
+
+### Component-based API
+Use the `` component to open and close windows declaratively. The window opens when the component is rendered and closes when it is removed from the render tree:
+
+```razor
+@using BlazorDesktop.Components
+
+
+
+@if (showSettings)
+{
+
+}
+
+@code {
+ private bool showSettings = false;
+}
+```
+
+The `OnClosed` callback fires when the user closes the window (e.g. clicks the X button), allowing you to keep your state in sync.
+
+### Window Options
+Both APIs accept the same set of window options:
+
+| Option | Type | Default | Description |
+|--------|------|---------|-------------|
+| `Title` | `string?` | App name | Window title |
+| `Width` | `int?` | `1366` | Window width in pixels |
+| `Height` | `int?` | `768` | Window height in pixels |
+| `MinWidth` | `int?` | `0` | Minimum width |
+| `MinHeight` | `int?` | `0` | Minimum height |
+| `MaxWidth` | `int?` | unlimited | Maximum width |
+| `MaxHeight` | `int?` | unlimited | Maximum height |
+| `Frame` | `bool?` | `true` | Use standard window frame |
+| `Resizable` | `bool?` | `true` | Allow resizing |
+| `Icon` | `string?` | `favicon.ico` | Icon path (relative to `wwwroot`) |
+
+### IWindowManager Reference
+The `IWindowManager` service is available in the DI container and provides:
+
+| Member | Description |
+|--------|-------------|
+| `MainWindow` | Handle to the main application window |
+| `Windows` | List of all currently open window handles |
+| `OpenAsync(...)` | Open a new window with a Blazor component |
+| `OpenAsync(Type, ...)` | Open a new window with a component type |
+| `CloseAsync(handle)` | Close a child window |
+| `WindowOpened` | Event fired when any window is opened |
+| `WindowClosed` | Event fired when any window is closed |
+
+### Behavior Notes
+- **Child window ownership**: All child windows are owned by the main window, so they stack and minimize together following standard WPF behavior.
+- **Shutdown policy**: Closing the main window closes the entire application and all child windows. Closing a child window only removes that window.
+- **DI scoping**: Each window runs its own `BlazorWebView` which creates an independent Blazor DI scope. Scoped services are per-window; singleton services are shared across all windows.
+- **Thread safety**: `IWindowManager` is safe to call from any thread. All WPF operations are automatically marshaled to the UI thread.
+- **Component parameters**: You can pass parameters to child window root components via the `parameters` argument on `OpenAsync` or the `Parameters` property on ``.
+
+### Full Example
+The `BlazorDesktop.Sample` project includes a working multi-window demo. Navigate to the **Multi-Window** page to try both APIs. The sample shows:
+
+- Opening child windows with the service-based API via `IWindowManager.OpenAsync()`
+- Toggling child windows with the component-based API via ``
+- Each child window running its own independent counter
+- Tracking the number of open windows in real time
+
## Issues
Under the hood, Blazor Desktop uses WebView2 which has limitations right now with composition. Due to this, if you disable the window border through the `Window.UseFrame(false)` API, the top edge of the window is unusable as a resizing zone for the window. However all the other corners and edges work.
diff --git a/src/BlazorDesktop.Sample/Components/Layout/NavMenu.razor b/src/BlazorDesktop.Sample/Components/Layout/NavMenu.razor
index bf62b7a..1f46f71 100644
--- a/src/BlazorDesktop.Sample/Components/Layout/NavMenu.razor
+++ b/src/BlazorDesktop.Sample/Components/Layout/NavMenu.razor
@@ -29,5 +29,11 @@
Weather
This is running in its own window with an independent Blazor component tree.
+
+
+
+
Counter
+
Current count: @currentCount
+
+
+
+
+
+ Window opened at @openedAt.ToString("HH:mm:ss")
+
+
+
+@code {
+ private int currentCount = 0;
+ private DateTime openedAt = DateTime.Now;
+
+ private void IncrementCount()
+ {
+ currentCount++;
+ }
+}
diff --git a/src/BlazorDesktop.Sample/Components/Pages/MultiWindow.razor b/src/BlazorDesktop.Sample/Components/Pages/MultiWindow.razor
new file mode 100644
index 0000000..b09bbfe
--- /dev/null
+++ b/src/BlazorDesktop.Sample/Components/Pages/MultiWindow.razor
@@ -0,0 +1,59 @@
+@* Licensed to the .NET Extension Contributors under one or more agreements. *@
+@* The .NET Extension Contributors licenses this file to you under the MIT license. *@
+@* See the LICENSE file in the project root for more information. *@
+
+@page "/multiwindow"
+@using BlazorDesktop.Components
+@using BlazorDesktop.Services
+@inject IWindowManager WindowManager
+
+
Multi-Window Demo
+
+
This page demonstrates opening child windows using both the service-based and component-based APIs.
+
+
Service-based API
+
Open windows programmatically via IWindowManager.
+
+
+
+
+
Component-based API
+
Toggle a window declaratively with the <DesktopWindow> component.
+
+
+@if (_showComponentWindow)
+{
+
+}
+
+
+
+
Open Windows
+
Currently tracking @WindowManager.Windows.Count window(s).
+
+@code {
+ private bool _showComponentWindow;
+
+ private async Task OpenServiceWindow()
+ {
+ var handle = await WindowManager.OpenAsync(options =>
+ {
+ options.Title = "Service Window";
+ options.Width = 500;
+ options.Height = 350;
+ });
+
+ handle.Closed += (_, _) => InvokeAsync(StateHasChanged);
+
+ StateHasChanged();
+ }
+
+ private void ToggleComponentWindow()
+ {
+ _showComponentWindow = !_showComponentWindow;
+ }
+}
diff --git a/src/BlazorDesktop/Components/DesktopWindow.cs b/src/BlazorDesktop/Components/DesktopWindow.cs
new file mode 100644
index 0000000..2891f31
--- /dev/null
+++ b/src/BlazorDesktop/Components/DesktopWindow.cs
@@ -0,0 +1,164 @@
+// Licensed to the .NET Extension Contributors under one or more agreements.
+// The .NET Extension Contributors licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using BlazorDesktop.Hosting;
+using BlazorDesktop.Services;
+using Microsoft.AspNetCore.Components;
+
+namespace BlazorDesktop.Components;
+
+///
+/// A Blazor component that manages a desktop window declaratively.
+/// When rendered, opens a new window. When removed from the render tree, closes it.
+///
+public sealed class DesktopWindow : ComponentBase, IAsyncDisposable
+{
+ [Inject]
+ private IWindowManager WindowManager { get; set; } = default!;
+
+ ///
+ /// Gets or sets the root component type for the window.
+ ///
+ [Parameter, EditorRequired]
+ public Type ComponentType { get; set; } = default!;
+
+ ///
+ /// Gets or sets the window title.
+ ///
+ [Parameter]
+ public string? Title { get; set; }
+
+ ///
+ /// Gets or sets the window width.
+ ///
+ [Parameter]
+ public int? Width { get; set; }
+
+ ///
+ /// Gets or sets the window height.
+ ///
+ [Parameter]
+ public int? Height { get; set; }
+
+ ///
+ /// Gets or sets the minimum window width.
+ ///
+ [Parameter]
+ public int? MinWidth { get; set; }
+
+ ///
+ /// Gets or sets the minimum window height.
+ ///
+ [Parameter]
+ public int? MinHeight { get; set; }
+
+ ///
+ /// Gets or sets the maximum window width.
+ ///
+ [Parameter]
+ public int? MaxWidth { get; set; }
+
+ ///
+ /// Gets or sets the maximum window height.
+ ///
+ [Parameter]
+ public int? MaxHeight { get; set; }
+
+ ///
+ /// Gets or sets whether the window uses a standard frame.
+ ///
+ [Parameter]
+ public bool? Frame { get; set; }
+
+ ///
+ /// Gets or sets whether the window is resizable.
+ ///
+ [Parameter]
+ public bool? Resizable { get; set; }
+
+ ///
+ /// Gets or sets the window icon path.
+ ///
+ [Parameter]
+ public string? Icon { get; set; }
+
+ ///
+ /// Gets or sets optional parameters to pass to the root component.
+ ///
+ [Parameter]
+ public IDictionary? Parameters { get; set; }
+
+ ///
+ /// Called when the window is closed (either programmatically or by the user).
+ ///
+ [Parameter]
+ public EventCallback OnClosed { get; set; }
+
+ private DesktopWindowHandle? _handle;
+ private bool _disposed;
+
+ ///
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (!firstRender)
+ {
+ return;
+ }
+
+ _handle = await WindowManager.OpenAsync(ComponentType, options =>
+ {
+ options.Title = Title;
+ options.Width = Width;
+ options.Height = Height;
+ options.MinWidth = MinWidth;
+ options.MinHeight = MinHeight;
+ options.MaxWidth = MaxWidth;
+ options.MaxHeight = MaxHeight;
+ options.Frame = Frame;
+ options.Resizable = Resizable;
+ options.Icon = Icon;
+ }, Parameters);
+
+ _handle.Closed += OnHandleClosed;
+ }
+
+ private async void OnHandleClosed(object? sender, EventArgs e)
+ {
+ if (_handle is not null)
+ {
+ _handle.Closed -= OnHandleClosed;
+ }
+
+ await InvokeAsync(async () =>
+ {
+ if (OnClosed.HasDelegate)
+ {
+ await OnClosed.InvokeAsync();
+ }
+ });
+ }
+
+ ///
+ public async ValueTask DisposeAsync()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+
+ if (_handle is not null)
+ {
+ _handle.Closed -= OnHandleClosed;
+
+ if (_handle.NativeWindow is not null)
+ {
+ await WindowManager.CloseAsync(_handle);
+ }
+
+ _handle = null;
+ }
+ }
+}
diff --git a/src/BlazorDesktop/Hosting/BlazorDesktopHostBuilder.cs b/src/BlazorDesktop/Hosting/BlazorDesktopHostBuilder.cs
index 4fb1c5e..4c6acb2 100644
--- a/src/BlazorDesktop/Hosting/BlazorDesktopHostBuilder.cs
+++ b/src/BlazorDesktop/Hosting/BlazorDesktopHostBuilder.cs
@@ -4,7 +4,6 @@
using System.Windows;
using BlazorDesktop.Services;
-using BlazorDesktop.Wpf;
namespace BlazorDesktop.Hosting;
@@ -123,7 +122,7 @@ private void InitializeDefaultServices()
Services.AddWpfBlazorWebView();
Services.AddSingleton();
Services.AddSingleton();
- Services.AddSingleton();
+ Services.AddSingleton();
Services.AddHostedService();
}
diff --git a/src/BlazorDesktop/Hosting/DesktopWindowHandle.cs b/src/BlazorDesktop/Hosting/DesktopWindowHandle.cs
new file mode 100644
index 0000000..cb7951d
--- /dev/null
+++ b/src/BlazorDesktop/Hosting/DesktopWindowHandle.cs
@@ -0,0 +1,52 @@
+// Licensed to the .NET Extension Contributors under one or more agreements.
+// The .NET Extension Contributors licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using BlazorDesktop.Wpf;
+
+namespace BlazorDesktop.Hosting;
+
+///
+/// A lightweight, thread-safe handle to a managed desktop window.
+///
+public sealed class DesktopWindowHandle
+{
+ ///
+ /// Gets the unique identifier for this window.
+ ///
+ public string Id { get; }
+
+ ///
+ /// Gets whether this is the main application window.
+ ///
+ public bool IsMainWindow { get; }
+
+ ///
+ /// Gets the native WPF window. Internal use only.
+ ///
+ internal BlazorDesktopWindow? NativeWindow { get; set; }
+
+ ///
+ /// Occurs when the window is closed.
+ ///
+ public event EventHandler? Closed;
+
+ ///
+ /// Creates a new .
+ ///
+ /// The unique window identifier.
+ /// Whether this is the main window.
+ internal DesktopWindowHandle(string id, bool isMainWindow)
+ {
+ Id = id;
+ IsMainWindow = isMainWindow;
+ }
+
+ ///
+ /// Raises the event.
+ ///
+ internal void OnClosed()
+ {
+ Closed?.Invoke(this, EventArgs.Empty);
+ }
+}
diff --git a/src/BlazorDesktop/Hosting/WindowOptions.cs b/src/BlazorDesktop/Hosting/WindowOptions.cs
new file mode 100644
index 0000000..dca2728
--- /dev/null
+++ b/src/BlazorDesktop/Hosting/WindowOptions.cs
@@ -0,0 +1,83 @@
+// Licensed to the .NET Extension Contributors under one or more agreements.
+// The .NET Extension Contributors licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace BlazorDesktop.Hosting;
+
+///
+/// Per-window configuration options.
+///
+public class WindowOptions
+{
+ ///
+ /// Gets or sets the window title.
+ ///
+ public string? Title { get; set; }
+
+ ///
+ /// Gets or sets the window height.
+ ///
+ public int? Height { get; set; }
+
+ ///
+ /// Gets or sets the window width.
+ ///
+ public int? Width { get; set; }
+
+ ///
+ /// Gets or sets the minimum window height.
+ ///
+ public int? MinHeight { get; set; }
+
+ ///
+ /// Gets or sets the minimum window width.
+ ///
+ public int? MinWidth { get; set; }
+
+ ///
+ /// Gets or sets the maximum window height.
+ ///
+ public int? MaxHeight { get; set; }
+
+ ///
+ /// Gets or sets the maximum window width.
+ ///
+ public int? MaxWidth { get; set; }
+
+ ///
+ /// Gets or sets whether the window uses a standard frame.
+ ///
+ public bool? Frame { get; set; }
+
+ ///
+ /// Gets or sets whether the window is resizable.
+ ///
+ public bool? Resizable { get; set; }
+
+ ///
+ /// Gets or sets the window icon path.
+ ///
+ public string? Icon { get; set; }
+
+ ///
+ /// Creates a from the existing keys.
+ ///
+ /// The configuration.
+ /// A populated .
+ public static WindowOptions FromConfiguration(IConfiguration config)
+ {
+ return new WindowOptions
+ {
+ Title = config.GetValue(WindowDefaults.Title),
+ Height = config.GetValue(WindowDefaults.Height),
+ Width = config.GetValue(WindowDefaults.Width),
+ MinHeight = config.GetValue(WindowDefaults.MinHeight),
+ MinWidth = config.GetValue(WindowDefaults.MinWidth),
+ MaxHeight = config.GetValue(WindowDefaults.MaxHeight),
+ MaxWidth = config.GetValue(WindowDefaults.MaxWidth),
+ Frame = config.GetValue(WindowDefaults.Frame),
+ Resizable = config.GetValue(WindowDefaults.Resizable),
+ Icon = config.GetValue(WindowDefaults.Icon)
+ };
+ }
+}
diff --git a/src/BlazorDesktop/Services/BlazorDesktopService.cs b/src/BlazorDesktop/Services/BlazorDesktopService.cs
index 1e81107..d8a7002 100644
--- a/src/BlazorDesktop/Services/BlazorDesktopService.cs
+++ b/src/BlazorDesktop/Services/BlazorDesktopService.cs
@@ -2,6 +2,7 @@
// The .NET Extension Contributors licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using BlazorDesktop.Hosting;
using BlazorDesktop.Wpf;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
@@ -69,7 +70,15 @@ public Task StopAsync(CancellationToken cancellationToken)
private void ApplicationThread()
{
var app = _services.GetRequiredService();
- var mainWindow = _services.GetRequiredService();
+ var config = _services.GetRequiredService();
+ var environment = _services.GetRequiredService();
+ var rootComponents = _services.GetRequiredService();
+ var options = WindowOptions.FromConfiguration(config);
+
+ var mainWindow = new BlazorDesktopWindow(_services, environment, options, rootComponents);
+
+ var windowManager = (WindowManager)_services.GetRequiredService();
+ windowManager.RegisterMainWindow(mainWindow, app.Dispatcher);
app.Startup += OnApplicationStartup;
app.Exit += OnApplicationExit;
diff --git a/src/BlazorDesktop/Services/IWindowManager.cs b/src/BlazorDesktop/Services/IWindowManager.cs
new file mode 100644
index 0000000..7a15b7b
--- /dev/null
+++ b/src/BlazorDesktop/Services/IWindowManager.cs
@@ -0,0 +1,58 @@
+// Licensed to the .NET Extension Contributors under one or more agreements.
+// The .NET Extension Contributors licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using BlazorDesktop.Hosting;
+using Microsoft.AspNetCore.Components;
+
+namespace BlazorDesktop.Services;
+
+///
+/// Manages multiple desktop windows. Safe to call from any thread.
+///
+public interface IWindowManager
+{
+ ///
+ /// Gets the main application window handle.
+ ///
+ DesktopWindowHandle MainWindow { get; }
+
+ ///
+ /// Gets all currently open window handles.
+ ///
+ IReadOnlyList Windows { get; }
+
+ ///
+ /// Opens a new window hosting the specified component.
+ ///
+ /// The root Blazor component for the window.
+ /// Optional configuration for window options.
+ /// Optional parameters to pass to the root component.
+ /// A handle to the opened window.
+ Task OpenAsync(Action? configure = null, IDictionary? parameters = null) where TComponent : IComponent;
+
+ ///
+ /// Opens a new window hosting the specified component type.
+ ///
+ /// The root Blazor component type for the window.
+ /// Optional configuration for window options.
+ /// Optional parameters to pass to the root component.
+ /// A handle to the opened window.
+ Task OpenAsync(Type componentType, Action? configure = null, IDictionary? parameters = null);
+
+ ///
+ /// Closes the specified window.
+ ///
+ /// The window handle to close.
+ Task CloseAsync(DesktopWindowHandle handle);
+
+ ///
+ /// Occurs when a window is opened.
+ ///
+ event EventHandler? WindowOpened;
+
+ ///
+ /// Occurs when a window is closed.
+ ///
+ event EventHandler? WindowClosed;
+}
diff --git a/src/BlazorDesktop/Services/WindowManager.cs b/src/BlazorDesktop/Services/WindowManager.cs
new file mode 100644
index 0000000..59d4df6
--- /dev/null
+++ b/src/BlazorDesktop/Services/WindowManager.cs
@@ -0,0 +1,138 @@
+// Licensed to the .NET Extension Contributors under one or more agreements.
+// The .NET Extension Contributors licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Collections.Concurrent;
+using BlazorDesktop.Hosting;
+using BlazorDesktop.Wpf;
+using Microsoft.AspNetCore.Components;
+using WpfDispatcher = System.Windows.Threading.Dispatcher;
+
+namespace BlazorDesktop.Services;
+
+///
+/// Internal implementation of .
+///
+internal sealed class WindowManager : IWindowManager
+{
+ private readonly ConcurrentDictionary _windows = new();
+ private readonly IServiceProvider _services;
+ private readonly IWebHostEnvironment _environment;
+ private DesktopWindowHandle? _mainWindow;
+ private WpfDispatcher? _dispatcher;
+
+ ///
+ public DesktopWindowHandle MainWindow => _mainWindow ?? throw new InvalidOperationException("The main window has not been registered yet.");
+
+ ///
+ public IReadOnlyList Windows => _windows.Values.ToList().AsReadOnly();
+
+ ///
+ public event EventHandler? WindowOpened;
+
+ ///
+ public event EventHandler? WindowClosed;
+
+ public WindowManager(IServiceProvider services, IWebHostEnvironment environment)
+ {
+ _services = services;
+ _environment = environment;
+ }
+
+ ///
+ public Task OpenAsync(Action? configure = null, IDictionary? parameters = null) where TComponent : IComponent
+ {
+ return OpenAsync(typeof(TComponent), configure, parameters);
+ }
+
+ ///
+ public async Task OpenAsync(Type componentType, Action? configure = null, IDictionary? parameters = null)
+ {
+ ArgumentNullException.ThrowIfNull(componentType);
+
+ if (!typeof(IComponent).IsAssignableFrom(componentType))
+ {
+ throw new ArgumentException($"The type '{componentType.Name}' must implement {nameof(IComponent)} to be used as a root component.", nameof(componentType));
+ }
+
+ var dispatcher = _dispatcher ?? throw new InvalidOperationException("The WPF dispatcher has not been set. Ensure the main window is registered first.");
+
+ var options = new WindowOptions();
+ configure?.Invoke(options);
+
+ var handle = new DesktopWindowHandle(Guid.NewGuid().ToString("N"), isMainWindow: false);
+
+ var rootComponents = new RootComponentMappingCollection();
+ if (parameters is not null)
+ {
+ rootComponents.Add(componentType, "#app", ParameterView.FromDictionary(parameters));
+ }
+ else
+ {
+ rootComponents.Add(componentType, "#app");
+ }
+
+ await dispatcher.InvokeAsync(() =>
+ {
+ var window = new BlazorDesktopWindow(_services, _environment, options, rootComponents);
+
+ if (_mainWindow?.NativeWindow is not null)
+ {
+ window.Owner = _mainWindow.NativeWindow;
+ }
+
+ handle.NativeWindow = window;
+
+ window.Closed += (_, _) => OnChildWindowClosed(handle);
+
+ window.Show();
+ });
+
+ _windows.TryAdd(handle.Id, handle);
+ WindowOpened?.Invoke(this, handle);
+
+ return handle;
+ }
+
+ ///
+ public async Task CloseAsync(DesktopWindowHandle handle)
+ {
+ ArgumentNullException.ThrowIfNull(handle);
+
+ if (handle.IsMainWindow)
+ {
+ throw new InvalidOperationException("The main window cannot be closed via IWindowManager. Use application shutdown instead.");
+ }
+
+ var dispatcher = _dispatcher ?? throw new InvalidOperationException("The WPF dispatcher has not been set.");
+
+ await dispatcher.InvokeAsync(() =>
+ {
+ handle.NativeWindow?.Close();
+ });
+ }
+
+ ///
+ /// Registers the main window and stores the WPF dispatcher. Called by .
+ ///
+ internal void RegisterMainWindow(BlazorDesktopWindow window, WpfDispatcher dispatcher)
+ {
+ _dispatcher = dispatcher;
+
+ var handle = new DesktopWindowHandle("main", isMainWindow: true)
+ {
+ NativeWindow = window
+ };
+
+ _mainWindow = handle;
+ _windows.TryAdd(handle.Id, handle);
+ }
+
+ private void OnChildWindowClosed(DesktopWindowHandle handle)
+ {
+ _windows.TryRemove(handle.Id, out _);
+ handle.NativeWindow = null;
+ handle.OnClosed();
+ WindowClosed?.Invoke(this, handle);
+ }
+}
diff --git a/src/BlazorDesktop/Wpf/BlazorDesktopWindow.cs b/src/BlazorDesktop/Wpf/BlazorDesktopWindow.cs
index a7da917..607d981 100644
--- a/src/BlazorDesktop/Wpf/BlazorDesktopWindow.cs
+++ b/src/BlazorDesktop/Wpf/BlazorDesktopWindow.cs
@@ -45,8 +45,9 @@ public partial class BlazorDesktopWindow : Window
private WindowState _fullscreenStoredState = WindowState.Normal;
private readonly IServiceProvider _services;
- private readonly IConfiguration _config;
private readonly IWebHostEnvironment _environment;
+ private readonly WindowOptions _options;
+ private readonly RootComponentMappingCollection _rootComponents;
private readonly UISettings _uiSettings;
private readonly double[] _zoomSizes =
[5, 4, 3, 2.5, 2, 1.75, 1.5, 1.25, 1.1, 1, 0.9, 0.8, 0.75, 0.66, 0.5, 0.33, 0.25];
@@ -67,18 +68,31 @@ public partial class BlazorDesktopWindow : Window
";
///
- /// Creates a instance.
+ /// Creates a instance from configuration.
///
/// The services.
/// The configuration.
/// The hosting environment.
public BlazorDesktopWindow(IServiceProvider services, IConfiguration config, IWebHostEnvironment environment)
+ : this(services, environment, WindowOptions.FromConfiguration(config), services.GetRequiredService())
+ {
+ }
+
+ ///
+ /// Creates a instance with explicit options and root components.
+ ///
+ /// The services.
+ /// The hosting environment.
+ /// The window options.
+ /// The root component mappings.
+ internal BlazorDesktopWindow(IServiceProvider services, IWebHostEnvironment environment, WindowOptions options, RootComponentMappingCollection rootComponents)
{
WebView = new BlazorWebView();
WebViewBorder = new Border();
_services = services;
- _config = config;
_environment = environment;
+ _options = options;
+ _rootComponents = rootComponents;
_uiSettings = new UISettings();
InitializeWindow();
@@ -92,12 +106,14 @@ public BlazorDesktopWindow(IServiceProvider services, IConfiguration config, IWe
///
public void ToggleFullScreen()
{
+ var useFrame = _options.Frame ?? true;
+
if (WindowStyle == WindowStyle.SingleBorderWindow)
{
IsFullscreen = true;
_fullscreenStoredState = WindowState;
- UseFrame(_config.GetValue(WindowDefaults.Frame) ?? true);
+ UseFrame(useFrame);
WindowStyle = WindowStyle.None;
if (WindowState == WindowState.Maximized)
@@ -113,7 +129,7 @@ public void ToggleFullScreen()
{
IsFullscreen = false;
- UseFrame(_config.GetValue(WindowDefaults.Frame) ?? true);
+ UseFrame(useFrame);
WindowStyle = WindowStyle.SingleBorderWindow;
WindowState = _fullscreenStoredState;
@@ -171,14 +187,14 @@ public void ZoomOut()
private void InitializeWindow()
{
- var height = _config.GetValue(WindowDefaults.Height) ?? 768;
- var width = _config.GetValue(WindowDefaults.Width) ?? 1366;
- var minHeight = _config.GetValue(WindowDefaults.MinHeight) ?? 0;
- var minWidth = _config.GetValue(WindowDefaults.MinWidth) ?? 0;
- var maxHeight = _config.GetValue(WindowDefaults.MaxHeight) ?? double.PositiveInfinity;
- var maxWidth = _config.GetValue(WindowDefaults.MaxWidth) ?? double.PositiveInfinity;
+ var height = _options.Height ?? 768;
+ var width = _options.Width ?? 1366;
+ var minHeight = _options.MinHeight ?? 0;
+ var minWidth = _options.MinWidth ?? 0;
+ var maxHeight = (double?)_options.MaxHeight ?? double.PositiveInfinity;
+ var maxWidth = (double?)_options.MaxWidth ?? double.PositiveInfinity;
- var useFrame = _config.GetValue(WindowDefaults.Frame) ?? true;
+ var useFrame = _options.Frame ?? true;
if (useFrame)
{
@@ -232,7 +248,7 @@ private void InitializeWindow()
}
Name = "BlazorDesktopWindow";
- Title = _config.GetValue(WindowDefaults.Title) ?? _environment.ApplicationName;
+ Title = _options.Title ?? _environment.ApplicationName;
Height = height;
Width = width;
MinHeight = minHeight;
@@ -240,8 +256,8 @@ private void InitializeWindow()
MaxHeight = maxHeight;
MaxWidth = maxWidth;
UseFrame(useFrame);
- ResizeMode = (_config.GetValue(WindowDefaults.Resizable) ?? true) ? ResizeMode.CanResize : ResizeMode.NoResize;
- UseIcon(_config.GetValue(WindowDefaults.Icon) ?? string.Empty);
+ ResizeMode = (_options.Resizable ?? true) ? ResizeMode.CanResize : ResizeMode.NoResize;
+ UseIcon(_options.Icon ?? string.Empty);
Content = WebViewBorder;
StateChanged += WindowStateChanged;
KeyDown += WindowKeyDown;
@@ -261,7 +277,7 @@ private void InitializeWebView()
WebView.HostPage = Path.Combine(_environment.WebRootPath, "index.html");
WebView.Services = _services;
- foreach (var rootComponent in _services.GetRequiredService())
+ foreach (var rootComponent in _rootComponents)
{
WebView.RootComponents.Add(new()
{
@@ -305,7 +321,7 @@ private void ThemeChanged(UISettings sender, object args)
private void UpdateWebViewBorderThickness()
{
- var useFrame = _config.GetValue(WindowDefaults.Frame) ?? true;
+ var useFrame = _options.Frame ?? true;
WebViewBorder.BorderThickness = new Thickness(20, 20, 20, 20);