CommunityToolkit / CommunityToolkit/Maui

[Proposal] Modal windows for desktop apps

Open
#842 9 comments 15 reactions 0 assignees View on GitHub
blocked dependencies new proposal
Dominant language
C#
Stars
2.7k
Forks
500
Avg merge
2d 14h
Merged PRs (30d)
7

Description

### Feature name

Modal Windows for desktop apps

### Link to discussion

https://github.com/CommunityToolkit/Maui/discussions/840

### Progress tracker

- [ ] Android Implementation
- [ ] iOS Implementation
- [ ] MacCatalyst Implementation
- [ ] Windows Implementation
- [ ] Tizen Implementation
- [ ] Unit Tests
- [ ] Samples
- [ ] Documentation

### Summary

Enable use of modal windows in multi-window desktop apps that:

- Can be moved beyond the confines of a specific window e.g., so it can be displayed next to the window or on another monitor
- Prevents interaction with all other application windows until that modal window has been closed
- Keeps focus (and appears on top of the other application windows) even when other application windows receive input
- Does not appear alongside other windows in the taskbar and/or switcher

Existing options, within .NET MAUI and other third-party components, only support the display of a Page and/or dialog that is constrained to the Window it was opened from.

### Motivation

In desktop apps, a modal window is often used to ensure a task or flow is completed before further interaction with application functionality is able to take place on any other window in the application. For example, in Visual Studio when a user needs to sign in, switch accounts, or change some global settings. This feature would make it easier for .NET MAUI developers to use modal windows and follow paradigms commonly used by desktop apps.

### Detailed Design

#### ModalWindow.cs

This is principally to allow for a platform-specific handler that can perform actions where needed before the ```CreatePlatformElement``` method returns the native ```Window``` / ```UIWindow```.

```csharp
public sealed class ModalWindow : Window
{
public ModalWindow() : base() {}
public ModalWindow(Page page) : base(page) {}
}
```

#### INavigationExtensions.cs

Extends ```INavigation``` with a version of ```INavigation.PushModalAsync``` that will show the specified ```Page``` in a modal ```Window``` if supported but otherwise use the current method to display it within the current ```Window```.

```csharp
namespace Microsoft.Maui.Controls;

public static class INavigationExtensions
{
public static Task PushModalAsyncEx(this INavigation navigation, Page page)
{
#if MACCATALYST || WINDOWS
Application.Current.OpenWindow(new ModalWindow(page));
return Task.CompletedTask;
#else
return navigation.PushModalAsync(page);
#endif
}
}
```

Custom ```WindowHandler``` implementations would need to orchestrate the modal configuration/behavior using the requisite platform APIs, such as those identified below (see [Platform APIs](#platform-apis)). On MacCatalyst, a custom ```MauiUISceneDelegate``` could potentially get allocated to the ```UIWindow.WindowScene``` to handle the configuration and running/stopping of the modal loop when the respective ```UIWindow``` opens and closes. Likewise on Windows, the ```Xaml.Window``` could be configured and run as a modal on creation then stop the modal behavior on closing.

# Platform APIs

The expectation is that this would require use of the following platform-specific APIs.

#### MacCatalyst

A modal event loop can be started and stopped for a specific [NSWindow](https://developer.apple.com/documentation/appkit/nswindow?language=objc) using the following APIs from the [NSApplication](https://developer.apple.com/documentation/appkit/nsapplication?language=objc) class:

- [runModal(for:)](https://developer.apple.com/documentation/appkit/nsapplication/1428436-runmodal)
- [stopModal](https://developer.apple.com/documentation/appkit/nsapplication/1428489-stopmodal)

The standard buttons on a given [NSWindow](https://developer.apple.com/documentation/appkit/nswindow?language=objc) can be acquired by type using [standardWindowButton:](https://developer.apple.com/documentation/appkit/nswindow/1419491-standardwindowbutton?language=objc) and configured as needed.

#### WinUI3

A combination of [Win32](https://docs.microsoft.com/windows/win32/apiindex/windows-api-list) and [Windows App SDK Interop](https://learn.microsoft.com/windows/apps/desktop/modernize/winrt-com-interop-csharp#available-as-part-of-the-windows-app-sdk) APIs alongside the [Windows App SDK](https://learn.microsoft.com/windows/windows-app-sdk) components as described in an answer to a [recent forum question](https://docs.microsoft.com/answers/questions/910658/winui3how-to-show-a-model-window.html) and inferred from [usage in WPF](https://github.com/dotnet/wpf/blob/ec69834f378fb98ef5f623db1c55610ac074001d/src/Microsoft.DotNet.Wpf/src/Shared/MS/Win32/UnsafeNativeMethodsOther.cs#L422). Notable members, types, and APIs include:

##### Types and Members
- [AppWindow.IsShownInSwitchers](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.windowing.appwindow.isshowninswitchers?view=windows-app-sdk-1.1)
- [AppWindow.Presenter](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.windowing.appwindow.presenter?view=windows-app-sdk-1.1)
- [OverlappedPresenter.IsMaximizable](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.windowing.overlappedpresenter.ismaximizable?view=windows-app-sdk-1.1)
- [OverlappedPresenter.IsMinimizable](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.windowing.overlappedpresenter.isminimizable?view=windows-app-sdk-1.1)
- [OverlappedPresenter.IsResizable](https://learn.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.windowing.overlappedpresenter.isresizable?view=windows-app-sdk-1.1)

##### Win32 APIs and Constants
- [GetWindow](https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-getwindow)
- [EnableWindow](https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-enablewindow)
- [SetWindowLong](https://learn.microsoft.com/windows/win32/api/winuser/nf-winuser-setwindowlonga)
- [WindowStyles](https://learn.microsoft.com/windows/win32/winmsg/window-styles)

### Usage Syntax

#### MainPage (code-behind)

The following is an indicative Button Clicked event handler from a ```ContentPage``` (**MainPage**) that opens a modal window hosting the specified ```ContentPage``` (**MyPage**).

```csharp
public partial class MainPage : ContentPage
{
void OnButtonClicked(object sender, EventArgs e)
=> _ = Navigation.PushModalAsyncEx(new MyPage());
}
```

### Drawbacks

The requisite platform APIs aren't exposed directly by the [MacCatalyst](https://developer.apple.com/documentation/uikit/mac_catalyst?language=objc) and [WinUI3](https://docs.microsoft.com/windows/apps/winui/winui3/) SDKs. A challenge common to both [MacCatalyst](https://developer.apple.com/documentation/uikit/mac_catalyst?language=objc) and [WinUI3](https://docs.microsoft.com/windows/apps/winui/winui3/) is the potential to miss key details of the implementation when orchestrating several separate lower-level APIs or not anticipating things that may impact the modal behavior. The effort and approaches required to build this feature will also vary per platform and have their own considerations.

#### MacCatalyst

This feature would require use of foundational [AppKit](https://developer.apple.com/documentation/appkit?language=objc) APIs and Types that aren't exposed by the [MacCatalyst](https://developer.apple.com/documentation/uikit/mac_catalyst?language=objc) SDK. Notably [NSApplication](https://developer.apple.com/documentation/appkit/nsapplication?language=objc), [NSWindow](https://developer.apple.com/documentation/appkit/nswindow), and manipulation of its standard window buttons.

[MacCatalyst](https://developer.apple.com/documentation/uikit/mac_catalyst?language=objc) currently depends on several [AppKit](https://developer.apple.com/documentation/appkit?language=objc) Types and concepts. For example, a [MacCatalyst](https://developer.apple.com/documentation/uikit/mac_catalyst?language=objc) specific class called `UINSWindow` is created when a scene is activated and is added to the [NSApplication.sharedApplication.windows](https://developer.apple.com/documentation/appkit/nsapplication/1428402-windows?language=objc) array. This is a special private class that's essentially a bridge between [UIWindow](https://developer.apple.com/documentation/uikit/uiwindow?language=objc) and [NSWindow](https://developer.apple.com/documentation/appkit/nswindow) allowing scenes to be presented as Mac windows. `UINSWindow` inherits from [NSWindow](https://developer.apple.com/documentation/appkit/nswindow) and so its technically possible to invoke its functionality at runtime and pass it to other [AppKit](https://developer.apple.com/documentation/appkit?language=objc) classes, including for use with the aforementioned [NSApplication](https://developer.apple.com/documentation/appkit/nsapplication?language=objc) functions.

While the [NSApplication](https://developer.apple.com/documentation/appkit/nsapplication?language=objc), [NSWindow](https://developer.apple.com/documentation/appkit/nswindow), and [NSButton](https://developer.apple.com/documentation/appkit/nsbutton?language=objc) types cannot be used directly in [MacCatalyst](https://developer.apple.com/documentation/uikit/mac_catalyst?language=objc), it's possible to get a reference to those types and invoke the requisite functionality on them via selectors.

##### Key Considerations

- There's a risk that Apple could remove or prevent use of the requisite [AppKit](https://developer.apple.com/documentation/appkit?language=objc) APIs by a [MacCatalyst](https://developer.apple.com/documentation/uikit/mac_catalyst?language=objc) app in future
- The `UINSWindow` is a private class. If you use it directly in a [MacCatalyst](https://developer.apple.com/documentation/uikit/mac_catalyst?language=objc) app, and its use is detected, it might be considered grounds for rejection during the App Store review process. It's not clear whether getting a reference to and calling functionality on the [NSWindow](https://developer.apple.com/documentation/appkit/nswindow) (which the `UINSWindow` derives from) would be detectable and/or considered in the same way since it's public even if it's not exposed by the [MacCatalyst](https://developer.apple.com/documentation/uikit/mac_catalyst?language=objc) SDK.

#### WinUI3

There's an [open proposal](https://github.com/microsoft/microsoft-ui-xaml/issues/885) for adding modal dialog support to [WinUI3](https://docs.microsoft.com/windows/apps/winui/winui3/) but it does not currently support this concept directly. Therefore, this feature would require orchestration of several lower level APIs making it more challenging and error prone when compared to calling a couple of high-level APIs.

##### Key Considerations

- Effort associated with ensuring the correct sequencing and handling of the requisite lower level APIs
- Change if modal dialog support is added to [WinUI3](https://docs.microsoft.com/windows/apps/winui/winui3/) in future

### Alternatives

_No response_

### Unresolved Questions

_No response_

Contributor guide

Open the contributing guide

Research direction

Start by reviewing the proposed ModalWindow.cs and INavigationExtensions.cs entry points, then compare the platform APIs listed for MacCatalyst and WinUI3. The work is done when the tracked Android, iOS, MacCatalyst, Windows, Tizen, unit test, sample, and documentation items are addressed with the requested modal behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
desktop
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.