1

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:

enter image description here

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

enter image description here

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

John Mc
  • 212
  • 2
  • 16
  • I don't have an explanation for the problem, but I notice that `Application.Current.MainPage = displayPage;` is not consistent with the rest of your code. Specifically, you wouldn't be able to use `MainPage.Navigation.anymethodhere` after that, because you don't have a `NavigationPage` any more. For example, you won't be able to pop back to a previous page. Consider ``Application.Current.MainPage = new NavigationPage(displayPage);`. (This is not related to the problem you encountered; just FYI.) – ToolmakerSteve Aug 24 '22 at 23:27
  • ToolmakerSteve: you are right, that should be a new NavigationPage. It hadn't bothered me because that path is a one page application (since I bypass the LoginPage). In any case, I made the change and, as you would expect, SizeChanged still breaks on PushAsync. – John Mc Aug 25 '22 at 15:14

0 Answers0