Maui Shell backwards navigation - c#

I want the user to be able to move products from one location to another. When done, I want to go back to the products page because he's likely to keep moving products from the same location. I've got two slightly different scenarios.
Scenario 1:
//SourceAisle/SourceLocation/Products/TargetAisle/TargetLocation
Scenario 2:
//SourceAisle/SourceLocation/Products/FavoriteLocation
In both scenarios when the user is done I want to go back to
//SourceAisle/SourceLocation/Products
I could do Shell.Current.GoToAsync("../..") in scenario 1 and Shell.Current.GoToAsync("..") in scenario 2 but I'd like to have some shared logic ie go back to products page.
According to the documentation it's possible to search backwards using either /route or ///route but I'm not sure I understand how it works. I don't want to "push" any new page, I basically want to "pop" all pages up to the page I want to be displayed.
Is it possible using Shell navigation?
NOTES
I had created an extension methods in an older Xamarin project that does exactly the above but I'd prefer not to use it if the Shell provides a native way to do so.
// await Shell.Current.Navigation.PopToAsync<ProductsPage>();
internal static class NavigationExtensions
{
private static readonly SemaphoreSlim _semaphore = new (1, 1);
public static async Task PopToAsync<T>(this INavigation nav, Page nextPage = null, bool animated = true) where T : Page
{
await _semaphore.WaitAsync();
try
{
bool exists = nav.NavigationStack
.Where((p, i) => i <= nav.NavigationStack.Count - 2)
.Any(p => p is T);
if (!exists)
throw new ArgumentException($"The specified page {typeof(T).Name} does not exist in the stack");
for (var index = nav.NavigationStack.Count - 2; index > 0; index--)
{
if (nav.NavigationStack[index] is T)
break;
nav.RemovePage(nav.NavigationStack[index]);
}
if (nextPage is not null)
nav.InsertPageBefore(nextPage, nav.NavigationStack[nav.NavigationStack.Count - 1]);
await nav.PopAsync(animated);
}
finally
{
_semaphore.Release();
}
}
}
EDIT
AppShell.xaml
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
x:Class="MyApp.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:MyApp.Views">
<FlyoutItem Route="SourceAisle" Title="Stock Movement">
<ShellContent ContentTemplate="{DataTemplate views:AislesPage}" />
</FlyoutItem>
</Shell>
AppShell.xaml.cs
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute("SourceAisle", typeof(AislesPage));
Routing.RegisterRoute("SourceLocation", typeof(LocationsPage));
Routing.RegisterRoute("Products", typeof(ProductsPage));
Routing.RegisterRoute("TargetAisle", typeof(AislesPage));
Routing.RegisterRoute("TargetLocation", typeof(LocationsPage));
Routing.RegisterRoute("FavoriteLocation", typeof(FavoritesPage));
}
}
The aisle and location pages are used for both source and target and have some state to deal with the two different cases but that's out of the scope of this question.
From LocationsViewModel I call await Shell.Current.GoToAsync("Products"), from ProductsViewModel I call await Shell.Current.GoToAsync("TargetAisle") and so on. I think this is what's called relative routing and not absolute, not sure if I should do things differently.

You can write a command like this which can recognize the page name then navigate to the target page.
public class MyViewModel: ContentPage
{
public ICommand NavigateCommand { get; private set; }
public MyViewModel()
{
NavigateCommand = new Command<Type>(
async (Type pageType) =>
{
Page page = (Page)Activator.CreateInstance(pageType);
await Navigation.PushAsync(page);
});
}
}
Here is the style of the command.
<TextCell Text="Entry"
Command="{Binding NavigateCommand}"
CommandParameter="{x:Type views:CustomizeSpecificEntryPage}" />

Related

Passing Data on Navigation

I need to pass data to the home page when the user is logged in. I need to pass the username to the home page.
public void Login(string Username, string password)
{
// ..... Do login and if success
var Logindata = database.GetUsername(_usernamelogin);
Application.Current.MainPage.Navigation.PushAsync(new Homepage(Logindata));
}
My method to get the user name is
public Register_person GetUsername(string mail1)
{
return Conn.Table<Register_person>().FirstOrDefault(t => t.UserName == mail1);
}
my home page XAML
In my home page code behind cs, I retrieve the incoming data
public Register_person register_Person;
public Homepage (Register_person loindata)
{
InitializeComponent ();
l1.Text = logindata.UserName;
}
This code works, I can get the username. But I am using MVVM and not sure how to implement this in MVVM.
The pure MVVM way of doing this would be to abstract the navigation and invoke it from your viewmodel (see Prisms navigation service as a reference). Anyway, there can be quite some pitfalls in implementing such a navigation service. If at any rate possible I'd suggest to integrate Prism in your solution and go full MVVM.
There is, however, a hybrid approach that would be way easier to implement, but is not pure MVVM. Assuming that you are not injecting dependencies you could define your binding directly in your XAML
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:App1"
xmlns:generic="clr-namespace:System.Collections.Generic;assembly=netstandard"
x:Class="App1.MainPage"
x:Name="Page">
<ContentPage.BindingContext>
<local:ViewModel />
</ContentPage.BindingContext>
<!-- Your content goes here -->
</ContentPage>
Within your viewmodel you can now define a command to log the user in and an event that is used to communicate to your view that the user was logged in successfully (please note that this code is stripped down to the bare minimum)
class ViewModel
{
/// <summary>Initializes a new instance of the <see cref="T:System.Object"></see> class.</summary>
public ViewModel()
{
LogInCommand = new Command(OnLogIn);
}
private void OnLogIn()
{
// your login logic shall go here
// your password and user name shall be bound
// via other properties
// Invoke the LoggedIn event with the user name
// of the logged in user.
LoggedIn?.Invoke(userName);
}
public event Action<string> LoggedIn;
public Command LogInCommand { get; }
}
From your view you can subscribe to LoggedIn
<ContentPage.BindingContext>
<local:ViewModel LoggedIn="ViewModel_OnLoggedIn" />
</ContentPage.BindingContext>
And of course you need the respective method in your code behind (the .xaml.cs file)
private void ViewModel_OnLoggedIn(string obj)
{
// navigate the other page here
}
This is not a solution you can plug in directly, but should point you to the right direction. Please note that you'll have to bind some Button or something else to LogInCommand, as well as entries to properties for the username and password.

Is there any way to modify a subclass so that all superclasses inherit the change?

My exact problem is I am trying to add logic to all the pages in my Xamarin app, globally.
That is, I am trying to create a "master page" that all my pages inherit from. I put header/footer and navigation logic in this "master page" so that I don't have to rewrite it on every page. ...I say "master page" because Xamarin has something called a master-detail page and that's not what I'm talking about. I'm talking about the concept of a master page like you might have used in ASP.NET Web Forms. The closest analog to that in Xamarin is <ControlTemplate> which, with a little work on your part, gives pretty much the same effect as what you might actually think of as a master page. It kind of looks like this:
App.xaml (the body of the master page)
...
<Application.Resources>
<ResourceDictionary>
<ControlTemplate x:Key="MainPageTemplate">
<StackLayout>
<Label Text="{TemplateBinding HeaderText}" />
<ContentPresenter /> <!-- this is a placeholder for content -->
<Label Text="{TemplateBinding FooterText}" />
</StackLayout>
</ControlTemplate>
</ResourceDictionary>
</Application.Resources>
...
MyPage.xaml (the content that goes in the master page)
<!-- Translation: this page inherits from CoolApp.Views.MyMasterPage.cs -->
<Views:MyMasterPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:Views="clr-namespace:CoolApp.Views"
x:Class="CoolApp.Views.MyPage"
ControlTemplate="{StaticResource MainPageTemplate}">
<Label Text="I am the content" />
</Views:MyMasterPage>
MyMasterPage.cs (the shared header/footer and navigation logic)
// it inherits from ContentPage here, which works fine. so long as your app only uses ContentPages
public class MyMasterPage : ContentPage, INotifyPropertyChanged
{
...
public static BindableProperty HeaderTextProperty;
public string HeaderText
{
get { return (string)GetValue(HeaderTextProperty); }
set
{
HeaderTextProperty = BindableProperty.Create("HeaderText", typeof(string), typeof(SearchPage), value);
OnPropertyChanged("HeaderText");
}
}
// footer logic
// navigation logic
...
}
This master page setup works great for me until I need to use page types other than ContentPage. Specifically, I need a CarouselPage to work exactly the same way. In the above code I could have MyMasterPage.cs inherit from CarouselPage instead of ContentPage and that would be fine except then I would have the global header/footer and navigation logic in two different master pages. I can't just put the logic in a master page that inherits from Page because then my pages would be of type Page instead of ContentPage and CarouselPage. But if I could modify Page directly then ContentPage and CarouselPage would both get my changes. Maybe that would sort of look like this?
I hope it's clear but in case it's not the question I am trying to ask is how can I get my header and footer and navigation logic all into a single master page?
I hear add functionality to a superclass and think extensions. I don't know anything about c# but, for instance, in swift you can add behavior to an existing type. This isn't achieved by modifying a child, however, but you can give a superclass new abilities. Not sure if this is exactly what you might be looking for in your use case
https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods
I have the same reasoning as nbpeth with extensions
if you want to add a property to all your pages (here MyContentPage and MyCarouselPage), you may want to create a single master page for each instead of only one master page, but centralize the navigation logic in an extension class
you could try something like the following (I do not guaranty this code works, I have no Xamarin setup at hand)
public static class PageExtensions
{
public string GetMyValue(this Page page, BindableProperty headerTextProperty)
{
return (string)page.GetValue(headerTextProperty);
}
public void SetMyValue(this Page page, out BindableProperty headerTextProperty, string value)
{
headerTextProperty = null;
if (page is INotifyPropertyChanged notifyPage)
{
headerTextProperty = BindableProperty.Create("HeaderText", typeof(string), typeof(SearchPage), value);
notifyPage.OnPropertyChanged("HeaderText");
}
}
}
public class MyMasterContentPage : ContentPage
{
// ...
public BindableProperty HeaderTextProperty;
public string HeaderText
{
get { return GetMyValue(HeaderTextProperty); }
set { SetMyValue(out HeaderTextProperty, value); }
}
}
public class MyContentPage : MyMasterContentPage, INotifyPropertyChanged
{
}
public class MyMasterCarouselPage : CarouselPage
{
// ...
public BindableProperty HeaderTextProperty;
public string HeaderText
{
get { return GetMyValue(HeaderTextProperty); }
set { SetMyValue(out HeaderTextProperty, value); }
}
}
public class MyCarouselPage : MyMasterCarouselPage, INotifyPropertyChanged
{
}
You would still have some duplicate code but not duplicate logic

Xamarin.Forms variable navigation

I'm currently making a stock control app for Android with Xamarin.Forms.
I have an extra page just for entering the Pruduct ID. I want to use that page in two scenarions:
You pick an product to add/remove them from stock
You pick an product to tell the office that new ones have to be ordered
After picking the product you will be redirectet to:
The "DetailStockmovementPage" in case 1
The "DetailOrderPage" in case 2
The Problem is, that the next Page depends on what button was clicked before.
In my ViewModel I have the following Code to navigate (for scenario 1):
public async Task GoToNextPage(Product product)
{
await Navigation.PushAsync(new DetailStockmovementPage(product), true);
}
How can I make the next Page variable? Can I define a variable "NextPage" in my ViewModel, set it from the View like "vm.NextPage = DetailStockmovementPage" and Navigate like
public async Task GoToNextPage(Product product)
{
await Navigation.PushAsync(new NextPage(product), true);
}
I know that does not work, but I think that poor try made clear what I want to achieve. I mean I could push a string for each page and make an if-query before navigating, but I don't think thats an good way to do it. How can I vary the page to be navigated to?
You could pass the user's choice from the first page into the page where they select the product, and then use that information to decide which page to navigate to. For example:
In your App.cs file add an enum:
public enum NavTarget
{
Order,
Stockmovement
}
Define a property in your VM for the target selected in your menu page:
public NavTarget Target { get; set; }
...and use it in your navigation method:
public async Task GoToNextPage(Product product)
{
if (Target == NavTarget.Stockmovement)
await Navigation.PushAsync(new DetailStockmovementPage(product), true);
else
await Navigation.PushAsync(new WhateverItsCalledPage(product), true);
}
Then set that property in the constructor for your ProductSelectionPage:
public ProductSelectionPage(NavTarget target)
{
InitializeComponent();
// some code that sets the VM for this page
// ...
vm.Target = target;
}

Xamarin.Forms binding not updating after initial value

I'm binding the title of my Xamarin.Forms.ContentPage to a property BuggyTitle in my view model (VM). The VM derives from MvxViewModel. Here's the simplified version:
BuggyPage.xaml:
<?xml version="1.0" encoding="UTF-8"?>
<local:ContentPage Title="{Binding BuggyTitle}"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MyProject.BuggyPage"
xmlns:local="clr-namespace:Xamarin.Forms;assembly=MyProject">
<ContentPage.Content NavigationPage.HasNavigationBar="false">
<Grid>
<ScrollView>
<!--and so on-->
</ContentPage.Content>
</local:ContentPage>
BuggyViewModel.cs:
namespace MyProject
{
[ImplementPropertyChanged]
public class BuggyViewModel : MvxViewModel
{
private Random _random;
public string BuggyTitle {get; set;}
public BuggyViewModel()
{
_random = new Random();
}
public override void Start()
{
base.Start();
BuggyTitle = "" + _random.Next(1000);
RaisePropertyChanged("BuggyTitle"); // this seems to make no difference
}
}
}
There's not much going on in the code behind other than a call to InitializeComponent() in the constructor.
The page is mapped to the VM generically in my project (not actually 'my' project, it's existing design), and it boils down to these (again, simplified) lines of code:
public static Page CreatePage(MvxViewModelRequest request)
{
var viewModelName = request.ViewModelType.Name;
var pageName = viewModelName.Replace ("ViewModel", "Page");
var pageType = (typeof (MvxPagePresentationHelpers)).GetTypeInfo ().Assembly.CreatableTypes().FirstOrDefault(t => t.Name == pageName);
var viewModelLoader = Mvx.Resolve<IMvxViewModelLoader>();
var viewModel = viewModelLoader.LoadViewModel(request, null);
var page = Activator.CreateInstance(pageType) as Page;
page.BindingContext = viewModel;
return page;
}
The problem:
When BuggyPage loads, I initially get the correct value for the title. Whenever it is displayed after that, even though I can see in the debugger that BuggyTitle is getting updated correctly, the change does not appear in the page.
Question:
Why don't updates to BuggyTitle get reflected in the page?
Edit 1:
To further describe the weirdness, I added a Label to my ContentPage, with x:Name="BuggyLabel" and Text="{Binding BuggyLabelText}".
In my code-behind, I added this:
var binding_context = (BindingContext as BuggyViewModel);
if (binding_context != null)
{
BuggyLabel.Text = binding_context.BuggyLabelText;
}
I set a breakpoint at BuggyLabel.Text =. It gets hit every time the page loads, and BuggyLabel.Text already seems to have the correct value (i.e, whatever binding_context.BuggyLabelText is set to). However, the actual page displayed only ever shows what the text in this label is initially set to.
And yes, have clean/built about a million times.
Edit 2 (further weirdness):
I put this in the code-behind so that it runs during page load:
var binding_context = (BindingContext as BuggyViewModel);
if (binding_context != null)
{
Device.BeginInvokeOnMainThread(() =>
{
binding_context.RefreshTitleCommand.Execute(null);
});
}
This again changes values in the debugger, but these changes don't get reflected in the displayed page.
I then added a button to the page and bound it to RefreshTitleCommand, and wham! the page updates its display.
Unfortunately I can't use this. Not only is it incredibly hackish, I can't have the user pressing buttons to have the page display what it's meant to on load.
I wonder if there's some caching going on with MvvmCross or Xamarin.
Answer
You need to add RaisePropertyChanged in BuggyTitle property declaration.
ViewModel
namespace MyProject
{
[ImplementPropertyChanged]
public class BuggyViewModel : MvxViewModel
{
private Random _random;
string _BuggyTitle { get; set; }
public string BuggyTitle
{
get { return _BuggyTitle; }
set { _BuggyTitle = value; RaisePropertyChanged(() => BuggyTitle); }
}
public BuggyViewModel()
{
_random = new Random();
}
public override void Start()
{
base.Start();
BuggyTitle = "" + _random.Next(1000);
}
}
}
-----New Update------
Code behind code
var binding_context = (BindingContext as BuggyViewModel);
if (binding_context != null)
{
Device.BeginInvokeOnMainThread(() =>
{
BuggyLabel.Text = binding_context.BuggyLabelText;
});
}
I don't have any experience at all with Xamarin (but i do want to try it out in the future when i get as comfortable as possible with UWP), but i guess the Data Binding process should be working similar to what i am used to there ...
You are mentioning that you have no problem with the values that are set when the page first loads, however when you actually update the values there's no "linking" to the visual layer, despite at debug time you actually seeing the value being set to something completely different from it's initial state.
Since you are dealing with properties-only viewmodel (Collections for instance in UWP are another level of events which need to be exposed), RaisePropertyChanged seems like the correct choice.
What i cannot understand is if when you first create your page, the Binding which you are creating is at least specified as One-Way mode, so changes in your viewmodel properties are propagated onto your UI when their set accessor methods are called.
You are setting your page context to viewmodel (each i figure is the same as DataContext in UWP/WPF), and therefore you can actually access those properties with the {Binding } markup. But what is the default mode for this operation in Xamarin ? (in UWP it is actually OneWay, and therefore it would work right of the bat for this situation ...).
I have seen that in Xamarin it might be a bit different , since you also have the Default option. Can that be it?
PS. Hopefully this might be useful to you, despite my lack of experience with Xamarin.
Edit2
Implementing the INotifyPropertyChanged,
public class BuggyViewModel : MvxViewModel, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Random _random;
string _BuggyTitle { get; set; }
public string BuggyTitle
{
get { return _BuggyTitle; }
set { _BuggyTitle = value; RaisePropertyChanged(() =>
BuggyTitle); }
}
public BuggyViewModel()
{
_random = new Random();
}
public override void Start()
{
base.Start();
BuggyTitle = "" + _random.Next(1000);
}
protected void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
I was setting a controls binding context from the property changed event of one of its properties.
This made my control stop tracking changes despite everything else binding correctly still, and the control would also correctly bind initially (first time is fine, further changes do not fire the property changed event again).
page.BindingContext = viewModel;

Alternative to hyperlink to navigate?

I'm using WPF (.net 4.5 target) and while i'm quite used to WPF this is the first time i'm working with the page / navigation model.
I see that to navigate to a page i can do something very simple (use an hyperlink) however this is very far from what i want in terms of style (i'd rather start from a button). Is there an alternative to hyperlinks or is my best shot to go with a button + code behind to navigate?
My goal is to provide styled buttons with icons as hyperlinks within a uniformgrid that takes the whole page for the home page.
In Silverlight/WP, there is HyperlinkButton, which is exactly what you're asking for, but apparently that does not exist in WPF. So, it seems that your best bet would be to use the regular Button, along with NavigationService.Navigate in its Command. So, something like this:
<Button Content="Navigate" Command="{Binding NavigateCommand}"
CommandParameter="/Views/SomePage.xaml" />
Then NavigateCommand would be a standard DelegateCommand, using the parameter as the URI:
public class MyViewModel : ViewModelBase
{
public ICommand NavigateCommand { get; private set; }
public MyViewModel()
{
NavigateCommand = new DelegateCommand<string>(url => {
var uri = new Uri(url, UriKind.Relative);
NavigationService.Navigate(uri);
});
}
}
You may instead want to write your own version of HyperlinkButton, which would go something like this:
public class HyperlinkButton : Button
{
public string NavigateUri { get; set; }
protected override void OnClick(EventArgs e)
{
if (NavigateUri != null)
{
var uri = new Uri(NavigateUri, UriKind.Relative);
NavigationService.Navigate(uri);
}
}
}

Categories

Resources