Global routes currently cannot be the only page on the stack - xaml

Getting the flyout menu to work in Xamarin Forms the way I want it to has been a major pain. I'm trying to get a certain menu item (My Stats) to go to a certain page if the user is logged in, or to a different page if the user is not (The login page). I have attempted two different approaches with no success.
Approach 1
First I tried setting the flyout Item in XAML with specified route in Shell Content like so:
<FlyoutItem Title="My Stats" Icon="icon_feed.png">
<ShellContent x:Name="shellContent_myStats" Route="MyStatsPage" ContentTemplate="{DataTemplate local:MyStatsPage}" />
</FlyoutItem>
Then I would modify the route programmtically when the user logs in/out:
//Logging Out
shellContent_myStats.ContentTemplate = new DataTemplate(typeof(LoginPage));
Routing.SetRoute(shellContent_myStats, "LoginPage");
...
//Logging In
shellContent_myStats.ContentTemplate = new DataTemplate(typeof(MyStatsPage));
Routing.SetRoute(shellContent_myStats, "MyStatsPage");
I could not get this to work. For whatever reason, only the first piece to get executed would take effect. For example, if I started the app while logged out, it would direct to the login page as expected but would not switch over once I log out. The reverse is true as well if I start the app while logged in, it navigates to the "My Stats" page as expected, but will continue to do so even after I log out.
I have tried a few different variations of the above code to no avail.
Approach 2
The second approach I tried, was instead specifying a menu item in XAML with an OnClick event handler like so:
<MenuItem Text="My Journey" StyleClass="MenuItemLayoutStyle" Clicked="OnMyStatsClicked"/>
Code behind:
public AppShell()
{
//Route now needs to be registered in page constructor
Routing.RegisterRoute(nameof(MyStatsPage), typeof(MyStatsPage));
...
public async void OnMyStatsClicked(object sender, EventArgs e)
{
Shell.Current.FlyoutIsPresented = false;
if (LoggedIn)
{
// Prefixing with `//` switches to a different navigation stack instead of pushing to the active one
await Shell.Current.GoToAsync($"//{nameof(MyStatsPage)}");
}
else
{
await Shell.Current.GoToAsync($"//{nameof(LoginPage)}");
}
}
...
Now when I select the menu item with this setup (while logged in), it throws the following error:
System.Exception: 'Global routes currently cannot be the only page on the stack, so absolute routing to global routes is not supported. For now, just navigate to: MyStatsPage'
If I follow the suggestion given by removing the two slashes in the route ("//"), the navigation works, but then the flyout menu is not available in the top left and is replaced with a back button. This is not desired, I want the menu to still be available. Why can't I use absolute routing on a route defined in the code behind..?
I appreciate any suggestions on either of the two methods mentioned to get this to work. Been at it for days trying to do something seemingly so simple.. Thanks

You can set up a trick using the property FlyoutItemIsVisible, the ShellContent (thus the related page) will be registered in the Shell hierarchy but won't be visible in the flyout, also it will be loaded only when it is needed since we are using ContentTemplate.
Assuming you are only using the flyout without the bottom tabs:
AppShell.xaml
<Shell Shell.TabBarIsVisible="False"
...
<FlyoutItem>
<ShellContent ContentTemplate="{DataTemplate local:MainPage}"/>
<ShellContent Route="AnotherPage"
FlyoutItemIsVisible="False"
ContentTemplate="{DataTemplate local:AnotherPage}"/>
<ShellContent Route="LoginPage"
FlyoutItemIsVisible="False"
ContentTemplate="{DataTemplate local:LoginPage}"/>
</FlyoutItem>
<MenuItem Text="My Stats Page"
Clicked="MenuItem_Clicked"/>
AppShell.xaml.cs
async void MenuItem_Clicked(object sender, System.EventArgs e)
{
if (LoggedIn)
await Shell.Current.GoToAsync($"//{nameof(AnotherPage)}");
else
await Shell.Current.GoToAsync($"//{nameof(LoginPage)}");
}
Note
In the case you are using bottom tabs at the same time, or as another approach I suggest to hide unrelated pages to the current log status (example: hide LoginPage if the user is logged in).
How can you restrict/control the navigation routes the user can visit based on login status/role?

Related

How to show the grid on full page in UWP?

I am using syncfusion controls for UWP. One of he feature I have seen in their sample application is showing the data grid on full page.
Something like this
After clicking on this the page looks like this
I am pretty sure that this is not full mode view of the app. After clicking on the button it hides the navigation drawer and the left panel as well.
Can someone please tell how it could be done?
Thanks in advance.
I don't know much about Syncfusion specifically, but I can describe how to implement it in general! This appears to be either a Navigation View or a Master/Details navigation design pattern (I linked both to thier respective documentation pages). Either way, the design pattern typically has the following structure:
Top Frame
Top Page
SplitView (or NavigationView)
Frame
Content Page
Content
Basically, the data grid normally sits on a page. That page is displayed in a frame that is in the content area of a SplitView or NavigationView, which is on the top-level page, which is displayed directly in the top-level frame in the app's window.
The "popping out" effect, that has the data grid display over the navigation drawer is likely some neat trickery going on with manipulating sub-pages, and the frames they are displayed in. So, lets take a look at how this effect can be implemented, given the above info about the structure of the navigation design pattern.
The data grid itself should be placed on its own page, which we will assume is called "DataGridPage" for the following code examples (note that the attributes for the page have been omitted for clarity):
<Page>
<!-- The data grid, or whatever content here -->
</Page>
We are actually adding two layers to the list presented above; the content page has a frame on it, and in that frame is a page with the data grid on it. So, your "content page" in the above list looks like this:
<Page>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition >
<RowDefinition Height="*"></RowDefinition >
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<!-- The bread-crumb, search box, and pop-out button are all in here -->
</Grid>
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<!-- The "left panel" here -->
</Grid>
<Frame Name="MyFrame"
Grid.Column="1"></Frame>
</Grid>
</Page>
And then in the constructor for your "content page" (I am assuming C#, but the logic is the same for VB):
public ContentPage()
{
this.InitializeComponent();
// You are telling the frame to display your page with the data grid on it.
// If you don't ever issue a different "Frame.Navigate" statement on this page,
// then it will statically display your data grid as if it was on the same
// page.
this.MyFrame.Navigate(typeof(DataGridPage));
}
Now, I don't know how the hamburger menu is implemented (the "navigation drawer"), and there are several ways to implement one, and I am guessing that it is a control that Syncfucsion provides for you... so I am going to skip trying to write out what the "Top Page" in the above list would look like (although I can detail one solution for implementing that without Syncfusion in a follow-up post if you would like). All that matters here, is that it has, somewhere on it, a frame that displays the "ContentPage" (which as detailed above contains the left panel and a display of your data grid page).
At the top level of the UWP app, the window your app is running in contains a single frame, which is itself displaying the "Top Page". This is important to note, as it is critical to how this "popping out" effect likely works.
On your "Content Page", handle the "Click" event of the "pop-out" button in the upper right of the page. In that event handler, you are going to take your data grid page, and display it in the top frame (the one directly inside of the app window) rather than the "Top Page" (which it is currently displaying).
The mechanics behind this are a little tricky, since you are handling an event on the "Content Page" (the click of the "pop-out" button), but you need to get a reference to the frame the parent page is displayed in (rather than the frame the content page is displayed in).
The solution is to write a custom event. The idea is that you fire this custom event whenever the "pop-out" button is clicked, and then the "top page" responds to the event, asking it's parent frame (the "top frame") to display the data grid page.
Here is a lengthy (and possibly overwhelming) overview of events. But the code you need looks something like the following. On your "Content page":
<!-- Other attributes omitted -->
<Button Name="PopOutButton"
Click="PopOutButton_Click">
And then in the code of the "Content page":
public sealed partial class ContentPage : Page
{
// This is the declaration for your custom event.
public delegate void PopOutRequestedHandler(object sender, PopOutRequestedEventArgs e);
public event PopOutRequestedHandler PopOutRequested;
private void RequestPopOut()
{
// Ensure that something is listening to the event.
if (this.PopOutRequested!= null)
{
// Create the args, and call the listening event handlers.
PopOutRequestedEventArgs args = new PopOutRequestedEventArgs();
this.PopOutRequested(this, args);
}
}
public void PopOutButton_Click(object sender, RoutedEventArgs e)
{
// Any animation logic you want goes here.
this.RequestPopOut();
}
}
You can define a class PopOutRequestedEventArgs to include whatever properties you want to pass in your event. None are strictly necessary for the basic implementation.
Now, on your "Top Page", you need to subscribe to the event. Do so in the "Navigated" event handler for the frame on the top page. So you have this somewhere on your top page:
<Frame Name="NavFrame"
Navigated="NavFrame_Navigated"></Frame>
Then in your "Top page" code, you have:
private void NavFrame_Navigated(object sender, NavigationEventArgs e)
{
if (e.Content is ContentPage page)
{
page.PopOutRequested += this.ContentPage_RequestedPopOut;
}
}
private void ContentPage_RequestedPopOut(object sender, PopOutRequestedEventArgs e)
{
// This tells the frame this page (the "Top Page") is in, to
// instead display the data grid page.
this.Frame.Navigate(typeof(DataGridPage));
}
That is the effect, in a nutshell. To get the page to "close" again, simply put a button on your datagrid page (which you can hide when the page is displayed in the frame on the "Content page"), and handle its Click event. In that event, request the frame navigate back to the "Content page". So, on your data grid page somewhere, you have the same button as detailed above on the "Content page", and in its event handler you put:
this.Frame.Navigate(typeof(ContentPage));
Now, it is important that you ONLY allow the button to be clicked when the "data grid page" is displayed in the "top frame", as that code references the frame the page is displayed in when it is run... it is not an absolute reference. You will get some unexpected results if you call that code while the "data grid page" is displayed in the frame on the "Content page".
As for restoring the state of the "Top page" when you navigate back to it when closing out of the "popped out" view of the data grid, you can accomplish that via the "NavigationEventArgs". Store the state of the "Top page" (such as the fact that the frame on it is displaying the "Content page") in your own class (we will call it "NavArgs" here), and then pass that as a parameter in the Navigate statements:
// Create an instance of the args class, and store your state info.
NavArgs navArgs = new NavArgs();
this.Frame.Navigate(typeof(DataGridPage), navArgs);
You can store the "navArgs" on your data grid page, and then when exiting out of the "popped out" view, and navigating back to the "Top page" in the "top frame", similarly pass them through the Frame.Navigate() call.
It's a little thorough (although still glosses over a lot of concepts), and it is only one possible way to implement that functionality, but hopefully it helps!
You can show the grid in full page by using the property GridColumnSizer.Star .
In GridColumnSizer.Star type, the available grid width is divided into equal sized regions and the divided width is set for each column. When you have three columns in the grid, the total space is divided by three and that value is allocated to the width of the column.
You can get more details about ColumnSizing from the below links.
https://help.syncfusion.com/uwp/sfdatagrid/columns#column-sizing
Regards,
Sathiyathanam

How to show navigation page button on all views Xamarin Forms

I am working with Xamarin Forms and I am using MasterDetailPage and NavigationPage and everything is working. Now, I need to configure the page button (three bars on the left) to be visible on all views.
I have a MasterDetailPage with a menu and the user clicks a menu its navigate to other pages. First page (homepage) look like that:
<MasterDetailPage.Detail>
<NavigationPage>
<x:Arguments>
<home:HomePage />
</x:Arguments>
</NavigationPage>
</MasterDetailPage.Detail>
When the user clicks in menu inside masterdetailpage or other menus outside masterdetailpage it performs a navigation:
The three bars page button is visible only on the first view, when I navigate to others views, that button disapears.
But, I want something like that:
The page back button showns automatically when i navigate to other pages and it's fine.
How to let the three bars page button visible to all views and maintain the back page button?
I am using the following code to navigate between views:
await Navigation.PushAsync(MyNewPage());
Thanks!
Your master detail page has to handle the navigation to the detail pages for it to work correctly.
Connect the menu listview:
ListView.ItemSelected += OnItemSelected;
In the OnItemSelected event.
{MasterDetailPage}.Detail.Navigation.PushAsync(new MyNewPage());
Here is an example of master detail navigation:
async void OnItemSelected(object sender, SelectedItemChangedEventArgs e)
{
var item = e.SelectedItem as MasterPageItem;
if (item != null)
{
NavigationPage nextDetailPage = null;
await Task.Run(() =>
{
nextDetailPage = new NavigationPage((Page)Activator.CreateInstance(item.TargetType));
});
Detail = nextDetailPage;
masterPage.ListView.SelectedItem = null;
IsPresented = false;
}
}
This is not possible, as you can't have two buttons on the left side. And as many applications I have seen, no app supports this kind of UI. Unless you write custom navigation bar and put it on top of every page.
You can create a stack of detail pages in your main MasterDetailPage. So whenever you want to push a new page, instead you can just set page as Detail and put old Detail in your own stack.
For Android you can wrap back button event to pop page from stack, but on iOS there is no back button, so you will have to create some sort of back button in detail page's toolbar.
I think that you will have to write a Custom Renderer to achieve what you want.
But if you want only to keep the hamburger button, without the back button, you can navigate to your pages using this code:
Detail = new NavigationPage(new MyNewPage());

Unable to navigate backward using hardware key in a Universal App

I navigate forward using Frame.Navigate but when I press the hardware back key on my phone I end up on the start screen and not the page I just visited.
What might be wrong?
The reason behind your problem is, you are creating a Blank Page. If you're creating a blank page, you should define what the app has to do when the back button is fired.
Better, consider adding "Basic page". It will have backstack by nature. If you are navigating from the MainPage to the Basic Page and when you pressed back button at the Basic Page it will back to the MainPage.
I hope this could solve your problem!
If you want to use Blank Page in your application, you need to use like this on your page where you wanna override back button:
add this in your header:
using Windows.Phone.UI.Input;
and then in your constructor:
HardwareButtons.BackPressed += HardwareButtons_BackPressed;
add this anywhere in your code:
void HardwareButtons_BackPressed(object sender, BackPressedEventArgs e)
{
HardwareButtons.BackPressed -= HardwareButtons_BackPressed;
this.Frame.Navigate(typeof(MainPage));
e.Handled = true;
}
You've probably forgotten to use the NavigationHelper included in the template of the universal app.
You should use it like this on every page:
NavigationHelper _navigationHelper;
public LoginPage()
{
this.InitializeComponent();
_navigationHelper = new NavigationHelper(this);
}

Navigate to the last frame from SwapChainBackgroundPanel

I am writing a Windows Store app using C++/XAML with DirectX interop - SwapChainBackgroundPanel.
The application is based on the template "Split Page". From each list view item, a DirectX page may be launched using code below.
Window::Current->Content = ref new MyD3Components::DirectXPage();
Window::Current->Activate();
This is working fine and DirectX page opens up and plays very well.
What I would like to have a button in the app bar which helps user to go back and display the "Split Page" to allow selecting another DirectX page. This I have not been able to accomplish yet.
Among several things I have tried, below is the most logical one to my opinion. It gives a "Platform::DisconnectedException" when user wants to go back to the last page.
Windows::UI::Xaml::Controls::Frame^ rootFrame = SDL::App::GetRootFrame();
Window::Current->Content = rootFrame;
Window::Current->Activate();
Please look to see if you have a suggestion or better a solution.
Here the sample example for your question :
What i am creating : 2 pages...
You will have (go to page 2)link on page 1...If u click that,the second page should appear that says "Page 2" at the top. Notice that there is a back button to the left of the page title. Click the button to return to the first page...
1.) Find the TextBlock element named pageTitle and change the Text property to Page 1. The XAML should look like this:
<TextBlock x:Name="pageTitle" Grid.Column="1" Text="Page 1"
Style="{StaticResource PageHeaderTextStyle}"/>
2.)Add the following XAML as a second child element to the root Grid. The StackPanel element should be a sibling to the Grid that contains the back button and page title.
<StackPanel Grid.Row="1"
Margin="120,0,120,60">
<HyperlinkButton Content="Click to go to page 2" Click="HyperlinkButton_Click_1"/>
</StackPanel>
3.)Make the following changes to BasicPage2.xaml.
Find the TextBlock element named pageTitle and change the Text property to Page 2. The XAML should look like this:
<TextBlock x:Name="pageTitle" Grid.Column="1" Text="Page 2"
Style="{StaticResource PageHeaderTextStyle}"/>
4.)Add the following XAML as a second child element to the root Grid. The StackPanel element should be a sibling to the Grid that contains the back button and page title.
<StackPanel Grid.Row="1"
Margin="120,0,120,60">
<TextBlock HorizontalAlignment="Left" Name="tb1" Text="Hello World!"/>
</StackPanel>
5.)Add the following code to the BasicPage1 class in BasicPage1.Xaml.cs
private void HyperlinkButton_Click_1(object sender, RoutedEventArgs e)
{
this.Frame.Navigate(typeof(BasicPage2));
}
6.)Now that we've prepared the new pages, we need to make BasicPage1 the first thing that appears when the app starts. Open app.xaml.cs and change the OnLaunched method to call Frame.Navigate by using BasicPage1 instead of the BlankPage. The entire OnLaunched method should look like the following:
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
// Create a Frame to act navigation context and navigate to the first page
var rootFrame = new Frame();
rootFrame.Navigate(typeof(BasicPage1));
// Place the frame in the current window and ensure that it is active
Window.Current.Content = rootFrame;
Window.Current.Activate();
}
Now you are ready to test the app. Start the app, and click the link that says Click to go to page 2. The second page should appear that says "Page 2" at the top. Notice that there is a back button to the left of the page title. Click the button to return to the first page.
Thats it! hope it helps u.
After a bit of trial and error, I am in the position to answer my own question. It seems that all I needed to do was to remove my rendering callbackfrom the CompositionTarget.
It was added like below.
m_eventToken = CompositionTarget::Rendering::add(ref new Windows::Foundation::EventHandler<Object^>(this, &DirectXPage::OnRendering));
Before replacing the current window and activating it, I called below.
CompositionTarget::Rendering::remove(m_eventToken);
I guessed this helped DirectX not to output to rendering pipeline and complain (disconnectedexception) when the target is not there.

StackOverflowException in Silverlight MVVM after page navigation

I am attempting to use MVVM Light to create a simple page for adding/editing/deleting Contacts and for users to send faxes from the other parts of the software. The MainPage has a simple header with some corporate info and two links, one to the Contacts page and the other to the Fax History page; most of the MainPage page is a navigation frame that is the target of the links, where the pages are loaded. I am using VS 2010/.NET 4.0/SL 4.0.
The Contacts page has two datagrids, one for Search Contacts Results ('gridContacts') and the other for Selected Contacts (those contacts who will receive the fax--'gridSelected'). There's a Delete button for the user to delete the contact of the selected row in the Search grid. There's also a Send Fax button to send the fax to those contacts in the gridContacts grid.
Problem is, if I ever select a row in the Search grid, navigate to Fax History, then come back, I get a StackOverflowException before the navigation occurs. If I navigate to the other page first, then come back, and then select a row, I'll get the exception at that point. I found that the Send Fax button also has causes the same exception when I removed the Delete button. On both buttons, I am able to work around the problem just coding everything in code-behind; obviously this is not ideal, so I want to learn to get it to work with MVVM.
When I step through the code for the problem with the Delete button, I see that the exception occurs in the DelegateCommand that the Delete button is bound to. In the CanExecute method, the first time through, the object parameter is correctly a Contact (since the grid is bound to a List of Contacts). It fires CanExecuteChanged(this,new EventArgs()) as part of the code logic--but then comes BACK into the function, but with null as the parameter passed in. Here is where an infinite loop becomes evident: CanExecute fires in a loop and alternates between having a Contact object and null as the parameter passed in. The stack trace shows External Code between the calls to CanExecute, but that's the only method present. The infinite loop eventually causes the StackOverflowException.
Here's the code in question:
public bool CanExecute(object parameter) //parameter alternates between Contact object and null
{
bool temp = _funcCanExecute(parameter); //_funcCanExecute is set to CanDelete in the DelegateCommand constructor.
if (_bCanExecuteCache != temp)
{
_bCanExecuteCache = temp;
if (CanExecuteChanged != null)
{
CanExecuteChanged(this, new EventArgs()); //this line somehow leads to another call into here --> infinite loop
}
}
return _bCanExecuteCache;
}
The Search grid ItemSource is bound to a VM via a Locator like so:
<sdk:DataGrid AutoGenerateColumns="False" x:Name="gridContacts" IsTabStop="False" Grid.Row="3" ItemsSource="{Binding Path=SearchContactsViewModel.Contacts, Mode=OneWay, Source={StaticResource Locator}}" SelectionMode="Single">
Here are the Delete button Command and CommandParameter (the overall data context is set as DataContext="{Binding Source={StaticResource Locator}, Path=SearchContactsViewModel}"):
<Button x:Name="btnDelete" Style="{StaticResource ButtonStyle}" Command="{Binding Path=SearchContactsViewModel.DeleteContactCommand, Source={StaticResource Locator}}" Grid.Row="1" Grid.Column="1"
CommandParameter="{Binding Path=SelectedItem, ElementName=gridContacts}" HorizontalAlignment="Right">
Here's the simplified code in the SearchContactsViewModel:
public DelegateCommand DeleteContactCommand
{
get;
private set;
}
private bool CanDeleteContact(object param)
{
Contact c = param as Contact;
if (c == null)
return false;
return true;
}
private void DeleteContact(object param)
{
int contactID = ((Contact)param).ID;
_ServiceAgent.DeleteContact(contactID,
(s, e) =>
{
if (!CheckErrorAndResult(e.Error, e.Result, e.errMsg))
return;
SearchContacts();
MessageBox.Show("Delete successful.");
});
}
Here is the line of code that wires up the command in the VM:
DeleteContactCommand = new DelegateCommand(DeleteContact, CanDeleteContact);
BUT, if I replace CanDeleteContact with "(x) => true" for testing, I do NOT get the problem.
Please let me know if I can provide more information. I'm trying to figure out the problem with the Delete button first and hopefully apply the same solution to the Send Fax button.
Thanks,
J
***UPDATE 9/6/2011: I have noticed that in Silverlight navigation, a new instance of the page is created each time. So would that account for why this problem only happens after navigation? And why the values handled in what I thought was an infinite loop are alternating between null and non-null values? Like maybe the old page now has null, but the new page is being shown and is correct, but both are tied to the same static VM object. Just shooting in the dark here.
I think problem is that you are mixing CanExecute and CanExecuteChanged. CanExecuteChanged is definitely going to make somebody call CanExecute (which is calling CanExecuteChanged...an so on...).
So those call should be separate - CanExecute should only return some value (bCanExecuteCache). And CanExecuteChanged should be called on specific events/calls/cases.
Here is some example on Silverlight forum that might help you (shows the use of CanExecuteChanged): concrete post | whole thread