Dynamic Tabs in Maui Shell - shell

I'm trying to create an App where Tabs can be dynamically added and removed during runtime. I have a Tabbar in my AppShell.xaml, as seen in the following code sample:
<Shell
x:Class="foo.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:foo"
xmlns:views="clr-namespace:foo.Views"
Shell.FlyoutBehavior="Disabled"
Shell.BackgroundColor="{DynamicResource ShellBackGroundColor}">
<TabBar x:Name="MainTabBar">
<ShellContent Title="Home"
Icon="alert_icon_2_neg.png"
ContentTemplate="{DataTemplate views:StartPage}">
</ShellContent>
</TabBar>
</Shell>
The code I use to update the Tabs is the following:
public void UpdateTabs(ObservableCollection<UserAction> list)
{
MainTabBar.Items.Clear();
foreach (UserAction action in list)
{
MainTabBar.Items.Add(new ShellContent()
{
Title = action.Title,
Icon = action.ImageSource,
ContentTemplate = new DataTemplate(() => action.ActionPage),
Route = action.Title
});
}
}
After this method is executed the MainTabBar.Items list has the correct shellcontents in it, however the Tabs are not displayed in the UI. How could I achieve this?
Also, I am aware of the similarities between this and a question asked by User Bagroy (How dynamically add tabs in code behind to Maui Shell TabBar?) but the solutions given in response to his question do not work for me.

Based on the code, seems that using MainTabBar.Items.Clear(); then Tabbar not display.
So the workaround here is to add this code after clear the Items,
MainTabBar.Items.Clear();
//add the following two lines
MainTabBar = new TabBar();
this.Items.Add(MainTabBar);
Hope it works for you.

Related

Navigate between tabs using the back button

I am trying to create my first Xamarin mobile application, I am a bit confused when it comes to navigating back to the previous screen/tab using the back button (android). I have created a test project and I am using Xamarin form shell to define the hierarchy.
The navigation type I am using is Tab. In my test application I got 2 tabs
This is my current method which I am using to navigate back to the first tab from the second tab if the user presses on the back button.
I override OnBackButtonPressed method and execute a command that executes GoTo method.
public partial class ItemsPage : ContentPage
{
ItemsViewModel _viewModel;
public ICommand goBack;
public ItemsPage()
{
InitializeComponent();
BindingContext = _viewModel = new ItemsViewModel();
goBack = new Command(async (x) => await Shell.Current.GoToAsync("////AboutPage"));
}
protected override void OnAppearing()
{
base.OnAppearing();
_viewModel.OnAppearing();
}
protected override bool OnBackButtonPressed()
{
base.OnBackButtonPressed();
goBack.Execute(null);
return true;
}
}
This works, but the problem is, I have hardcoded the page I wanna go back to. In the future, I will be adding 2 more tabs, so in that case say for example if the user is on the 4th tab and the previous tab was Tab 3, how can I retrieve that information so that I can successfully navigate the user back to that previous tab or page?
I tried to use the navigationstack in the Shell but couldn't figure out how to use it with tabs.
Thank you for your time.
Cheers
Edit
-Code in the AppShell which defines the hierarchy
<?xml version="1.0" encoding="UTF-8"?>
<Shell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:TestApp03.Views"
Title="TestApp03"
x:Class="TestApp03.AppShell">
<TabBar>
<ShellContent Title="About" Icon="icon_about.png" Route="AboutPage" ContentTemplate="{DataTemplate local:AboutPage}" />
<ShellContent Title="Browse" Icon="icon_feed.png" ContentTemplate="{DataTemplate local:ItemsPage}" />
</TabBar>
</Shell>
You can use await Shell.Current.GoToAsync(".."); to achieve forward jump. For more usage, please refer to this link: https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/shell/tabs

Xamarin Forms Shell - Navigation to old navigation stack rather than flyout page

I’m having trouble with my Xamarin Forms Shell app. I'm not sure if this is a bug or expected behaviour, can someone point me in the right direction.
I have an app with 2 pages in the Visual Shell Hierarchy:
Search & History
<FlyoutItem Title="Search" Icon="search.png">
<Tab Title="Search">
<ShellContent Route="searchpage">
<views:SearchPage />
</ShellContent>
</Tab>
</FlyoutItem>
<FlyoutItem Title="History" Icon="history.png">
<Tab Title="History">
<ShellContent>
<views:HistoryPage />
</ShellContent>
</Tab>
</FlyoutItem>
And several pages (let’s call them PageA, PageB and PageC) registered like so:
Routing.RegisterRoute("PageA", typeof(PageA));
Routing.RegisterRoute("PageB", typeof(PageB));
Routing.RegisterRoute("PageC", typeof(PageC)); (Oops, I should probably use nameof here)
Anyway, I start on the Search page and navigate to PageA like so:
Shell.Current.GoToAsync("PageA");
Because PageA is not in the visual hierarchy, this gives me a navigation stack like so:
"//searchpage/PageA"
Using the same relative navigation approach, I navigate to PageB then PageC, so my navigation stack is like so:
"//searchpage/PageA/PageB/PageC"
From PageC, I use the flyout menu to navigate to History, the history page opens fine.
Now on the history page, I use the flyout menu again and click the Search tab
But I'm not taken to the search page as expected, I'm taken back to PageC (taken back to my previous navigation stack).
From this page (PageC) if I use the flyout menu again and click the Search tab, it navigates correctly to the Search page.
How should this work and how can I stop it navigating to PageC when I select the Search tab from the flyout?
Thanks
(ps - I'm using Xamarin Forms 4.7 at present)
For those still looking for answers, an answer by Damir's Corner
await Shell.Current.GoToAsync($"///{tag}", false); //Checks the Navigation Stack Downward
The code above checks the current Navigation Stack, and searches for the Route Tag Downward (or inside the Navigation Stack) and then replaces the entire Stack with the Found Route.
Hope this helps for those looking for answers.
When jumping between different tabs (and by doing so different navigationstacks), Williams solution (Damir's Corner) worked for me.
My scenario:
Tab1 -> ListPage -> DetailsPage
Tab2 -> CreateStep1Page -> CreateStep2Page
After saving the entry in CreateSpecificPage, I navigate two times:
await Shell.Current.GoToAsync($"///{nameof(CreateStep1Page)}", false);
await Shell.Current.GoToAsync($"///{nameof(ListPage)}/{nameof(DetailsPage)}", true);
I will be raising this as a feature request when I get 5 minutes, but I have found a solution to this issue.
I made the last page (PageC) a Root page and removed it from my Routes Script. I did this by adding the following to the AppShell.Xaml like so:
<ShellItem Route="PageC">
<ShellContent ContentTemplate="{DataTemplate views:PageC}" />
</ShellItem>
and removing this code:
Routing.RegisterRoute("PageC", typeof(PageC));
Now when navigating to PageC, I am no longer pushing onto the stack with a relative navigation, I am now navigating to a root page like so:
await Shell.Current.GoToAsync("//PageC");
Before we navigate to PageC, we need to clear the current navigation stack. I tried PopToRootAsync, but this showed the SearchPage before navigating to PageC. I found the following code works:
// FYI - Navigation Stack: //SearchPage/PageA/PageB
var pageB = Shell.Current.Navigation.NavigationStack[2];
var pageA = Shell.Current.Navigation.NavigationStack[1];
Shell.Current.Navigation.RemovePage(pageB);
Shell.Current.Navigation.RemovePage(pageA);
// Now navigate to our route page, PageC.
await Shell.Current.GoToAsync("//PageC");
I will be using a slightly more elegant way of getting the pages rather than hard coding the index, but thought this was the easiest way to demonstrate what was happening.
The next time I navigate to the SearchPage now, I get the SearchPage
I think this feature is intentional. One thing to understand is that as mentioned in the docs, every Tab contains separate navigation stack, Shell.Current.Navigation points to the currently active/visible Tab navigation stack. So what you can do is actually quite simple.
You just override Shell.OnNavigating event, and from remove every page except root one from the current navigation stack only if you are navigating between different route trees.
Like, from //route1/page1 to //route2/page2.
You can adapt following code per your use case.
public partial class AppShell : Xamarin.Forms.Shell
{
public AppShell()
{
InitializeComponent();
Setup.RegisterRoutes(this);
}
protected override void OnNavigating(ShellNavigatingEventArgs args)
{
base.OnNavigating(args);
if (args.Current != null && args.Target != null && args.Current.Location.OriginalString.StartsWith("//") && args.Target.Location.OriginalString.StartsWith("//")) {
var currentRoot = args.Current.Location.OriginalString.TrimStart('/').Split('/').First();
var targetRoot = args.Target.Location.OriginalString.TrimStart('/').Split('/').First();
// we are navigating between tabs
if (!string.Equals(currentRoot, targetRoot, StringComparison.OrdinalIgnoreCase) && Navigation.NavigationStack.Count > 1) {
for (var i = Navigation.NavigationStack.Count - 1; i > 0; i--) {
Navigation.RemovePage(Navigation.NavigationStack[i]);
}
}
}
}
}

Hamburger menu AutomationId for UI Testing

I'm trying to run a UI Test on a page (About.xaml).
Below are the steps to get to the page when the application loads.
Launch the Login Screen
User enters username
User enters password
User clicks on the Login Button
User lands on the Home Page in an AppShell.
User clicks on the Hamburger menu
User clicks on the About Menu in the Flyout Menu Items.
My question is, how do I set the Automation Id for the Hamburger Menu (Flyout Menu) of the AppShell?
Here's the UI Test Case.
[Test]
public async Task AboutPage_UITest()
{
//Arange
app.EnterText("UsernameEntryId", "user1");
app.EnterText("PasswordEntryId", "Abc#123");
//Act
app.DismissKeyboard();
app.Tap(x => x.Marked("LoginButtonId"));
app.Tap(x => x.Marked("AppShellId"));
//app.Tap(c => c.Class("OverflowMenuButton")); I tried this as well but no luck.
await Task.Delay(30000);
app.Tap(x => x.Marked("AboutId"));
//Assert
var appResult = app.Query("EmailId").First(result => result.Text == "abc#example.com");
Assert.IsTrue(appResult != null, "Label is not displaying the right result!");
app.Screenshot("About Page");
}
In the AppShell.xaml, here's the top section.
<Shell xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
FlyoutHeaderBehavior="CollapseOnScroll"
Shell.ItemTemplate="{StaticResource FlyoutTemplate}"
Shell.MenuItemTemplate="{StaticResource FlyoutTemplate}"
FlyoutBackgroundColor="WhiteSmoke"
Navigating="OnNavigating"
Navigated="OnNavigated"
AutomationId="AppShellId"
x:Class="DemoApp.AppShell">
Welcome to SO!
After researching that , you can use follow ways to set the AutomationId for the Hamburger Menu.
In AppShell.xaml.cs:
public AppShell()
{
InitializeComponent();
FlyoutIcon.AutomationId = "FlyoutIcon";
//or
FlyoutIcon.SetValue(AutomationProperties.NameProperty, "FlyoutIcon");
}
Note: Also need to set a value for FlyoutIcon(Such as: FlyoutIcon="xxx.png") make it work, otherwise above code will occur error System.NullReferenceException: 'Object reference not set to an instance of an object.'.
In addition, here it the way to go to About Page without tapping the Hamburger menu.
You can add TabBar to AppShell.xaml, and defining the About page and other page inside it.
Such as follow:
<TabBar>
<Tab Title="About"
AutomationId="About"
Icon="tab_about.png">
<ShellContent>
<local:AboutPage />
</ShellContent>
</Tab>
<Tab Title="Browse"
AutomationId="Browse"
Icon="tab_feed.png">
<ShellContent>
<local:ItemsPage />
</ShellContent>
</Tab>
</TabBar>
Then in Test method can navigate to your wanted page as follow:
app.Tap(x => x.Marked("About"));
Or, directly in Xaml, like this:
<Shell.FlyoutIcon>
<FontImageSource AutomationId="THE_ID_YOU_WANT" ... other props />
</Shell.FlyoutIcon>
You can also use FileImageSource or UrlImageSource or whatever kind you need. The important thing is that the AutomationId goes on the ImageSource tag itself.
Annoyingly, you don't seem to be able to set the AutomationId without manually defining an icon. And the Id it gives you is not consistent every run (Sometimes it is "OK" and sometimes "Open navigation drawer" - so this is the best way to get a consistent Id.

MasterDetailPage - can't find any way to target 'detail' page window

I have created a Master Detail Page
I am loading a list of items into the 'detail' frame / window
I want to replace the contents of that page with a template / view which never has any reason to exist as an item in the Menu Items
I have tried replacing MainPage and Navigation which load the page but you lose the Master Detail context - the menu
Please can someone tell me what I call in order to replace the current page with one of my choice while staying within the context of Master Detail?
This does not work, for example - it removes the MasterDetail menu
Navigation.PushAsync(new Arcade.Index());
I have created the MasterDetailPage by pretty much letting Visual Studio generate it. I set it after a successful login, like so:
var welcome = new Pages.Welcome();
Application.Current.MainPage = welcome;
This is an excerpt of the XAML for Welcome
<MasterDetailPage.Master>
<pages:WelcomeMaster x:Name="MasterPage" />
</MasterDetailPage.Master>
<MasterDetailPage.Detail>
<NavigationPage>
<x:Arguments>
<pages:Index />
</x:Arguments>
</NavigationPage>
I've added this to the code behind for Welcome
InitializeComponent();
MasterPage.ListView.ItemSelected += ListView_ItemSelected;
this.Detail = new NavigationPage(new Arcade.Index());
In spite of all that, when I call this later, the MasterDetail menu disappears
((MasterDetailPage)Application.Current.MainPage).Detail = new Arcade.Index();
In you App.xaml.cs add static method then you can use it to navigate in you code
public static void SetDatailPage(Page page)
{
if (App.Current.MainPage is MasterDetailPage)
{
var masterPage = (MasterDetailPage)App.Current.MainPage;
masterPage.Detail = new NavigationPage(page);
}
}
And use it like this
App.SetDatailPage(new YourPageYouWannaNavigateTo());
1 - use App.Current.Mainpage
var md = (MasterDetailPage)App.Current.MainPage;
md.Detail = new MyPage();
2 - use navigation
when you initially create your detail, wrap it in a NavigationPage
this.Detail = new NavigaitionPage(new Page1());
then later, Page1 can navigate to Page2 within the Detail pane
Navigation.PushAsync(new Page2());
3 - explicitly pass a reference to the MasterDetailPage
// when you create Page1, pass a reference to the MasterDetailPage
// you will also need to modify Page1's constructor
this.Detail = new Page1(this);

Using a Command Bar inside of the NavView Navigation Pane UWP

I'm struggling to create the UI I have in my head and to date have been fairly unsuccessful.
I'm trying to create a Main Page which hosts my NavView and inside of by NavView I wish to have a command bar which will control which NavViewItems are visible. I have created a quick image of what I'm trying to achieve.
In my example I have the home button in the command bar activated which displays
Nav Item Header
Navigation Item 1
etc...
I want to be able to click documents and have the indicator switch to documents and hide the navigation items corresponding to Home and show the navigation items corresponding to Documents.
Finally, I want the command bar to collapse when the NavView pane is compact but the user should be able to click the Command bar button and expand the command bar to change between Home, Documents etc.
Really looking for any help/advice for the best places to start.
I'm still learning the UWP controls and Xaml.
I think you should use a SplitView instead of NavigationView outside, and then,
inside the Pane of the Splitview, use a NavigationView with some trick to achieve what you desired.
Key points are:
Keep the NavigationView's PaneDisplayMode LeftComact
don't use the PaneToggleButton in the NavigationView to prevent
user from changing PaneDisplayMode by clicking it, use a custom one instead to open and close pane.
Change the PaneDisplayMode of the NavigationView to Top when
pane opens, and backt to LeftComact again when pane closes.
Here is what I have achieved with NavigationView inside Splitview.Pane:
You can decorate it and make it more visually satisfying, like adding an AutoSuggestBox or Setting button, but that's the basic. Btw, don't use the NavigationView's Setting button, as I have seen it behaving strangely here.
XAML:
<SplitView
x:Name="Split"
DisplayMode="CompactInline"
CompactPaneLength="40">
<SplitView.Pane>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition />
</Grid.RowDefinitions>
<Button
Click="Button_Click">
<SymbolIcon Symbol="List"/>
</Button>
<NavigationView
Grid.Row="1"
x:Name="NavView"
PaneDisplayMode="LeftCompact"
CompactPaneLength="{x:Bind Split.CompactPaneLength}"
IsBackButtonVisible="Collapsed"
IsPaneToggleButtonVisible="False"
IsSettingsVisible="False"
SelectionChanged="NavView_SelectionChanged">
<NavigationView.MenuItems>
<NavigationViewItem x:Name="HomeItem" Icon="Home" VerticalAlignment="Stretch"/>
<NavigationViewItem x:Name="DocumentItem" Icon="Document" />
<NavigationViewItem x:Name="PeopleItem" Icon="People" />
</NavigationView.MenuItems>
<ContentControl>
<ListView
x:Name="ItemList"/>
</ContentControl>
</NavigationView>
</Grid>
</SplitView.Pane>
</SplitView>
Code behind:
public sealed partial class MainPage : Page
{
public List<string> HomeItemList;
public List<string> DocumentItemList;
public List<string> PeopleItemList;
public MainPage()
{
InitializeComponent();
HomeItemList = new List<string> { "HomeItem1", "HomeItem2", "HomeItem3" };
DocumentItemList = new List<string> { "DocumentItem1", "DocumentItem2", "DocumentItem3" };
PeopleItemList = new List<string> { "PeopleItem1", "PeopleItem2", "PeopleItem3" };
}
private void Button_Click(object sender, RoutedEventArgs e)
{
Split.IsPaneOpen = !Split.IsPaneOpen;
if (Split.IsPaneOpen)
{
NavView.PaneDisplayMode = NavigationViewPaneDisplayMode.Top;
}
else NavView.PaneDisplayMode = NavigationViewPaneDisplayMode.LeftCompact;
}
private void NavView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
{
if (args.SelectedItem != null)
{
Split.IsPaneOpen = true;
NavView.PaneDisplayMode = NavigationViewPaneDisplayMode.Top;
if (sender.SelectedItem == HomeItem)
ItemList.ItemsSource = HomeItemList;
else if(sender.SelectedItem == DocumentItem)
ItemList.ItemsSource = DocumentItemList;
else if(sender.SelectedItem == PeopleItem)
ItemList.ItemsSource = PeopleItemList;
}
}
}
Hope that helps.
First thing is to decide if you want to use NavigationView. In XAML controls are defined by their behavior (properties and methods that they implement), while the visual appearance is irrelevant and can be altered completely. If NavigationView is right for your task then you can alter its style partially or completely - in XAML editor right click on it, then click Edit Template > Edit a Copy. Now you'll get the XAML style definition that defines appearance of NavigationView, that's the place to start.
But it might be very well that you can't use NavigationView and that starting with SplitView might be a better idea as #Muzib said.
Not sure if this is a good idea for learning XAML, but you'll learn one thing - XAML can be customized to the great extent, but doing it may also be a very complex task.
I think there are a few problems from the UX perspective.
Not all the navigation items are shown at once and a used must expand the menu to change between the sets of items.
The positions of the navigation items changes when the navigation pane is expanded. Currently the way the control works it is though the pane is expanding to show the text of the button. With your suggested approach it would like the items jump down on open.
I wonder if would be easier to have a fixed side pane with the controls laid out like you want and no hamburger button etc. This is not so unusual, the Settings app does it.
If you do go with a fixed width pane, I recommend looking at the XAML that defines the NavigationView control, which can be found inside C:\Program Files (x86)\Windows Kits\10\DesignTime\CommonConfiguration\Neutral\UAP\10.0.17763.0\Generic\generic.xaml
(according to the version of your SDK). Then you can make sure to use the theme resources used by the Windows so that your custom control has a similar look and feel.

Resources