How cool would it be to be able to navigate through our apps without having to define Commands on the ViewModel? Instead, we can simply define our navigation routes directly in our XAML? This idea isn't new, in fact I basically stole it from Jason Smith's Segue portion of his MaterialShell Spec.
I've already spiked this out in a separate repo, hoping to get additional eyes on it.
_(thanks to @brianlagunas for the direction on getting the INavigationService into the Markup Extension)_
<!-- basic nav -->
<Button Text="Go To About" Command"{prism:NavigateTo 'About'}" />
<!-- basic nav without animation-->
<Button Text="Go To About" Command"{prism:NavigateTo 'About', Animated=False}" />
<!-- basic modal nav -->
<Button Text="Go To About" Command"{prism:NavigateTo 'About', UseModalNavigation=True}" />
<!-- custom can navigate support -->
<Button Text="Go To About" Command="{prism:NavigateTo 'About'}" prism:Navigation.CanNavigate="{Binding CanNavigate}" />
<!-- go to new page, but remove one along the way -->
<Button Text="Go To Settings" Command"{prism:NavigateTo '../Settings'}" />
<!-- nav with VM Parameters -->
<Button Text="Go To About"
Command"{prism:NavigateTo 'About'}"
CommandParameters="{Binding MyNavParams}" />
<!-- Go Back -->
<Button Text="Go Back" Command="{prism:GoBack}" />
<!-- Go Back To Root -->
<Button Text="Go Back To Root" Command="{prism:GoBack ToRoot}" />
<!-- Xaml defined parameters -->
<Button Text="Go To About" Command="{prism:NavigateTo 'About'}" >
<Button.CommandParameter>
<prism:XamlNavigationParameters Parent="{x:Reference this}">
<prism:XamlNavigationParameter Key="MainPageViewModel" Value="{Binding .}" />
</prism:XamlNavigationParameters>
</Button.CommandParameter>
</Button>
<!-- can navigate on a parent object-->
<ContentView>
<ContentView prism:Navigation.CanNavigate="False">
<ContentView>
<Button Text="Cannot Navigate" Command="{prism:GoBack}" />
</ContentView>
</ContentView>
</ContentView>
Love it! Having the navigation on the xaml would make our VMs so much cleaner!
I think the next big step is to figure out how to properly cache the INavService for a page so that multiple buttons on the same page do not create different instances of the INavService. When doing this, we must also consider how to remove that INavServce from the cache so we do not create memory leaks.
Also, we must point out that this is not supported in custom ContentViews
Also, we must point out that this is not supported in custom ContentViews
Do the custom ContentViews not pass the same values in the IServiceProvider? I assumed they would. I'm accessing the page from the provided values from there.
Really, does it work? We currently have issues getting the parent Page object from within a CustomView with the current VM based approach, but if you're saying this markup extension gets it no problem, then maybe this is a non-issue. That would be pretty cool.
Well now I have to test it...
You are correct, when it's nested inside a ControlTemplate you only have access to everything up-to and including the ControlTemplate, but not the page hosting the control template.
You have to do this to get it to work
<!-- Control Template -->
<ContentView.ControlTemplate>
<ControlTemplate>
<StackLayout>
<Button Text="{TemplateBinding Text}" Command="{TemplateBinding Command}"/>
</StackLayout>
</ControlTemplate>
</ContentView.ControlTemplate>
// code behind
public static readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text),
typeof(string),
typeof(CustomControlButton),
default(string));
/// <summary>
/// Text summary. This is a bindable property.
/// </summary>
public string Text
{
get => (string) GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
public static readonly BindableProperty CommandProperty = BindableProperty.Create(nameof(Command),
typeof(ICommand),
typeof(CustomControlButton),
default(ICommand));
/// <summary>
/// Command summary. This is a bindable property.
/// </summary>
public ICommand Command
{
get => (ICommand) GetValue(CommandProperty);
set => SetValue(CommandProperty, value);
}
Access it this way
<customControls:CustomControlButton Text="First Text" Command="{prism:NavigateTo 'About'}"/>
@ChaseFlorell this is a fantastic idea... @brianlagunas I already have an idea that would ensure we're using a single instance of INavigationService... actually shouldn't be terribly difficult to do with a XAML extension.
@dansiegel I would absolutely love to hear your idea. I'm thinking of going down the attached property path, and have it pretty well sorted out, but I don't know a clean way to listen for Popped on the page in order to null out the attached property.
There is a Github project that has implemented something like this: Segues - https://github.com/chkn/Xamarin.Forms.Segues
Yup that is the original spike we used to test the idea. Its not getting a fuller implementation to go into Forms. We will want to get feedback from the Prism developers to make sure that it is useful to Prism as well so that Prism can hopefully take advantage of Segues.
Segues will eventually offer more than just A to B functionality, but transition capabilities as well.
@jassmith To fully understand Xamarin's expected function, we'll need some bits to play with 馃槂. As you know, Prism already have a very powerful navigation framework which includes deeplinking, flexible parameters, and with various options for developers to hook into the navigation process to respond to events such as CanNavigate, OnNavigatingTo, OnNavigtedTo and OnNavigatedTo.
I'm not sure we would be able to utilize anythign that is built into Forms as we would want to make sure all of the current navigation features are kept and function as expected. However, we would definitely take what you create and modify it to fit our needs. It may be as simple as deriving a custom markup extension from your implementation and overriding some methods that you provide to inject our navigation service that does the actual navigation.
What we want to avoid having is two different ways of navigation and two different navigation behaviors. The behavior of navigation in Prism should be the same regardless of if you are within the MaterialShell, or not.
It is hard for me to understand how navigation in the MaterialShell will actually behave. There are no screenshots to accompany the code snippets, and the instructions aren't quite clear enough for me to visualize how the navigation stack is modified when navigating within the MaterialShell.
For example:
<ShellItem>
<local:MyFirstPage x:Name="page1" />
<local:MySecondPage x:Name="page2" />
<local:MyThirdPage x:Name="page3" />
</ShellItem>
This snippet doesn't really make sense. You have a parent object called ShellItem, but multiple items within that element. What is the context of these pages? are they within a TabbedPage/TabbedView, or something else?
When you get a nightly build of your experimental branch, we wil most definitely play around with it and see if we can use what you have, derive from what you have, or create something new to support this.
Pull out an android phone, scroll to the end of the spec where it talks about recreating the google play store and follow along with the Google Play Store on your phone :)
Sorry that really is the best I can offer at the moment. You could also build the shell branch if you want but that only supports iOS currently.
I really like the idea of a Segue/Transition to be defined in Xaml. I wonder if it can be isolated from the actual navigation though? IE: Go To This Page, Using This Segue?!?!
await navigationService.NavigateAsync("path/to/page", segue: new StarburstSegue());
or
<Button Text="Starburst"
Command="{prism:NavigateTo 'path/to/page', Segue={StaticResource StarburstSegue}}" />
How about sending an event (e.g. NatvigateEvent) when such command is called ? and the navigation service is basically subscribes to this event and navigate according to its parameters ?
@ahmad-crossplatform, are you talking about doing this within xaml? What exactly would it look like?
@ChaseFlorell I was more commenting on Brian's points regarding having new instances of NavigationService for each command.
U suggested to have it like this
<!-- Xaml defined parameters -->
<Button Text="Go To About" Command="{prism:NavigateTo 'About'}" >
<Button.CommandParameter>
<prism:XamlNavigationParameters Parent="{x:Reference this}">
<prism:XamlNavigationParameter Key="MainPageViewModel" Value="{Binding .}" />
</prism:XamlNavigationParameters>
</Button.CommandParameter>
</Button>
So my suggestion of prism:NavigateTo is instead of creating a new prism service, instead the actual NavigateTo that is used by Xaml sends an event to the NavigationService that is already registered.
Maybe NavigationService should be registered as a singleton?
I do not know how easy or realistic implementing this. It is just an idea.
Another suggestion is to have NavigationService as globally accessible, static class, or a singleton , as It does not really save any state. This way we do not have to think of memory leak or something .
I'm not sure a singleton is the best approach, and although I cannot speak for @brianlagunas, I'm pretty sure he'd say the same thing.
The appraoch I'm looking to take is to attach the NavigationService to the page via an Attached property. As long as the IServiceProvider is supplying the same Page instance in every request, then everything _should_ work as expected.
_Edited_
I'm also looking at supporting MasterDetailPage and TabbedPage based on some feedback from @dansiegel. The IServiceProvder provides the actual Content page as the rootObject of the Detail, and the "MasterDetail" page as the rootObject of the Master. Dan had asked to be able to specify it directly, however after further conversation with Brian, this might be a non-issue. We just need to use the rootObject that the IServiceProvider provides.
_/Edited_
<MasterDetailPage x:Name="this">
<MasterDetailPage.Master>
<ContentPage prism:Navigation.Parent="{x:Reference this}">
<Button Text="Foo" Command="{prism:NavigateTo '/MyMaster/NavigationPage/MyDetail'}" />
</ContentPage>
</MasterDetailPage.Master>
<MasterDetailPage>
Although this is more verbose, I still think this would be good to have as an option:
<MasterDetailPage x:Name="this">
<MasterDetailPage.Master>
<ContentPage>
<Button Text="ViewA" Command="{prism:NavigtateTo ViewA,Page={x:Reference this}}" />
</ContentPage>
</MasterDetailPage.Master>
</MasterDetailPage>
The behavior would be:
var page = GetPage(); // ContentPage from example above
if(Page != null)
{
GetNavigationService(Page);
}
else if(Navigation.GetParent(page) != null)
{
GetNavigationService(Navigation.GetParent(page));
}
else
{
GetNavigationService(page);
}
@dansiegel is there ever a time in Prism when the navigation service is an instance of the nested page? I thought that the Prism opinion was always to use the MD page and not the nested page.
Interestingly, the IServiceProvider gives the MD page as the RootObject for elements inside the Master, but not for elements inside the Detail.
When dealing with a Master in the MDP, the VM should always be the MDP VM. When within the Detail of the MDP, the VM should always be of the Detail, and not the MDP. So based on your description of the IServiceProvider behavior, it works as expected.
@brianlagunas but if that's the case, why do we need to define the Page in xaml at all? Unless I'm missing something, it seems as though Prism figures this out for us already.
We don't need to provide a Page in XAML unless the IServiceProvider can't give it to us, which seems to be the issue you ran into in your post above.
No, my comment was based around @dansiegel's recommendation to be able to provide a specific page instance in the Markup Extension. I never ran into an issue per-se, but Dan wanted to be able to do something like this.
<Button Text="ViewA" Command="{prism:NavigtateTo ViewA,Page={x:Reference this}}" />
The Markup Extension always gives me the Page that the control is in. If "Detail" it gives me the detail page, if "Master" it gives me the MasterDetail page.
I thought it was related to your comment:
I'm also looking at supporting MasterDetailPage and TabbedPage. The IServiceProvider doesn't actually send that page but rather the contentpage within. I think we need a way to supply the page via an attached Property
If this isn't an issue, then great.
Correct, not an issue, only based on feedback from @dansiegel. I also misspoke when I said it only sends back the page within the MD page. I'll correct that statement via an edit.
@ChaseFlorell I was operating under the assumption that you were getting the ContentPage and not the MDP... that said, I would be curious what you get if your Master is a NavigationPage with a ContentPage.
Ultimately I would believe having a Page property on the XAML Extension would be good for the edge cases none of us are thinking of. It's a lot easier IMO to build it in from the start. You could think of it like the Binding Source property... most of the time you never set it, and it picks it up automatically, but in those scenarios where you need it you have it.
I would be curious what you get if your Master is a NavigationPage with a ContentPage.
I still get the MasterDetailPage 馃憤 馃槃
<MasterDetailPage.Master>
<NavigationPage Title="Master">
<x:Arguments>
<ContentPage >
<StackLayout>
<Button Text="Go to about (1)" Command="{prism:NavigateTo 'About'}" prism:Navigation.CanNavigate="{Binding CanNavigate}" >
<Button.CommandParameter>
<prism:XamlNavigationParameters Parent="{x:Reference this}">
<prism:XamlNavigationParameter Key="MainPageViewModel" Value="{Binding .}" />
</prism:XamlNavigationParameters>
</Button.CommandParameter>
</Button>
<Button Text="Go to about (2)" Command="{prism:NavigateTo 'About'}" prism:Navigation.CanNavigate="{Binding CanNavigate}" >
<Button.CommandParameter>
<prism:XamlNavigationParameters Parent="{x:Reference this}">
<prism:XamlNavigationParameter Key="MainPageViewModel" Value="{Binding .}" />
</prism:XamlNavigationParameters>
</Button.CommandParameter>
</Button>
<Button Text="Go to about (3)" Command="{prism:NavigateTo 'About'}" prism:Navigation.CanNavigate="{Binding CanNavigate}" >
<Button.CommandParameter>
<prism:XamlNavigationParameters Parent="{x:Reference this}">
<prism:XamlNavigationParameter Key="MainPageViewModel" Value="{Binding .}" />
</prism:XamlNavigationParameters>
</Button.CommandParameter>
</Button>
</StackLayout>
</ContentPage>
</x:Arguments>
</NavigationPage>
</MasterDetailPage.Master>
Ultimately I would believe having a Page property on the XAML Extension would be good for the edge cases none of us are thinking of. It's a lot easier IMO to build it in from the start. You could think of it like the Binding Source property... most of the time you never set it, and it picks it up automatically, but in those scenarios where you need it you have it.
This is perfectly valid
Merged
Sorry guys, what namespace is prism in Command="{prism:GoBack}"?
I get this error:
Type GoBack not found in xmlns clr-namespace:Prism.Mvvm;assembly=Prism.Forms
Ahhh thank you!
I got confused because i already had this in the xaml page:
xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Most helpful comment
I think the next big step is to figure out how to properly cache the INavService for a page so that multiple buttons on the same page do not create different instances of the INavService. When doing this, we must also consider how to remove that INavServce from the cache so we do not create memory leaks.