I have a xamarin form page where a user can update some data in a form. I need to intercept the Navigation Bar Back Button Clicked to warn the user if some data have not been saved.How to do it?
I'm able to intercept the hardware Bar Back Button Clicked in Android using the Android.MainActivity.OnBackPressed(), but that event is raised only on hardware Bar Back Button Clicked, not on Navigation Bar Back Button Clicked.
I tried also to override Xamarin.Forms.NavigationPageOnBackButtonPressed() but it doesn't work. Why?
Any one have already solved that issue?
I also tried by overriding OnDisappear(), there are two problems:
The page has already visually disappeared so the "Are you sure?" dialog appears over the previous page.
Cannot cancel the back action.
So, is it possible to intercept the navigation bar back button press?
I was able to show a confirmation dialog that could cancel navigation by overriding the following methods in the FormsApplicationActivity.
// navigation back button
public override bool OnOptionsItemSelected(IMenuItem item)
{
if (item.ItemId == 16908332)
{
var currentViewModel = (IViewModel)navigator.CurrentPage.BindingContext;
currentViewModel.CanNavigateFromAsync().ContinueWith(t =>
{
if (t.Result)
{
navigator.PopAsync();
}
}, TaskScheduler.FromCurrentSynchronizationContext());
return false;
}
else
{
return base.OnOptionsItemSelected(item);
}
}
// hardware back button
public async override void OnBackPressed()
{
var currentViewModel = (IViewModel)navigator.CurrentPage.BindingContext;
// could display a confirmation dialog (ex: "Cancel changes?")
var canNavigate = await currentViewModel.CanNavigateFromAsync();
if (canNavigate)
{
base.OnBackPressed();
}
}
The navigator.CurrentPage is a wrapper around the INavigation service. I do not have to cancel navigation from modal pages so I am only handling the NavigationStack.
this.navigation.NavigationStack[this.navigation.NavigationStack.Count - 1];
The easiest, as #JordanMazurke also somewhat mentions, since the event for the back button cannot be handled currently (other than the physical back button for Android), is to either:
NavigationPage.ShowHasBackButton(this, false)
Pushing a Modal instead of a Page
Then afterwards, you can add an ActionbarItem from where you can handle the Event.
I personally spoke to the iOS team from Xamarin concerning this matter, and they basically told me we shouldn't expect support for handling the Event for the BackButtonPressed in the NavigationBar. The reason being, that on iOS, it's bad practice for the users to receive a message when Back is pressed.
Best way I have found is adding my own NavigationRenderer to intercept the navigation methods and a simple Interface
[assembly: ExportRenderer(typeof(NavigationPage), typeof(CustomerMobile.Droid.NavigationPageRenderer))]
public class NavigationPageRenderer : Xamarin.Forms.Platform.Android.AppCompat.NavigationPageRenderer
{
public Activity context;
public NavigationPageRenderer(Context context)
: base(context)
{}
protected override Task<bool> OnPushAsync(Page page, bool animated) {...}
protected override Task<bool> OnPopToRootAsync(Page page, bool animated){...}
protected override Task<bool> OnPopViewAsync(Page page, bool animated)
{
// if the page implements my interface then first check the page
//itself is not already handling a redirection ( Handling Navigation)
//if don't then let the handler to check whether to process
// Navitation or not .
if (page is INavigationHandler handler && !handler.HandlingNavigation
&& handler.HandlePopAsync(page, animated))
return Task.FromResult(false);
return base.OnPopViewAsync(page, animated);
}
}
Then my INavigationHandler interface would look like this
public interface INavigationHandler
{
public bool HandlingNavigation { get; }
public bool HandlePopAsync(Xamarin.Forms.Page view, bool animated);
public bool HandlePopToRootAsync(Xamarin.Forms.Page view, bool animated);
public bool HandlePuchAsync(Xamarin.Forms.Page view, bool animated);
}
Finally in any ContentView, in this example when trying to navigate back I'm just collapsing a menu and preventing a navigation back.
public partial class MenusList : INavigationHandler
{
public bool HandlingNavigation { get; private set; }
public bool HandlePopAsync(Page view, bool animated)
{
HandlingNavigation = true;
try
{
if (Menu.Expanded)
{
Menu.Collapse();
return true;
}
else return false;
}
finally
{
HandlingNavigation = false;
}
}
}
This is an inherently difficult task and the only way I got around it was to remove the back button entirely and then handle the backwards navigation from a 'save' button.
I have done a brief search of the Xamarin.Forms forum and the following has been suggested:
public override bool OnOptionsItemSelected(Android.Views.IMenuItem item)
{
return false;
}
The link for the post is as follows:
https://forums.xamarin.com/discussion/21631/is-there-nay-way-of-cancelling-the-back-button-event-from-the-navigationpage
As has already been said - you cannot do this cross-platform. However, you can handle it natively with arguably not so much effort: https://theconfuzedsourcecode.wordpress.com/2017/03/12/lets-override-navigation-bar-back-button-click-in-xamarin-forms/
The article covers iOS and Android. If you have a UWP project you'll have to hammer your own solution for it.
Edit:
Here is the UWP solution! It actually turned out to be pretty easy – there is just one back button and it’s supported by Forms so you just have to override ContentPage’s OnBackButtonPressed:
protected override bool OnBackButtonPressed()
{
if (Device.RuntimePlatform.Equals(Device.UWP))
{
OnClosePageRequested();
return true;
}
else
{
base.OnBackButtonPressed();
return false;
}
}
async void OnClosePageRequested()
{
var tdvm = (TaskDetailsViewModel)BindingContext;
if (tdvm.CanSaveTask())
{
var result = await DisplayAlert("Wait", "You have unsaved changes! Are you sure you want to go back?", "Discard changes", "Cancel");
if (result)
{
tdvm.DiscardChanges();
await Navigation.PopAsync(true);
}
}
else
{
await Navigation.PopAsync(true);
}
}
Back button appearance and behavior can be redefined by setting the BackButtonBehavior attached property to a BackButtonBehavior object.
<Shell.BackButtonBehavior>
<BackButtonBehavior Command="{Binding GoBackCommand}"/></Shell.BackButtonBehavior>
GobackCommand defined in your View Model.
You can follow below link for more details:
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/shell/navigation
In Xamarin.Forms, the Page class has an OnBackButtonPressed() method that you can tap into for all platforms
Related
This question already has answers here:
How to detect click/touch events on UI and GameObjects
(4 answers)
Closed 3 years ago.
I have a UI button. I want to show a text when the user is pressing the button and hide the text when the user releases the button.
How can I do this?
this anwser is basically fine but there is a huge drawback: You can't have additional fields in the Inspector since Button already has a built-in EditorScript which overwrites the default Inspector - You would need to extend this via a custom Inspector every time.
I would instead implement it as completely additional component implementing IPointerDownHandler and IPointerUpHandler (and maybe also IPointerExitHandler to reset also when exiting the button while holding the mouse/pointer still pressed).
For the doing something while the button stays pressed I'ld use a Coroutine.
In general I'ld use UnityEvents:
[RequireComponent(typeof(Button))]
public class PointerDownUpHandler : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IPointerEnterHandler, IPointerExitHandler
{
public UnityEvent onPointerDown;
public UnityEvent onPointerUp;
// gets invoked every frame while pointer is down
public UnityEvent whilePointerPressed;
private Button _button;
private void Awake()
{
_button = GetComponent<Button>();
}
private IEnumerator WhilePressed()
{
// this looks strange but is okey in a Coroutine
// as long as you yield somewhere
while(true)
{
whilePointerPressed?.Invoke();
yield return null;
}
}
public void OnPointerDown(PointerEventData eventData)
{
// ignore if button not interactable
if(!_button.interactable) return;
// just to be sure kill all current routines
// (although there should be none)
StopAllCoroutines();
StartCoroutine(WhilePressed);
onPointerDown?.Invoke();
}
public void OnPointerUp(PointerEventData eventData)
{
StopAllCoroutines();
onPointerUp?.Invoke();
}
public void OnPointerExit(PointerEventData eventData)
{
StopAllCoroutines();
onPointerUp?.Invoke();
}
// Afaik needed so Pointer exit works .. doing nothing further
public void OnPointerEnter(PointerEventData eventData) { }
}
Than you can reference any callback in onPointerDown, onPointerUp and whilePointerPressed just the way you would do it with the onClick event of the Button.
You have to create your own custom button by extending Button class and override method OnPoiterDown and OnPointerUp.
Attach MyButton component instead of Button to your gameobject
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public class MyButton : Button
{
public override void OnPointerDown(PointerEventData eventData)
{
base.OnPointerDown(eventData);
Debug.Log("Down");
//show text
}
public override void OnPointerUp(PointerEventData eventData)
{
base.OnPointerUp(eventData);
Debug.Log("Up");
//hide text
}
}
I've started working with the new navigation component and I'm really digging it! I do have one issue though - How am I supposed to handle the back button when I'm at the starting destination of the graph?
This is the code I'm using now:
findNavController(this, R.id.my_nav_host_fragment)
.navigateUp()
When I'm anywhere on my graph, it's working great, it send me back, but when I'm at the start of it - the app crashes since the backstack is empty.
This all makes sense to me, I'm just not sure how to handle it.
While I can check if the current fragment's ID is the same as the one that I know to be the root of the graph, I'm looking for a more elegant solution like some bool flag of wether or not the current location in the graph is the starting location or not.
Ideas?
I had a similar scenario where I wanted to finish the activity when I was at the start destination and do a regular 'navigateUp' when I was further down the navigation graph. I solved this through a simple extension function:
fun NavController.navigateUpOrFinish(activity: AppCompatActivity): Boolean {
return if (navigateUp()) {
true
} else {
activity.finish()
true
}
}
And then call it like:
override fun onSupportNavigateUp() =
findNavController(R.id.nav_fragment).navigateUpOrFinish(this)
However I was unable to use NavigationUI as this would hide the back arrow whenever I was at the start destination. So instead of:
NavigationUI.setupActionBarWithNavController(this, controller)
I manually controlled the home icon:
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_navigation_back)
Override onBackPressed in your activity and check if the current destination is the start destination or not.
Practically it looks like this:
#Override
public void onBackPressed() {
if (Navigation.findNavController(this,R.id.nav_host_fragment)
.getCurrentDestination().getId() == R.id.your_start_destination) {
// handle back button the way you want here
return;
}
super.onBackPressed();
}
You shouldn't override "onBackPressed", you should override "onSupportNavigateUp" and put there
findNavController(this, R.id.my_nav_host_fragment)
.navigateUp()
From the official documentation:
You will also overwrite AppCompatActivity.onSupportNavigateUp() and call NavController.navigateUp
https://developer.android.com/topic/libraries/architecture/navigation/navigation-implementing
In Jetpack Navigation Component, if you want to perform some operation when fragment is poped then you need to override following functions.
Add OnBackPressedCallback in fragment to run your special operation when back present in system navigation bar at bottom is pressed .
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
//perform your operation and call navigateUp
findNavController().navigateUp()
}
}
requireActivity().onBackPressedDispatcher.addCallback(onBackPressedCallback)
}
Add onOptionsItemMenu in fragment to handle back arrow press present at top left corner within the app.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setHasOptionsMenu(true)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
//perform your operation and call navigateUp
findNavController().navigateUp()
return true
}
return super.onOptionsItemSelected(item)
}
If there is no special code to be run when back is pressed on host fragment then use onSupportNavigateUp in Activity.
override fun onSupportNavigateUp(): Boolean {
if (navController.navigateUp() == false){
//navigateUp() returns false if there are no more fragments to pop
onBackPressed()
}
return navController.navigateUp()
}
Note that onSupportNavigateUp() is not called if the fragment contains onOptionsItemSelected()
As my back button works correctly, and using NavController.navigateUp() crashed on start destination back button. I have changed this code to something like this. Other possibility will be to just check if currentDestination == startDestination.id but I want to close Activity and go back to other Activity.
override fun onSupportNavigateUp() : Boolean {
//return findNavController(R.id.wizard_nav_host_fragment).navigateUp()
onBackPressed()
return true
}
/** in your activity **/
private boolean doubleBackToExitPressedOnce = false;
#RequiresApi(api = Build.VERSION_CODES.M)
#Override
public void onBackPressed() {
int start = Navigation.findNavController(this, R.id.nav_host_fragment).getCurrentDestination().getId();
if (start == R.id.nav_home) {
if (doubleBackToExitPressedOnce) {
super.onBackPressed();
return;
}
this.doubleBackToExitPressedOnce = true;
Toast.makeText(MainActivity.this, "Press back again to exits", Toast.LENGTH_SHORT).show();
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
doubleBackToExitPressedOnce = false;
}
}, 2000);
} else {
super.onBackPressed();
}
}
If you mean the start of your "root" navigation graph (just incase you have nested navigation graphs) then you shouldn't be showing an up button at all, at least according to the navigation principles.
Just call this in your back button Onclick
requireActivity().finish()
I have an app where I want to show page A, from which the user can navigate to page B or C, from B back to A or to C, and from C only back to A, even if the user when through B to get to C
Currently when I'm executing the B->C transition I first PopAsync to get back to A and then I do PushAsync to get to C, so that the '
The question is: is there a civilized way to set up this navigation scheme while still relying on the built-in Navigation to keep track of navigation stack - I don't want to do that myself and use PushModalAsync.
Note that (as reflected in the image) A and C aren't the end points of the whole navigation stack, there are pages before A and after C, so the stack has to be preserved.
On iOS the NavigationRenderer has virtual methods OnPopViewAsync and OnPushAsync (similar on Android):
protected override Task<bool> OnPopViewAsync(Page page, bool animated)
{
return base.OnPopViewAsync(page, animated);
}
protected override Task<bool> OnPushAsync(Page page, bool animated)
{
return base.OnPushAsync(page, animated);
}
They call the corresponding base method with two arguments, the page and whether to animate the transition. Thus, you might be able to enable or disable the animation using the following approach:
Derive a custom navigation page.
Add an "Animated" property.
Derive a custom navigation renderer for your custom navigation page.
Override the pop and push methods calling their base methods with the "Animated" property.
Note that I haven't tried this approach, yet, since it is quite some work to do. But disabling animations on all navigation pages did work this way.
Edit: It took me several hours to actually implement my solution for my own project. Therefore, I'll share some more details. (I developed and tested on Xamarin.Forms 1.2.3-pre4.)
The custom navigation page
Besides the above-mentioned Animated property my navigation page re-implements the two transition functions and adds an optional argument animated, which is true by default. This way we'll be able to keep all existing code and only add a false where needed.
Furthermore, both method will sleep for a very short time (10 ms) after pushing/popping the page. Without this delay we'd ran into trouble with consecutive calls.
public class CustomNavigationPage: NavigationPage
{
public bool Animated { get; private set; }
public CustomNavigationPage(Page page) : base(page)
{
}
// Analysis disable once MethodOverloadWithOptionalParameter
public async Task PushAsync(Page page, bool animated = true)
{
Animated = animated;
await base.PushAsync(page);
await Task.Run(delegate {
Thread.Sleep(10);
});
}
// Analysis disable once MethodOverloadWithOptionalParameter
public async Task<Page> PopAsync(bool animated = true)
{
Animated = animated;
var task = await base.PopAsync();
await Task.Run(delegate {
Thread.Sleep(10);
});
return task;
}
}
The custom navigation renderer
The renderer for my custom navigation page overrides both transition methods and passes the Animated property to their base methods. (It's kind of ugly to inject a flag this way, but I couldn't find a better solution.)
public class CustomNavigationRenderer: NavigationRenderer
{
protected override Task<bool> OnPopViewAsync(Page page, bool animated)
{
return base.OnPopViewAsync(page, (Element as CustomNavigationPage).Animated);
}
protected override Task<bool> OnPushAsync(Page page, bool animated)
{
return base.OnPushAsync(page, (Element as CustomNavigationPage).Animated);
}
}
This is for iOS. But on Android it's almost identically.
An example application
To demonstrate the possibilities of consecutively pushing and popping pages, I wrote the following application.
The App class simply creates a new DemoPage wrapped into a CustomNavigationPage. Note that this instance must be publicly accessible for this example.
public static class App
{
public static CustomNavigationPage NavigationPage;
public static Page GetMainPage()
{
return NavigationPage = new CustomNavigationPage(new DemoPage("Root"));
}
}
The demo page contains a number of buttons that push and pop pages in different orders. You can add or remove the false option for each call to PushAsync or PopAsync.
public class DemoPage: ContentPage
{
public DemoPage(string title)
{
Title = title;
Content = new StackLayout {
Children = {
new Button {
Text = "Push",
Command = new Command(o => App.NavigationPage.PushAsync(new DemoPage("Pushed"))),
},
new Button {
Text = "Pop",
Command = new Command(o => App.NavigationPage.PopAsync()),
},
new Button {
Text = "Push + Pop",
Command = new Command(async o => {
await App.NavigationPage.PushAsync(new DemoPage("Pushed (will pop immediately)"));
await App.NavigationPage.PopAsync();
}),
},
new Button {
Text = "Pop + Push",
Command = new Command(async o => {
await App.NavigationPage.PopAsync(false);
await App.NavigationPage.PushAsync(new DemoPage("Popped and pushed immediately"));
}),
},
new Button {
Text = "Push twice",
Command = new Command(async o => {
await App.NavigationPage.PushAsync(new DemoPage("Pushed (1/2)"), false);
await App.NavigationPage.PushAsync(new DemoPage("Pushed (2/2)"));
}),
},
new Button {
Text = "Pop twice",
Command = new Command(async o => {
await App.NavigationPage.PopAsync(false);
await App.NavigationPage.PopAsync();
}),
},
},
};
}
}
Important hint: It cost me hours of debugging to find out that you need to use an instance of NavigationPage (or a derivative) rather than the ContentPage's Navigation! Otherwise the immediate call of two or more pops or pushes leads to strange behaviour and crashes.
#Falko
You now have the possibility to include a boolean parameter:
Navigation.PushAsync (new Page2Xaml (), false);
Xamarin Documentation
Currently the Xamarin Forms navigation is very spartanic and I doubt there is a nice way to achieve that. Besides Doing and extra "Pop" when necessary.
Here's a collection of snippets I whipped together along with some other niceties to improve NaviagationPage for iOS. Link to comment and code on xamarin forums.
What I would do if I were doing this is push Page C on to your NavigationStack and then take page B off of the stack. That way when you pop from page C, you would go to page A.
// Push the page you want to go to on top of the stack.
await NavigationPage.PushAsync(new CPage()));
// Remove page B from the stack, so when you want to go back next time
//you will go to page A.
Navigation.RemovePage(Navigation.NavigationStack[Navigation.NavigationStack.Count - 2] );
Alternatively, when even you pop from page C, you could remove all instances of type page B from the stack, and then pop back 1. In that case, page B would remain on the stack until you were about to move back from page C to page A.
I need to recreate new page instance on every page load (also when user pressed Back button).
So I overrided OnBackKeyPress method:
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
if (NavigationService.CanGoBack) {
e.Cancel = true;
var j = NavigationService.RemoveBackEntry();
NavigationService.Navigate(j.Source);
NavigationService.RemoveBackEntry();
}
}
The problem is that I can't handle case when user press back button to close CustomMessageBox dialog. How can I check it? Or is there any way to force recreation of page instance when going back through history state?
Why do you need to recreate the page instance? If you are simply trying to re-read the data to be displayed, why not put the data loading logic into OnNavigatedTo()?
Assuming that is what you are actually trying to achieve, try something like this...
public partial class MainPage : PhoneApplicationPage
{
// Constructor
public MainPage()
{
InitializeComponent();
// don't do your data loading here. This will only be called on page creation.
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
LoadData();
base.OnNavigatedTo(e);
}
MyViewModel model;
async void LoadData()
{
model = new MyViewModel();
await model.LoadDataAsync();
}
}
If you also have specific logic that you need to run on first construction of the page vs. on a back key navigation, check the NavigationMode property of the NavigationEventArgs object that gets passed to OnNavigatedTo.
if(e.NavigationMode == NavigationMode.New)
{
//do what you need to do specifically for a new page instance
}
if (e.NavigationMode == NavigationMode.Back)
{
// do anything specific for back navigation here.
}
Ha, in the near thread, i have opposite question :)
What about MessageBox - it depends, which one are you using. It can be custom message box, for example. Anyway, try to check MessageBox.IsOpened (or alternative for your MessageBox) in your OnBackKeyPress().
Another solution is to use OnNavigatedTo() of the page you want to be new each time.
Third solution: in case you works with Mvvm Light, add some unique id in ViewModel getter, like
public MyViewModel MyViewModel
{
get
{
return ServiceLocator.Current.GetInstance<MyViewModel>((++Uid).ToString());
}
}
This would force to recreate new ViewModel each time, so you'd have different instance of VM, so you would have another data on the View.
I have a list of items that goes to another page, That page is hooked up to a view model. In the constructor of this view model I have code that grabs data from the server for that particular item.
What I found is that when I hit the back button and choose another item fromt hat list and it goes to the other page the constructor does not get hit.
I think it is because the VM is now created and thinks it does not need a new one. I am wondering how do I force a cleanup so that a fresh one is always grabbed when I select from my list?
I faced the same issue, that's how i solved it.
Have a BaseView class, override OnNavigatedTo
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (NavigatedToCommand != null && NavigatedToCommand.CanExecute(null))
NavigatedToCommand.Execute(null);
}
add DependencyProperty.
public static readonly DependencyProperty NavigatedToCommandProperty =
DependencyProperty.Register("NavigatedToCommand", typeof(ICommand), typeof(BaseView), null);
public ICommand NavigatedToCommand
{
get { return (ICommand)GetValue(NavigatedToCommandProperty); }
set { SetValue(NavigatedToCommandProperty, value); }
}
On the necessary pages, add to xaml (and, of course, inherit BaseView )
NavigatedToCommand="{Binding OnNavigatedToCommand}"
In the ViewModel, make command itself
public RelayCommand OnNavigatedToCommand
{ get { return new RelayCommand(OnNavigatedTo); } }
and implement method you want to call to update list
public async void OnNavigatedTo()
{
var result = await myDataService.UpdateMyList();
if (result.Status == OK)
MyList = result.List;
}
So, now, every time you navigate to page with list, inside of overriden OnNavigatedTo(), a NavigatedToCommand would be executed, which would execute OnNavigatedToCommand (which you set in xaml), which would call OnNavigatedTo, which would update your list.
A bit messy, but MVVM :)
EDIT: What about cleanings, they can be done in OnNavigatedFrom(), which works the same. Or OnNavigatingFrom(), which also can be useful in some cases.