MSibalik
MSibalik

Reputation: 65

Defining routes for and navigating to pages beyond those specified in AppShell.xaml

I have a Xamarin shell application with the following defined in AppShell.xaml:

   <TabBar Route="tabs" x:Name="tbTabs">
        <Tab Title="Home"
             Route="home"             
             Style="{StaticResource BaseStyle}"
             Icon="home.png" >
            <ShellContent>
                <views:LandingPage />
            </ShellContent>
        </Tab>
        <Tab Title="Services"
             Route="service"             
             Style="{StaticResource BaseStyle}"
             Icon="service.png">
            <ShellContent>
                <views:ServicesPage/>
            </ShellContent>
        </Tab>
   </TabBar>

My goal is to Navigate from the home page to pages that are defined outside of the shell structure by clicking an icon on the Home page to go to a page that lists a history of notifications received by the app. Clicking a notification in the list, takes you to a detail page for that notification.

Home <-> Notification History <-> Notification Details

The routes for the pages that are not defined as part of the shell in AppShell.xaml are defined in AppShell.xaml.cs as:

Routing.RegisterRoute("notificationhistorypage", typeof(NotificationHistoryPage));
Routing.RegisterRoute("notificationdetailspage", typeof(NotificationDetailsPage));

The problem is, when I click a notification on the notification history page, the navigation animation shows navigation back to the home page before navigating to the detail page. When I navigate back from the detail page, rather than going back to the list of notifications, it goes directly to the home page.

I have tried a number of things to fix this. All seem to exhibit the same behavior, or cause an exception.

From what I read at https://github.com/xamarin/Xamarin.Forms/issues/5790 it would seem more logical that the routes should be defined as:

Routing.RegisterRoute("//tabs/home/notificationhistorypage", typeof(NotificationHistoryPage));
Routing.RegisterRoute("//tabs/home/notificationhistorypage/notificationdetailspage", typeof(NotificationDetailsPage));

Unfortunately, this produces an exception at run time:

"unable to figure out route for: /notificationhistorypage\nParameter name: uri"

For navigation to get from the home page to the the notification history page, I’ve tried these:

await Shell.Current.GoToAsync("/notificationhistorypage");

await Shell.Current.GoToAsync("//tabs/home/notificationhistorypage");

For navigation from the notification history page to the the notification details page, I've tried these:

await Shell.Current.GoToAsync($"/notificationdetailspage?itemid={notification.ID}", true);

await Shell.Current.GoToAsync($"///tabs/home/notificationhistorypage/notificationdetailspage?itemid={notification.ID}", true);

await Shell.Current.Navigation.PushAsync(new NotificationDetailsPage(notification.ID.ToString()));

The result is always the same. For some reason, the navigation history page is always popped off the navigation stack before the notification details page is pushed on. Is what I'm trying to do even possible, or should I get rid of shell, and do it all with the old navigation?

Upvotes: 1

Views: 3130

Answers (2)

Cherry Bu - MSFT
Cherry Bu - MSFT

Reputation: 10346

The problem is, when I click a notification on the notification history page, the navigation animation shows navigation back to the home page before navigating to the detail page. When I navigate back from the detail page, rather than going back to the list of notifications, it goes directly to the home page.

Flyout items in AppShell.xaml:

<FlyoutItem FlyoutDisplayOptions="AsMultipleItems">
    <ShellContent
        Title="About"
        ContentTemplate="{DataTemplate local:AboutPage}"
        Icon="tab_about.png"
        Route="AboutPage" />
    <ShellContent
        Title="Browse"
        ContentTemplate="{DataTemplate local:ItemsPage}"
        Icon="tab_feed.png" />
    <ShellContent
        Title="Main"
        ContentTemplate="{DataTemplate local:MainPage}"
        Icon="tab_feed.png" />
</FlyoutItem>

Routing.RegisterRoute method in In the Shell subclass constructor:

public AppShell()
{
    InitializeComponent();
    
    Routing.RegisterRoute("NotificationHistory", typeof(NotificationHistory));
    Routing.RegisterRoute("NotificationDetails", typeof(NotificationDetails));
}

Navigating to NotificationHistory page from MainPage.

private async void btn1_Clicked(object sender, EventArgs e)
{
    await Shell.Current.GoToAsync("NotificationHistory");
}

Navigating to NotificationDetailspage from NotificationHistory.

async void OnItemSelected(Item item)
{
    if (item == null)
        return;

    // This will push the ItemDetailPage onto the navigation stack
    await Shell.Current.GoToAsync($"{nameof(ItemDetailPage)}?{nameof(ItemDetailViewModel.ItemId)}={item.Id}");
}

NotificationHistory page:

<StackLayout>
    <CollectionView
        x:Name="ItemsListView"
        ItemsSource="{Binding Items}"
        SelectionMode="None">
        <CollectionView.ItemTemplate>
            <DataTemplate>
                <StackLayout Padding="10" x:DataType="model:Item">
                    <Label
                        FontSize="16"
                        LineBreakMode="NoWrap"
                        Text="{Binding Text}" />
                    <Label
                        FontSize="13"
                        LineBreakMode="NoWrap"
                        Text="{Binding Description}" />
                    <StackLayout.GestureRecognizers>
                        <TapGestureRecognizer
                            Command="{Binding Source={RelativeSource AncestorType={x:Type local:HistoryViewModel}}, Path=ItemTapped}"
                            CommandParameter="{Binding .}"
                            NumberOfTapsRequired="1" />
                    </StackLayout.GestureRecognizers>
                </StackLayout>
            </DataTemplate>
        </CollectionView.ItemTemplate>
    </CollectionView>
</StackLayout>
public partial class NotificationHistory : ContentPage
{
    HistoryViewModel _viewModel;

    public NotificationHistory()
    {
        InitializeComponent();
        BindingContext = _viewModel = new HistoryViewModel();
    }
}
public class HistoryViewModel : BaseViewModel
{  
    public   ObservableCollection<Item> Items { get; set; }    
    public Command<Item> ItemTapped { get; }
    public HistoryViewModel()
    {
        Title = "history";
        Items = new ObservableCollection<Item>()
        {
            new Item { Id ="1" , Text = "First item", Description="This is an item description." },
            new Item { Id = "2", Text = "Second item", Description="This is an item description." },
            new Item { Id = "3", Text = "Third item", Description="This is an item description." },
            new Item { Id = "4", Text = "Fourth item", Description="This is an item description." },
            new Item { Id = "5", Text = "Fifth item", Description="This is an item description." },
            new Item { Id = "6", Text = "Sixth item", Description="This is an item description." }
        };

        ItemTapped = new Command<Item>(OnItemSelected);
    }
    async void OnItemSelected(Item item)
    {
        if (item == null)
            return;

        // This will push the ItemDetailPage onto the navigation stack
        await Shell.Current.GoToAsync($"{nameof(NotificationDetails)}?{nameof(DetailViewModel.ItemId)}={item.Id}");
    }
}

NotificationDetails page:

<StackLayout Padding="15" Spacing="20">
    <Label FontSize="Medium" Text="Text:" />
    <Label FontSize="Small" Text="{Binding Text}" />
    <Label FontSize="Medium" Text="Description:" />
    <Label FontSize="Small" Text="{Binding Description}" />
</StackLayout>
public partial class NotificationDetails : ContentPage
{
    public NotificationDetails()
    {
        InitializeComponent();
        this.BindingContext =new DetailViewModel();
    }
}
[QueryProperty(nameof(ItemId), nameof(ItemId))]
public class DetailViewModel : BaseViewModel
{
    private string itemId;
    private string text;
    private string description;
    public string Id { get; set; }

    public string Text
    {
        get => text;
        set => SetProperty(ref text, value);
    }

    public string Description
    {
        get => description;
        set => SetProperty(ref description, value);
    }

    public string ItemId
    {
        get
        {
            return itemId;
        }
        set
        {
            itemId = value;
            LoadItemId(value);
        }
    }

    public async void LoadItemId(string itemId)
    {
        HistoryViewModel items = new HistoryViewModel();
        try
        {
            var item = items.Items.Where(x => x.Id == itemId).FirstOrDefault();
            if(item!=null)
            {
                Id = item.Id;
                Text = item.Text;
                Description = item.Description;
            }
          
        }
        catch (Exception)
        {
            Debug.WriteLine("Failed to Load Item");
        }
    }
}

The BaseViewModel is class that implementing INotifyPropertyChanged

public class BaseViewModel : INotifyPropertyChanged
{
   
    string title = string.Empty;
    public string Title
    {
        get { return title; }
        set { SetProperty(ref title, value); }
    }

    protected bool SetProperty<T>(ref T backingStore, T value,
        [CallerMemberName] string propertyName = "",
        Action onChanged = null)
    {
        if (EqualityComparer<T>.Default.Equals(backingStore, value))
            return false;

        backingStore = value;
        onChanged?.Invoke();
        OnPropertyChanged(propertyName);
        return true;
    }

    #region INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        var changed = PropertyChanged;
        if (changed == null)
            return;

        changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
    #endregion
}

Upvotes: 2

MSibalik
MSibalik

Reputation: 65

Unfortunately, the issue was caused by some code I have in OnNavigating() in AppShell.xaml.cs to pop everything of the navigation stack when certain tabs are pressed. I had not realized I needed to keep that code from running when args.Source == ShellNavigationSource.Push. I should also mention, for my code fix to work, I had to update to Xamarin.Forms 5.0 from 4.8.

Upvotes: 0

Related Questions