Skip to content

Commit a92f9fb

Browse files
committed
Fix INavigation issues on Windows
1 parent 1226c7e commit a92f9fb

File tree

5 files changed

+172
-80
lines changed

5 files changed

+172
-80
lines changed

src/Bible.Alarm/App.xaml.cs

Lines changed: 86 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
using Bible.Alarm.Views.General;
88
using Bible.Alarm.ViewModels;
99
using Bible.Alarm.ViewModels.Shared;
10-
using Bible.Alarm.Views.Shared;
11-
1210
#nullable enable
1311

1412
namespace Bible.Alarm;
@@ -19,13 +17,14 @@ public partial class App : Application,
1917
IRecipient<ShowMediaProgressModalMessage>,
2018
IRecipient<HideMediaProgressModalMessage>,
2119
IRecipient<ShowToastMessage>,
22-
IRecipient<ClearToastsMessage>
20+
IRecipient<ClearToastsMessage>,
21+
IRecipient<InitializedMessage>
2322
{
2423
private readonly Serilog.ILogger _logger;
2524
private readonly IServiceProvider _serviceProvider;
2625

2726
public static bool IsInForeground { get; set; } = false;
28-
private INavigation? _navigation;
27+
private INavigation? Navigation => _serviceProvider.GetService<INavigation>();
2928

3029
public App(Serilog.ILogger logger, IServiceProvider serviceProvider)
3130
{
@@ -34,6 +33,10 @@ public App(Serilog.ILogger logger, IServiceProvider serviceProvider)
3433
_serviceProvider = serviceProvider;
3534
InitializeComponent();
3635

36+
// Register for InitializedMessage before bootstrap is called
37+
// This will handle showing HomePage after bootstrap completes
38+
WeakReferenceMessenger.Default.Register<InitializedMessage>(this);
39+
3740
// Register for modal messages
3841
WeakReferenceMessenger.Default.Register<ShowAlarmModalMessage>(this);
3942
WeakReferenceMessenger.Default.Register<HideAlarmModalMessage>(this);
@@ -46,75 +49,31 @@ public App(Serilog.ILogger logger, IServiceProvider serviceProvider)
4649
protected override Window CreateWindow(IActivationState? activationState)
4750
{
4851
System.Diagnostics.Debug.WriteLine("CreateWindow called!");
49-
50-
var homePage = _serviceProvider.GetRequiredService<Home>();
51-
var navigationPage = new NavigationPage(homePage)
52+
53+
var loadingPage = _serviceProvider.GetRequiredService<LoadingPage>();
54+
var navigationPage = new NavigationPage(loadingPage)
5255
{
5356
BarBackgroundColor = Colors.Transparent,
5457
BarTextColor = Colors.White
5558
};
5659

57-
// Hide the nav bar on HomePage only
58-
NavigationPage.SetHasNavigationBar(homePage, false);
60+
NavigationPage.SetHasNavigationBar(loadingPage, false);
5961

60-
// Run bootstrapper after navigation page setup for foreground launch
61-
// This ensures UI is ready before bootstrap initializes
62-
MauiProgram.InitializePlatformBootstrap(_serviceProvider, isForeground: true);
63-
64-
#if IOS
65-
// Note: Large titles in MAUI are typically configured via platform-specific code
66-
// or using Shell if needed. For now, we'll skip this configuration.
67-
#endif
68-
69-
_navigation = navigationPage.Navigation;
70-
71-
var window = new Window(navigationPage);
72-
73-
// Initialize services in background
74-
_ = Task.Run(async () =>
75-
{
76-
try
77-
{
78-
System.Diagnostics.Debug.WriteLine("Starting service initialization...");
79-
80-
var playbackService = _serviceProvider.GetRequiredService<IPlaybackService>();
81-
if (playbackService.IsPrepared) WeakReferenceMessenger.Default.Send(new ShowAlarmModalMessage(null));
82-
83-
await Task.Delay(100); // Small delay to ensure initialization
84-
85-
System.Diagnostics.Debug.WriteLine("Service initialization completed!");
86-
}
87-
catch (Exception ex)
88-
{
89-
System.Diagnostics.Debug.WriteLine($"Error in initialization: {ex.Message}");
90-
System.Diagnostics.Debug.WriteLine($"Stack trace: {ex.StackTrace}");
91-
}
92-
});
93-
94-
return window;
62+
return new Window(navigationPage);
9563
}
9664

9765
protected override void OnStart()
9866
{
9967
IsInForeground = true;
10068

10169
base.OnStart();
70+
71+
MauiProgram.InitializePlatformBootstrap(_serviceProvider, isForeground: true);
72+
10273
Task.Run(async () =>
10374
{
10475
try
10576
{
106-
// Navigate to home
107-
if (_navigation != null)
108-
{
109-
await MainThread.InvokeOnMainThreadAsync(async () =>
110-
{
111-
while (_navigation.ModalStack.Count > 0)
112-
await _navigation.PopModalAsync();
113-
114-
while (_navigation.NavigationStack.Count > 1)
115-
await _navigation.PopAsync();
116-
});
117-
}
11877

11978
var playbackService = _serviceProvider.GetRequiredService<IPlaybackService>();
12079

@@ -149,8 +108,9 @@ protected override void OnResume()
149108
try
150109
{
151110
var playbackService = _serviceProvider.GetRequiredService<IPlaybackService>();
152-
// Handle when your app resumes
153-
if (playbackService.IsPrepared) WeakReferenceMessenger.Default.Send(new ShowAlarmModalMessage(null));
111+
112+
if (playbackService.IsPrepared)
113+
WeakReferenceMessenger.Default.Send(new ShowAlarmModalMessage(null));
154114

155115
await Task.Delay(1000);
156116

@@ -169,29 +129,29 @@ public void Receive(ShowAlarmModalMessage message)
169129
{
170130
_ = MainThread.InvokeOnMainThreadAsync(async () =>
171131
{
172-
if (_navigation == null) return;
132+
if (Navigation == null) return;
173133

174-
// Prevent showing alarm modal when playback is not active
175134
var playbackService = _serviceProvider.GetRequiredService<IPlaybackService>();
176-
if (!playbackService.IsPlaying) return;
135+
if (!playbackService.IsPlaying)
136+
return;
177137

178-
if (_navigation.ModalStack.LastOrDefault()?.GetType() == typeof(AlarmModal)) return;
138+
if (Navigation.ModalStack.LastOrDefault()?.GetType() == typeof(AlarmModal)) return;
179139

180140
var vm = _serviceProvider.GetRequiredService<AlarmViewModal>();
181141
var modal = _serviceProvider.GetRequiredService<AlarmModal>();
182142
modal.BindingContext = vm;
183-
await _navigation.PushModalAsync(modal);
143+
await Navigation.PushModalAsync(modal);
184144
});
185145
}
186146

187147
public void Receive(HideAlarmModalMessage message)
188148
{
189149
_ = MainThread.InvokeOnMainThreadAsync(async () =>
190150
{
191-
if (_navigation == null) return;
192-
if (_navigation.ModalStack.Count > 0)
151+
if (Navigation == null) return;
152+
if (Navigation.ModalStack.Count > 0)
193153
{
194-
var modal = await _navigation.PopModalAsync();
154+
var modal = await Navigation.PopModalAsync();
195155
if (modal.BindingContext is IDisposable disposable) disposable.Dispose();
196156
}
197157
});
@@ -201,25 +161,25 @@ public void Receive(ShowMediaProgressModalMessage message)
201161
{
202162
_ = MainThread.InvokeOnMainThreadAsync(async () =>
203163
{
204-
if (_navigation == null) return;
164+
if (Navigation == null) return;
205165

206-
if (_navigation.ModalStack.LastOrDefault()?.GetType() == typeof(MediaProgressModal)) return;
166+
if (Navigation.ModalStack.LastOrDefault()?.GetType() == typeof(MediaProgressModal)) return;
207167

208168
var vm = _serviceProvider.GetRequiredService<MediaProgressViewModal>();
209169
var modal = _serviceProvider.GetRequiredService<MediaProgressModal>();
210170
modal.BindingContext = vm;
211-
await _navigation.PushModalAsync(modal);
171+
await Navigation.PushModalAsync(modal);
212172
});
213173
}
214174

215175
public void Receive(HideMediaProgressModalMessage message)
216176
{
217177
_ = MainThread.InvokeOnMainThreadAsync(async () =>
218178
{
219-
if (_navigation == null) return;
220-
if (_navigation.ModalStack.Count > 0)
179+
if (Navigation == null) return;
180+
if (Navigation.ModalStack.Count > 0)
221181
{
222-
var modal = await _navigation.PopModalAsync();
182+
var modal = await Navigation.PopModalAsync();
223183
if (modal.BindingContext is IDisposable disposable) disposable.Dispose();
224184
}
225185
});
@@ -242,4 +202,58 @@ public void Receive(ClearToastsMessage message)
242202
await toastService.Clear();
243203
});
244204
}
205+
206+
public void Receive(InitializedMessage message)
207+
{
208+
_ = MainThread.InvokeOnMainThreadAsync(async () =>
209+
{
210+
if (Navigation == null) return;
211+
212+
try
213+
{
214+
var homePage = _serviceProvider.GetRequiredService<Home>();
215+
216+
NavigationPage.SetHasNavigationBar(homePage, false);
217+
218+
await Navigation.PushAsync(homePage);
219+
220+
// PopAsync removes the TOP page (HomePage), so we need to manipulate the stack differently
221+
var pagesToRemove = Navigation.NavigationStack.Where(p => p != homePage).ToList();
222+
foreach (var page in pagesToRemove)
223+
{
224+
Navigation.RemovePage(page);
225+
}
226+
227+
if (homePage.BindingContext is HomeViewModel homeViewModel)
228+
{
229+
await homeViewModel.InitializeAsync();
230+
}
231+
232+
233+
_ = Task.Run(async () =>
234+
{
235+
try
236+
{
237+
System.Diagnostics.Debug.WriteLine("Starting service initialization...");
238+
239+
var playbackService = _serviceProvider.GetRequiredService<IPlaybackService>();
240+
if (playbackService.IsPrepared) WeakReferenceMessenger.Default.Send(new ShowAlarmModalMessage(null));
241+
242+
await Task.Delay(100);
243+
244+
System.Diagnostics.Debug.WriteLine("Service initialization completed!");
245+
}
246+
catch (Exception ex)
247+
{
248+
System.Diagnostics.Debug.WriteLine($"Error in initialization: {ex.Message}");
249+
System.Diagnostics.Debug.WriteLine($"Stack trace: {ex.StackTrace}");
250+
}
251+
});
252+
}
253+
catch (Exception e)
254+
{
255+
_logger.Error(e, "An error happened while showing HomePage after initialization.");
256+
}
257+
});
258+
}
245259
}

src/Bible.Alarm/MauiProgram.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,33 @@ private static void RegisterCommonServices(IServiceCollection services)
183183

184184
// Register TaskScheduler for compatibility - use default scheduler instead of UI context
185185
services.AddSingleton<TaskScheduler>(sp => TaskScheduler.Default);
186+
187+
#if WINDOWS
188+
// CRITICAL FIX FOR WINDOWS — INavigation is NOT auto-registered on Windows
189+
// This is a known MAUI bug that affects Windows but not Android/iOS
190+
// Android/iOS auto-register INavigation, but Windows does not
191+
services.AddSingleton<Microsoft.Maui.Controls.INavigation>(sp =>
192+
{
193+
var app = Application.Current;
194+
if (app?.MainPage is NavigationPage navPage)
195+
{
196+
return navPage.Navigation;
197+
}
198+
199+
// Fallback — try to get from Window's Page (set in CreateWindow)
200+
if (app?.Windows.Count > 0)
201+
{
202+
var window = app.Windows[0];
203+
if (window.Page is NavigationPage windowNavPage)
204+
{
205+
return windowNavPage.Navigation;
206+
}
207+
}
208+
209+
// During startup, this might not be ready yet
210+
throw new InvalidOperationException("INavigation is not available. NavigationPage must be set before resolving INavigation.");
211+
});
212+
#endif
186213
}
187214

188215
private static void RegisterViewModels(IServiceCollection services)
@@ -226,6 +253,7 @@ private static void RegisterUiComponents(IServiceCollection services)
226253
services.AddTransient<BatteryOptimizationExclusionModal>();
227254
services.AddTransient<NumberOfChaptersModal>();
228255
services.AddTransient<MediaProgressModal>();
256+
services.AddTransient<LoadingPage>();
229257
}
230258

231259
/// <summary>

src/Bible.Alarm/ViewModels/HomeViewModel.cs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525
namespace Bible.Alarm.ViewModels;
2626

27-
public class HomeViewModel : ObservableObject, IDisposable, IRecipient<InitializedMessage>
27+
public class HomeViewModel : ObservableObject, IDisposable
2828
{
2929
private readonly ILogger _logger;
3030
private readonly IServiceScopeFactory _scopeFactory;
@@ -114,13 +114,15 @@ public HomeViewModel(
114114
IsBusy = false;
115115
}
116116

117-
// Subscribe to Initialized message using WeakReferenceMessenger
118-
WeakReferenceMessenger.Default.Register<InitializedMessage>(this);
119117
}
120118

121-
public void Receive(InitializedMessage message)
119+
/// <summary>
120+
/// Initializes the HomeViewModel after bootstrap is complete.
121+
/// This should be called from App.xaml.cs after InitializedMessage is received.
122+
/// </summary>
123+
public async Task InitializeAsync()
122124
{
123-
_ = HandleInitialized();
125+
await HandleInitialized();
124126
}
125127

126128
private async Task HandleInitialized()
@@ -415,9 +417,6 @@ private void SetupMediaCache(long scheduleId)
415417

416418
public void Dispose()
417419
{
418-
// Unsubscribe from WeakReferenceMessenger
419-
WeakReferenceMessenger.Default.Unregister<InitializedMessage>(this);
420-
421420
// Unsubscribe from all schedule IsEnabled changes
422421
if (Schedules != null)
423422
{
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
3+
<ContentPage
4+
x:Class="Bible.Alarm.Views.General.LoadingPage"
5+
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
6+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
7+
BackgroundColor="White"
8+
Title="Bible Alarm">
9+
<ContentPage.Content>
10+
<Grid>
11+
<StackLayout
12+
HorizontalOptions="Center"
13+
Orientation="Vertical"
14+
VerticalOptions="Center">
15+
<ActivityIndicator
16+
IsRunning="True"
17+
Color="SlateBlue"
18+
WidthRequest="100"
19+
HeightRequest="100">
20+
<ActivityIndicator.Scale>
21+
<OnPlatform x:TypeArguments="x:Double">
22+
<OnPlatform.Platforms>
23+
<On Platform="iOS" Value="2.0" />
24+
<On Platform="Android" Value="1.5" />
25+
<On Platform="Win" Value="1.5" />
26+
</OnPlatform.Platforms>
27+
</OnPlatform>
28+
</ActivityIndicator.Scale>
29+
</ActivityIndicator>
30+
<Label
31+
Text="Loading..."
32+
FontSize="Large"
33+
HorizontalOptions="Center"
34+
Margin="0,20,0,0"
35+
TextColor="SlateBlue" />
36+
</StackLayout>
37+
</Grid>
38+
</ContentPage.Content>
39+
</ContentPage>
40+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace Bible.Alarm.Views.General;
2+
3+
[XamlCompilation(XamlCompilationOptions.Compile)]
4+
public partial class LoadingPage : ContentPage
5+
{
6+
public LoadingPage()
7+
{
8+
InitializeComponent();
9+
}
10+
}
11+

0 commit comments

Comments
 (0)