Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Page-based navigation system #10557

Open
kekekeks opened this issue Mar 4, 2023 · 16 comments
Open

Page-based navigation system #10557

kekekeks opened this issue Mar 4, 2023 · 16 comments

Comments

@kekekeks
Copy link
Member

kekekeks commented Mar 4, 2023

Since we are in need of a mobile-first navigation system and currently lack resources and imagination
to design our own one, the proposal is to use Page-based navigation like Xamarin.Forms which is
already familiar to people doing mobile apps.

So this spec aims to provide a familiar API while adding some MVVM-friendliness that Xamarin.Forms lacks.

Xamarin.Forms Shell's counterpart is out of the scope of this spec.

Overview

The basic element of the navigation system is a Page. There can be only one "active" page, which is the
inner-most page of the navigation system:

image

Each navigation page that has children only has at most one active child towards which it forwards any platform information and
asks it for the desired titlebar color or window state.

Any platform events like system Back button being pressed are sent to the innermost active page.
Since the active page has nothing to do with the input focus, the page host is required to traverse the tree to find the element to use as the RoutedEvent's source.

View location

To make the system more MVVM-friendly, we are using object instead of Page.
To convert an object to a page, we should do the following:

  1. if object is not a Control, we are using our standard IDataTemplate lookup mechanism and convert it into a Control
  2. if original object or IDataTemplate-produced control is not a Page, wrap it into a ContentPage

PageNavigationHost

class PageNavigationHost : Control
{
    public object Page { get; set; }
}

This control acts as the page system host and is supposed to be used as ISingleViewApplicationLifetime.MainView or as Window.Content

This is the only control in this spec that would be communicating with TopLevel's IInsetsManager directly.

The only property is Page which is the root page of the app's navigation system.

Page

The base class for more specialized pages. It's not supposed to be used directly by the user code.

public class Page : TemplatedControl
{
    // The page's title, can be used by the parent page or by the page host
    public virtual string? Title { get; set; }
    
    // The page's icon, can be used by the parent page or by the page host
    public virtual IImage Icon { get; set; }
    
    // The safe area which identifies portions of the page area that are not obscured by any platform-drawn elements.
    // This property is set by the parent page or by the page host. 
    // The parent page is responsible for adjusting this value to account for padding
    //   that's already added by the parent page's content
    public virtual Thickness SafeAreaPadding { get; set; }
    
    // The system bar theme that is desired by the current page. 
    // The parent Page can ignore this property if it displays its own content adjacent to the System Bar  
    public virtual SystemBarTheme? SystemBarTheme { get; set; }
    
    // The child page that should be used as the source for navigation-related RoutedEvent
    public virtual Page? ActiveChildPage { get; }
}

ContentPage

This class is supposed to serve for used-defined content mostly acts in the same way as a UserControl, but can manage
SafeAreaPadding automatically

public class ContentPage : Page
{
    // Page's content, usually defined in MyPage.axaml
    public object Content { get; set; }
    
    // If this property is true, SafeAreaPadding is added to Padding property
    public bool AutomaticallyApplySafeAreaPadding {get; set;} = true;
}
<ControlTheme x:Key="SimpleContentPage" TargetType="ContentPage">
      <Setter Property="Template">
        <ControlTemplate>
          <ContentPresenter Name="PART_ContentPresenter"
                            HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                            VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
                            Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            Content="{TemplateBinding Content}"
                            ContentTemplate="{TemplateBinding ContentTemplate}"
                            CornerRadius="{TemplateBinding CornerRadius}" />
        </ControlTemplate>
      </Setter>
    </ControlTheme>

ContentPage uses AutomaticallyApplySafeAreaPadding, Padding and SafeAreaPadding properties to determine the Padding of PART_ContentPresenter.

MultiPage

This is a base class for pages that can have multiple child pages

public class MultiPage : Page
{
    // The list of pages. The last one is considered to be the active one.
    public IEnumerabe? Pages { get; set; }
    
    // A template to use for pages, overrides standard IDataTemplate lookup just like with ItemsControl
    public IDataTemplate? PageTemplate {get; set;}
}

NavigationPage

This Page maintains a navigation stack.

public class NavigationPage : MultiPage
{
    // Navigation bar background
    public IBrush BarBackground { get; set; }
    // Navigation bar text color
    public IBrush BarTextColor { get; set; }
    
    // Attached properties
    
    // Sets the text of the Back button when this page is active
    public static void SetBackButtonTitle(Page page, string title);
    
    // Hides or shows the Back button and controls OS back button behavior when this page is active
    // True by default
    public static void SetHasBackButton(Page page, bool hasButton);
    
    // Sets a custom control to be used as the title when this page is active, when it's null, Page.Title property is used
    public static void SetTitleView(Page page, object control);
    
    // Sets an icon to be drawn at the left of the title when this page is active
    public static void SetTitleIcon(Page page, IImage image);
}

SelectingMultiPage

This is a base class for MultiPages that can have user-initiated selection

public class SelectingMultiPage : MultiPage
{
    // The currently selected page
    public object SelectedPage {get; set;}
}

CarouselPage

A page that users can swipe from side to side to display pages of content, like a gallery.

public class CarouselPage : SelectingMultiPage
{
}

TabbedPage

A page that displays an array of tabs across the top of the screen, each of which loads content onto the screen.

This page should be using child Page's Title and Icon properties for the tab items

public class TabbedPage : SelectingMultiPage
{
    // Tab bar background
    public IBrush BarBackground { get; set; }
    // Tab bar text color
    public IBrush BarTextColor { get; set; }
}

MasterDetailPage

A page that manages two panes of information: A master page that presents data at a high level, and a detail page that displays low-level details about information in the master.
This should be implemented using our SplitView.

public class MasterDetailPage : Page
{
    public object Master {get; set;}
    public object Detail {get; set;}
    public bool IsPresented {get;set;}
}

System Back button handling

When Android's back button is pressed, it should be intercepted by PageNavigationHost
and raised as PageNavigationSystemBackButtonPressed routed event on the inner-most active Page.
NavigationPage should mark it as Handled if HasBackButton == true and
it has successfully popped the active page from the navigation stack

@maxkatz6
Copy link
Member

maxkatz6 commented Mar 5, 2023

  1. I assume we allow any way of hierarchy - TabbedPage in in the NavigationPage or the other way around?
  2. Insets don't have SystemBarTheme anymore. Instead we have theme variants.
  3. Shouldn't SafeAreaPadding be readonly (private setter) and used only in controls templates? I.e. there is no need for user to set this property, and no need to read it unelss they restyle control.
  4. IImage Icon - once again I am thinking about uwp IconElement control. Could be just a Control at this point.
  5. string? Title - why string only?
  6. AutomaticallyApplySafeAreaPadding - we used DisplayEdgeToEdge naming in InsetsManaged, should be more consistent here.
  7. Whos responsibility is to synchronize current page of MasterDetailPage + NavigationPage or TabbedPage + NavigationPage combinations? I.e. MasterDetailPage shows the list of pages which are selected on the inner navigation page.

In the future we might want to support a special Page - mix of MasterDetailPage and TabbedPage specialized on the items source. Similarly how NavigationView works. And this would allow us to implement all-in one NavigationView-like control as an alternative to xamarin Shell control, which isn't compatible with Pages.

@kekekeks
Copy link
Member Author

kekekeks commented Mar 5, 2023

  1. Yes
  2. Setting the system bar color is useful regardless of a theme variant, e. g. when you are displaying some non-app content (e. g. images) and want it to stay readable. We can skip this property for the MVP though
  3. The parent view should be able to set it for a child, since it needs to adjust the padding to account to its own content
  4. We can have it as object then
  5. The same title can be displayed in a tab control and in stack navigation title. I guess we can make it an object too?
  6. no preference
  7. I'm not sure that I understand your question. For any Page there is always at most one active child Page. The parent page doesn't need to know anything about its grandchildren. The only scenario when we need the innermost active page is raising system-triggered routed events like android back button.

@jack775544
Copy link

Is there any plan for the work done in this to be integrated with the already existing ReactiveUI routing or is this going to be a complete replacement for that system?

@kekekeks
Copy link
Member Author

kekekeks commented Mar 9, 2023

We have a policy to not depend on 3rd party MVVM frameworks. Avalonia should be 100% usable without an MVVM framework.
So any existing RxUI features are orthogonal to the page-based navigation system.

@maxkatz6
Copy link
Member

maxkatz6 commented Mar 9, 2023

Note, it could be possible in the other way around. I.e. make ReactiveUI suppose new avalonia types. Keeping implementations separated in Avalonia.ReactiveUI. As a reference https://github.com/reactiveui/ReactiveUI/blob/de86956daac9efcb20fabc7cc4ef3a438d0450d2/src/ReactiveUI.Maui/ReactiveMasterDetailPage.cs#L16 https://github.com/reactiveui/ReactiveUI.Samples/blob/main/Xamarin/MasterDetail/MasterDetail/MainPage.xaml
It's out of scope of initial prototype though.

This was referenced Mar 9, 2023
@stevemonaco
Copy link
Contributor

Please keep view caching configuration in mind for either this feature or the docs. If not built-in, the user needs to find a "seam" to manage a cache within and it might not be obvious where. eg. The ViewLocator shipped in the MVVM template is one potential place that can cover certain scenarios.

@agnauck
Copy link

agnauck commented Apr 20, 2023

I like the design, however I have 1 question.

Does the API trigger some load/unload (dispose) event on content pages?
When using navigation its often required to execute cleanup code, persist state or similar.

@kekekeks
Copy link
Member Author

Wont OnAttachedToVisualTree/OnDetachedFromVisualTree be sufficient?

@agnauck
Copy link

agnauck commented Apr 20, 2023

Wont OnAttachedToVisualTree/OnDetachedFromVisualTree be sufficient?

yes it should sufficient. But I think it would be nice to have wrappers for this in such an API.

@maxkatz6
Copy link
Member

Loaded/Unloaded as well.

But I think it would be nice to have wrappers for this in such an API.

What kind of wrappers? Normally, these events are control's lifetime is managed.

@agnauck
Copy link

agnauck commented Apr 21, 2023

OK, I will try to replace ReactiveUIs router in one of my POCs and give more feedback when done

@mysteryx93
Copy link

I'll point out that MvvmDialogs has a similar navigation system built-in
https://github.com/mysteryx93/HanumanInstitute.MvvmDialogs

It doesn't provide all the features listed above, but handles navigation and back button automatically.

Does the API trigger some load/unload (dispose) event on content pages?

And it provides Load/Closed events to the ViewModel automatically.

@MichaelRumpler
Copy link

MichaelRumpler commented Nov 2, 2023

Note that "master" is now considered a bad word in the USA. Xamarin.Forms has therefore renamed the MasterDetailPage to FlyoutPage a few years ago. It's called "Flyout" because the left part is not always visible. It may fly out on press of the hamburger icon or setting a property manually. On a tablet it may make sense to show it always, but on a phone space is scarce. You don't want to waste it.

Please also consider page transitions. There may be different animations when a new page is pushed/popped.

IMHO a string Title does make sense when you show it in a navigation tree or tab name. But in the title bar people also need to show buttons or other elements right (and sometimes even left!) of the page title. So additional anchors where people can add their content would be needed (e.g. PART_TitleBarLeft, PART_TitleBarRight).

@mysteryx93
Copy link

I'm realizing that there is no TabbedPage or swipe controls whatsoever in Avalonia, which makes it hard to develop any real mobile app!

Any development on this?

@damian-666
Copy link

damian-666 commented Jan 15, 2024 via email

@stefanolson
Copy link

I'm realizing that there is no TabbedPage or swipe controls whatsoever in Avalonia, which makes it hard to develop any real mobile app!

Any development on this?

You're very right. Because the mobile for Avalonia is quite new it seems that there are just aren't the controls that are necessary to build mobile apps yet. This is why uno built their own toolkit which has navigation/shell/tabs and other bits built in. At the very least I need a listbox drag left and right to remove etc... I'm sure there are many other things that are required.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants