The problem I find is that the push/pop navigation pattern seems to destroy my page’s SizeChanged event. Let’s go over the details. The following are the important bits of my example TesterPage which is the one with SizeChanged event.
THIS ONLY HAPPENS ON WINDOWS. When I deploy the same code to my MacBook and run under MacCatalyst, SizeChanged is triggered just fine.
All of my Maui pages are based on a custom class BaseControl that inherits from ContentPage. This is only important to know because the methods in question are designed to look for that base class. The only other thing the BaseControl does is manage the IsBusy state.
<?xml version="1.0" encoding="utf-8" ?>
<myCustomControls:BaseControl
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:common="clr-namespace:MBC3"
xmlns:myCustomControls="clr-namespace:MBC3.Controls.Custom_Controls"
xmlns:myUserControls="clr-namespace:MBC3.Controls.User_Controls"
x:Class="MBC3.Program.Views.TesterPage"
NavigationPage.HasNavigationBar="False"
NavigationPage.HasBackButton="False"
SizeChanged="ContentPage_SizeChanged"
>
There are only two pages: LoginPage and TesterPage
TesterPage invokes its SizeChanged event:
SizeChanged="ContentPage_SizeChanged"
The handler is in the code behind
using MBC3.Controls.Custom_Controls;
namespace MBC3.Program.Views;
public partial class TesterPage : BaseControl
{
public TesterPage()
{
InitializeComponent();
}
private void ContentPage_SizeChanged(object sender, EventArgs e)
{
var a = sender; // this line is just for allowing a breakpoint
}
}
All of my Maui ViewModels inherit from a custom class called VMBase which handles the INotityPropertyChanged stuff and the methods in question look for this base class.
The only viewmodels are LoginViewModel and TesterViewModel
To navigate by viewmodel, I wrote a NavigationService that, among its public interface, has Configure(Type viewModel, Type view) and NavigateTo<T>() where T : VMBase
When the app starts, the App constructor does the following:
// register the types
NavigationService.Configure(typeof(LoginViewModel), typeof(LoginPage))
NavigationService.Configure(typeof(TesterViewModel), typeof(TesterPage))
// show login page
MainPage = new NavigationPage(new LoginPage());
What’s going on is that NavigationService maintains an internal List of these Type pairs so that the act of navigation first looks into the list to extract the types and create instances if needed. Instances are then cached in a another List.
The flow of my app is to first open the LoginPage which just has username and password and a button. Upon successful login, the LoginViewModel navigates to the TesterPage.
// go to TesterPage using VM navigation
NavigationService.NavigateTo<TesterViewModel>();
The NavigateTo method definition
/// <summary>
/// Navigation by ViewModel type
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="parameter"></param>
/// <exception cref="Exception"></exception>
public async void NavigateTo<T>(BriefcaseDetailParameter parameter = null) where T : VMBase
{
if (pages.TryGetValue(typeof(T), out Type pageType))
{
try
{
T viewModel;
BaseControl displayPage;
// see if the instances for this type already exist
displayPage = currentViews.FirstOrDefault(t => t.GetType() == pageType);
if (displayPage != null)
viewModel = (T)displayPage.BindingContext;
else
{
// if not, create them
viewModel = (T)Activator.CreateInstance(typeof(T));
displayPage = (BaseControl)Activator.CreateInstance(pageType);
}
displayPage.SetNavigationArgs(parameter);
// make sure the page is bound to the viewmodel
displayPage.BindingContext = viewModel;
// capture the dispatcher and parameter
viewModel.Dispatcher = displayPage.Dispatcher;
viewModel.Parameter = parameter;
// cache the page
currentViews.Add(displayPage);
if (MainPage != null)
// push to the screen
await MainPage.Navigation.PushAsync(displayPage); // THIS BREAKS SizeChanged
else
// or if this is the first screen, just set it to display
Application.Current.MainPage = displayPage; // THIS DOES NOT BREAK SizeChanged
// fix the dimensions
if (displayPage.HeightRequest <= 0 && displayPage.Height > 0)
displayPage.HeightRequest = displayPage.Height;
if (displayPage.WidthRequest <= 0 && displayPage.Width > 0)
displayPage.WidthRequest = displayPage.Width;
// run any viewmodel initiation logic
viewModel.Init();
// signal that this is complete
NavigateToComplete?.Invoke(parameter ?? new BriefcaseDetailParameter());
}
catch (Exception ex)
{
var a = ex;
}
}
else
{
throw new Exception("No such page with viewmodel type : " + typeof(T));
}
}
}
At runtime, after NavigateTo<TesterViewModel>() is called, I put a breakpoint at the PushAsync:
See above that SizeChanged is still defined right before I push it to the MainPage. Then after MainPage is set, the SizeChanged handler is immediately called
However, in Windows, when I subsequently resize the window the handler is never called again.
If I skip the login and open TesterPage right off,
//MainPage = new NavigationPage(new LoginPage());
MainPage = new NavigationPage(new TesterPage());
Resize gets called all the time. Note, the above change does not hook up the viewmodel. LoginPage is the only one that has its ViewModel set in its code-behind. The intention is that other pages have their ViewModels attached by NavigateTo. So let’s use NavigateTo instead of setting MainPage directly.
//MainPage = new NavigationPage(new LoginPage());
//MainPage = new NavigationPage(new TesterPage());
NavigationService.NavigateTo<TesterViewModel>();
Because of this if branch, an uninitialized MainPage calls the else case.
if (MainPage != null)
// push to the screen
await MainPage.Navigation.PushAsync(displayPage);
else
// or if this is the first screen, just set it to display
Application.Current.MainPage = displayPage;
When that happens, Resize also gets called all the time. This tells me that the ViewModel is not responsible to the problem.
The only difference is the PushAsync! Why would doing that break the handler?
The PushAsync is implemented by Microsoft.
namespace Microsoft.Maui.Controls
{
public interface INavigation
{
IReadOnlyList<Page> ModalStack { get; }
IReadOnlyList<Page> NavigationStack { get; }
void InsertPageBefore(Page page, Page before);
Task<Page> PopAsync();
Task<Page> PopAsync(bool animated);
Task<Page> PopModalAsync();
Task<Page> PopModalAsync(bool animated);
Task PopToRootAsync();
Task PopToRootAsync(bool animated);
Task PushAsync(Page page);
Task PushAsync(Page page, bool animated);
Task PushModalAsync(Page page);
Task PushModalAsync(Page page, bool animated);
void RemovePage(Page page);
}
Edit: I'm using Visual Studio 17.3.2 and net6.0-Windows10.0.19041.0