Skip to content

Commit 74b35c7

Browse files
RedthCopilot
andcommitted
Dogfood the modal navigation seam in the in-box Tizen backend
The seam was so far only exercised by tests and by external backends. This makes the framework its own first consumer: the Tizen presentation now lives in TizenModalNavigationPlatform, an IModalNavigationPlatform written entirely against IModalNavigationHost with no access to ModalNavigationManager internals. If the contract were insufficient for a real backend, this refactor could not have been written — which is the point. The Tizen partial keeps the platform hooks but delegates to that type, and its stack bookkeeping now mirrors PushModalWithOverrideAsync / PopModalWithOverrideAsync exactly, so the fallback path and the seam path apply identical ordering. This is a mechanical translation. Every call and its order is preserved: pop still sends Disappearing, no-ops when the modal has no platform handler, pops the native stack, sends Appearing on the revealed page and disposes the handler; push still sends Disappearing, realizes the view, pushes, and sends Appearing only if the modal is still current. The one reordering is moving _platformModalPages.Add ahead of the platform call to satisfy the seam contract, which is equivalent because CurrentPage is computed from the requested stack, not from _platformModalPages. Deliberately no DI registration: with no factory registered the built-in path runs, and a registered IModalNavigationPlatformFactory still takes precedence and short-circuits before this type is ever constructed. So this changes no resolution behavior and needs no precedence rules. Note on verification. Tizen has IncludeTizenTargetFrameworks=false in CI ("Disabled until net10.0-tizen is available") and the Tizen SDK pack is not installable locally, so this code compiles nowhere in the normal build. It was instead validated by compiling both files against the real seam interfaces with stubs standing in only for the Tizen-native calls, which checks interface conformance, signatures, nullability and name resolution; the Tizen-native calls themselves are copied verbatim from the shipped file. Adds a test asserting the no-factory path maintains PlatformModalStack with the same ordering the seam promises, since the Tizen delegation now depends on that invariant. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 2f80937 commit 74b35c7

3 files changed

Lines changed: 128 additions & 33 deletions

File tree

Lines changed: 19 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,47 @@
11
#nullable enable
22

33
using System.Threading.Tasks;
4-
using Tizen.UIExtensions.NUI;
54

65
namespace Microsoft.Maui.Controls.Platform
76
{
87
internal partial class ModalNavigationManager
98
{
10-
NavigationStack _modalStack => WindowMauiContext.GetModalStack();
11-
IPageController CurrentPageController => CurrentPage!;
9+
TizenModalNavigationPlatform? _tizenPlatform;
10+
11+
// The built-in Tizen presentation is itself an IModalNavigationPlatform, so the fallback path
12+
// exercises the same public contract an external backend implements. Created lazily because a
13+
// registered IModalNavigationPlatformFactory takes precedence and these members never run.
14+
TizenModalNavigationPlatform TizenPlatform =>
15+
_tizenPlatform ??= new TizenModalNavigationPlatform(this);
1216

1317
Task SyncModalStackWhenPlatformIsReadyCoreAsync() =>
1418
SyncPlatformModalStackAsync();
1519

16-
bool IsModalPlatformReadyCore => true;
20+
bool IsModalPlatformReadyCore => TizenPlatform.IsReady;
1721

18-
partial void OnPageAttachedHandler()
19-
{
20-
WindowMauiContext.GetPlatformWindow().SetBackButtonPressedHandler(OnBackButtonPressed);
21-
}
22+
partial void OnPageAttachedHandler() =>
23+
TizenPlatform.PageAttached();
2224

2325
async Task<Page> PopModalPlatformCoreAsync(bool animated)
2426
{
2527
Page modal = CurrentPlatformModalPage;
26-
_platformModalPages.Remove(modal);
2728

28-
((IPageController)modal).SendDisappearing();
29+
// Mirrors PopModalWithOverrideAsync: the framework owns the platform stack and removes the
30+
// modal before the dismissal, so CurrentPlatformPage already refers to the revealed page.
31+
_platformModalPages.Remove(modal);
2932

30-
var modalRenderer = modal.Handler as IPlatformViewHandler;
31-
if (modalRenderer is not null)
32-
{
33-
await _modalStack.Pop(animated);
34-
CurrentPageController?.SendAppearing();
35-
(modal.Handler as IPlatformViewHandler)?.Dispose();
36-
}
33+
await TizenPlatform.PopModalAsync(modal, animated);
3734
return modal;
3835
}
3936

40-
async Task PushModalPlatformCoreAsync(Page modal, bool animated)
37+
Task PushModalPlatformCoreAsync(Page modal, bool animated)
4138
{
42-
CurrentPageController?.SendDisappearing();
39+
// Mirrors PushModalWithOverrideAsync. The add moved ahead of the platform call to match the
40+
// seam contract; it is equivalent because CurrentPage is computed from the requested stack,
41+
// not from _platformModalPages.
4342
_platformModalPages.Add(modal);
4443

45-
var nativeView = modal.ToPlatform(WindowMauiContext);
46-
47-
await _modalStack.Push(nativeView, animated);
48-
49-
// Verify that the modal is still on the stack
50-
if (CurrentPage == modal)
51-
((IPageController)modal).SendAppearing();
52-
}
53-
54-
bool OnBackButtonPressed()
55-
{
56-
bool handled = CurrentPage?.SendBackButtonPressed() ?? false;
57-
58-
return handled;
44+
return TizenPlatform.PushModalAsync(modal, animated);
5945
}
6046
}
6147
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
#nullable enable
2+
3+
using System.Threading.Tasks;
4+
using Tizen.UIExtensions.NUI;
5+
6+
namespace Microsoft.Maui.Controls.Platform
7+
{
8+
/// <summary>
9+
/// The in-box Tizen modal presentation, written against the public
10+
/// <see cref="IModalNavigationPlatform"/> seam.
11+
/// </summary>
12+
/// <remarks>
13+
/// <para>
14+
/// This is the framework's own consumer of the extensibility contract: everything Tizen needs to
15+
/// present and dismiss a modal is expressed through <see cref="IModalNavigationHost"/>, with no
16+
/// access to <c>ModalNavigationManager</c> internals. An external backend can be written the same
17+
/// way, and this type is the reference for doing so.
18+
/// </para>
19+
/// <para>
20+
/// It is used when no <see cref="IModalNavigationPlatformFactory"/> is registered. A registered
21+
/// factory takes precedence and this type is never constructed.
22+
/// </para>
23+
/// </remarks>
24+
internal sealed class TizenModalNavigationPlatform : IModalNavigationPlatform
25+
{
26+
readonly IModalNavigationHost _host;
27+
28+
public TizenModalNavigationPlatform(IModalNavigationHost host)
29+
{
30+
_host = host;
31+
}
32+
33+
NavigationStack ModalStack => _host.MauiContext.GetModalStack();
34+
35+
public bool IsReady => true;
36+
37+
public void PageAttached() =>
38+
_host.MauiContext.GetPlatformWindow().SetBackButtonPressedHandler(OnBackButtonPressed);
39+
40+
public async Task PushModalAsync(Page modal, bool animated)
41+
{
42+
// The framework has already put the modal on IModalNavigationHost.PlatformModalStack, which
43+
// does not affect CurrentPage, so this still observes the page that is going away.
44+
(_host.CurrentPage as IPageController)?.SendDisappearing();
45+
46+
var nativeView = modal.ToPlatform(_host.MauiContext);
47+
48+
await ModalStack.Push(nativeView, animated);
49+
50+
// Verify that the modal is still on the stack
51+
if (_host.CurrentPage == modal)
52+
((IPageController)modal).SendAppearing();
53+
}
54+
55+
public async Task PopModalAsync(Page modal, bool animated)
56+
{
57+
((IPageController)modal).SendDisappearing();
58+
59+
// A modal with no platform handler was never realized, so there is nothing to dismiss. This
60+
// is also what makes the method idempotent for a modal the platform already tore down.
61+
if (modal.Handler is IPlatformViewHandler handler)
62+
{
63+
await ModalStack.Pop(animated);
64+
(_host.CurrentPage as IPageController)?.SendAppearing();
65+
handler.Dispose();
66+
}
67+
}
68+
69+
// The navigation stack and the platform window are owned by the window's platform side, not by
70+
// this instance, so there is nothing of our own to release.
71+
public void Dispose()
72+
{
73+
}
74+
75+
bool OnBackButtonPressed() =>
76+
_host.CurrentPage?.SendBackButtonPressed() ?? false;
77+
}
78+
}

src/Controls/tests/Core.UnitTests/ModalNavigationPlatformTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,37 @@ public async Task NoRegistrationUsesBuiltInPlatform()
113113
Assert.Empty(Host(window).PlatformModalStack);
114114
}
115115

116+
[Fact]
117+
public async Task BuiltInPlatformMaintainsTheSameStackOrderingAsTheSeam()
118+
{
119+
// The in-box Tizen backend is implemented against IModalNavigationPlatform and relies on the
120+
// fallback path applying the same stack bookkeeping the seam contract promises: the modal is
121+
// on PlatformModalStack for the duration of a push, and off it for the duration of a pop.
122+
// This guards that invariant on the no-factory path, which is what the built-in platforms use.
123+
var window = CreateWindow();
124+
var root = AttachRootPage(window);
125+
var host = Host(window);
126+
127+
var first = new ContentPage();
128+
var second = new ContentPage();
129+
130+
await window.Navigation.PushModalAsync(first);
131+
Assert.Equal(new[] { first }, host.PlatformModalStack);
132+
Assert.Same(first, host.CurrentPlatformPage);
133+
134+
await window.Navigation.PushModalAsync(second);
135+
Assert.Equal(new[] { first, second }, host.PlatformModalStack);
136+
Assert.Same(second, host.CurrentPlatformPage);
137+
138+
await window.Navigation.PopModalAsync();
139+
Assert.Equal(new[] { first }, host.PlatformModalStack);
140+
Assert.Same(first, host.CurrentPlatformPage);
141+
142+
await window.Navigation.PopModalAsync();
143+
Assert.Empty(host.PlatformModalStack);
144+
Assert.Same(root, host.CurrentPlatformPage);
145+
}
146+
116147
[Fact]
117148
public async Task CustomPlatformResolvedFromDependencyInjection()
118149
{

0 commit comments

Comments
 (0)