aturnbul
aturnbul

Reputation: 397

How do I pre-load ShellPage during Activation in a WinUI 3 NavigationView Desktop project?

I'd like to pre-load the ShellPage in a WinUI 3 (v1.1.5) Desktop application. That is, during Activation (called by

await App.GetService<IActivationService>().ActivateAsync(args);

in the OnLaunched handler of the App class), I'd like to make sure ShellPage is loaded before any of the navigation pages are displayed. I've changed the service configuration to include

    services.AddSingleton<ShellPage>();
    services.AddSingleton<ShellViewModel>();

in the constructor for the App class which should mean only one of each of ShellPage and ShellViewModel will be instantiated for the app run but the question is when are they fully provisioned?

The normal progression is that the Activation step first assigns ShellPage to MainWindow.Content, then navigates to MainPage (these are the names for the default project). Because MainPage is actually loaded into a Frame on ShellPage, it seems layout for MainPage happens before ShellPage layout is completed.

Any idea how I do this on initial startup? This is only an issue when the first Page is presented. After that, ShellPage is reused.

Upvotes: 1

Views: 934

Answers (2)

Andrew KeepCoding
Andrew KeepCoding

Reputation: 13666

By default, TemplateStudio's navigation re-instantiates pages for every navigation. It doesn't use the ServicesProvider, so registering your pages as singleton won't help.

If you want to keep your page instances, you need to set NavigationCacheMode to Required on your pages. This way, your pages will be cached even after you navigate away.

Still, your pages won't be instantiated until you navigate to them once at least. In order to instantiate all of your pages at the very beginning, you need to navigate through them at least once.

You can get all the NavigationViewItems with a method like this.

private static IEnumerable<NavigationViewItem> GetNavigationViewItems(IEnumerable<object> items)
{
    foreach (var item in items.OfType<NavigationViewItem>())
    {
        yield return item;

        foreach (var grandChild in GetNavigationViewItems(item.MenuItems.OfType<NavigationViewItem>()))
        {
            yield return grandChild;
        }
    }
}

And use it like this in the NavigationViewService's Initialize method.

[MemberNotNull(nameof(_navigationView))]
public void Initialize(NavigationView navigationView)
{
    _navigationView = navigationView;
    _navigationView.BackRequested += OnBackRequested;
    _navigationView.ItemInvoked += OnItemInvoked;

    IEnumerable<NavigationViewItem> menuItems = 
    GetNavigationViewItems(_navigationView.MenuItems);

    foreach (var item in menuItems)
    {
        if (item.GetValue(NavigationHelper.NavigateToProperty) is string pageKey)
        {
            _navigationService.NavigateTo(pageKey);
        }
    }
}

Upvotes: 2

aturnbul
aturnbul

Reputation: 397

A little clarification first, and then the answer I found to the issue.

Andrew's answer (above) is great for instantiating all of the Pages in the NavigationView at startup but the very first page loaded still would not have access to a fully loaded ShellPage in its constructor (and thus, a fully populated element tree). Andrew is right that the NavigationViewItems (Pages) don't persist by default, but the ShellPage does as it's part of the UI. Specifically, it is the content of the MainWindow and defines a Frame into which NavigationViewItems are loaded. Regardless of which Page is displayed, it's the same instance of the ShellPage people see.

The issue arises because of the order in which Activation (specifically, the DefaultActivationHandler) is done at App startup. When the App starts, it calls

await App.GetService<IActivationService>().ActivateAsync(args);

which does

    // Set the MainWindow Content.
    if (App.MainWindow.Content == null)
    {
        _shell = App.GetService<ShellPage>();
        App.MainWindow.Content = _shell ?? new Frame();
    }

and navigates to the first Page (loads the first Page into the NavigationView.Frame by calling DefaultActivationHandler) before finishing the loading of ShellPage. Thus, ShellPage is not fully loaded (ShellPage.IsLoaded == false) when MainPage is loaded.

To fully instantiate ShellPage before any of the NavigationViewItem Pages are loaded, simply change the loading sequence. First, defer the navigation to the first page (whichever you choose) by editing HandleInternalAsync in DefaultActivationHandler.cs to

    protected async override Task HandleInternalAsync(LaunchActivatedEventArgs args)
    {
        //_navigationService.NavigateTo(typeof(MainViewModel).FullName!, args.Arguments);

        await Task.CompletedTask;
    }

Move the navigation to the OnLoaded handler in ShellPage.xaml.cs:

    private void OnLoaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
    {
        TitleBarHelper.UpdateTitleBar(RequestedTheme);

        KeyboardAccelerators.Add(BuildKeyboardAccelerator(VirtualKey.Left, VirtualKeyModifiers.Menu));
        KeyboardAccelerators.Add(BuildKeyboardAccelerator(VirtualKey.GoBack));

        App.GetService<INavigationService>().NavigateTo(typeof(MainViewModel).FullName!);
    }

All Pages now receive a loaded ShellPage when navigated to, regardless of order.

Upvotes: 3

Related Questions