I have a page "PageA" which listens to a Changed event on a global object.
The page is added in the navigation stack like this:
await Navigation.PushAsync(new PageA());
The page is removed by clicking the back button in the navigation bar.
When I do the same x times, I have x times PageA instances listening to the Changed event. So PageA is created over and over again, but is never really removed.
How do I really get rid of the PageA when clicking the back button?
Example of what you are doing
public partial class MyPage : ContentPage
{
MyPage()
{
this.SomeEvent += SomeEventBeyondTheScopeOfThisPage();
}
protected override void OnDisappearing()
{
//You should add this
this.SomeEvent -= SomeEventBeyondTheScopeOfThisPage();
}
}
Which even if the page is popped from your navigation stack does not remove the reference from MyPage.SomeEvent to SomeEventBeyondTheScopeOfThisPage. So when the Garbage Collector comes along this page is going to stay and this event is going to continue to be listened for.
Just detach the event in OnDisappearing for the simple answer. You dont dispose the page you just detach all references and events outside of its scope. A better idea would be to make your code more modular and not worry about detaching the event. If the event source were coming from within the page itself it would not need to be detached.
public partial class MyPage : ContentPage
{
MyPage()
{
this.SomeEvent += SomeEventWithinPagesScope();
}
private Task SomeEventWithinPagesScope()
{
//My cool event goes here
}
}
If it does have to be global another option would be Messaging Center. I do not believe you have to detach those listeners but I could be wrong.
https://developer.xamarin.com/guides/xamarin-forms/messaging-center/
another reason: NavigationPage.CurrentNavigationTask.Result still refer to the popped page instance. that is why GC will not collect it. unless you navigate to other pages.
Related
My Xamarin Forms Application has a MainPage set to
new NavigationPage(new CarsMasterDetailPage())
where CarsMasterDetailPage : MasterDetailPage.
In CarsMasterDetailPage constructor, I set the Master property to new CarsMasterPage() and the Detail property to new CarsDetailPage(). Both CarsMasterPage and CarsDetailPage extend ContentPage.
The master page contains a list of cars and a button which has an event handler that does:
await Navigation.PushAsync(new AddCar());
The AddCar page has a button with an event handler that does:
await Navigation.PopAsync();
When I first run the app, the OnAppearing method of the master page is called. The first time the navigation pops back to the master page, OnAppearing is called again. Subsequent navigation pushes and pops don't, though.
I can get around it by adding a delegate on the master page that the add car page calls when it's done, but that feels hacky since there are page events to handle this. Does anyone know why it's not working?
In my case, I did it like this and everything works fine:
My MainPage:
MainPage = new MasterDetailPage {
Master = new MasterDetailPage1Master(),
Detail = new MyNavigationPage(new MainPageLinearList(appType))
};
So the key point was to use NavigationPage in a DetailPage. After that everything works fine.
Subscribe to the NavigationPage.Popped event:
navigationPage.Popped += OnPopped;
...
void OnPopped(object sender, NavigationEventArgs e)
{
var currentPage = (sender as NavigationPage).CurrentPage;
var removedPage = e.Page;
...
}
I have a Xamarin.Forms application which uses a TabbedPage, let's call it T, T consists of 3 ContentPage children A, B and C. Since the usere has the possibility to edit some data on tab B, I want to notify user before leaving tab in order to allow him to cancel the navigation change and save changes first or to discard changes and leave. So far I have managed to override OnBackButtonPressed() method and the navigation bar back button (which would exit TabbedPage). However I quickly noticed that I am still loosing changes when switching between tabs. I would like to override the click on new tab, so I could first present user with the leaving dialog and the skip the change or continue with it. What would be the best way to do this? I am currently working only on Android platform, so solutions on the platform level are also acceptible.
Thank you for your suggestions and feedback :)
I do not think there is an easy way to do this ,
you can use OnDissappearing and OnAppearing for the pages, that is as easy as it gets .
However I think you are using the wrong design.
Having tabs are ment to make it easier to navigate between pages, if you are going to notify the user when changing the tabs then it would be annoying . If I were you i would save the data for each page locally. so when you get back to the page you will have the data anyway.
So in the end I followed the advice of Ahmad and implemented the persisting of data on individual tabs so they are not lost when tabs are switched. (I no longer refresh input fields from data from model when OnAppearing is called).
But in order to know if there are some unsaved changes on my ChildB page, I had to implement the following procedures:
I created the method HandleExit on my ChildB page, which checks for unsaved changes in fields (at least one value in input fields is different from the ones in stored model) and the either prompts the user that there are unsaved changes (if there are some) or pops the navigation stack if there are no changes.
private async Task HandleExit()
{
if(HasUnsavedChanges())
{
var action = await DisplayAlert("Alert", "There are unsaved changes, do you want to discard them?", "Discard changes", "Cancel");
if(!action)
{
return;
}
}
await Navigation.PopAsync();
}
Since there are two ways on how user can return from Tabbed page (pressing the back button on device or pressing the back button in navigation bar, I had to:
A: override the back button method on my ChildB page, so it calls the HandleExit method. But since Navigation.PopAsync() needs to be called on UI thread, I had to explicitly execute the method on UI thread as written below:
protected override bool OnBackButtonPressed()
{
Device.BeginInvokeOnMainThread(new Action(async () =>
{
await HandleExit();
}));
return true;
}
B: Since there is no way to intercept the navigation bar back button on the ContentPage, I had to intercept the event on the platform level (Android) and then pass the event to the ContentPage if necessary via MessagingCenter. So first we need to intercept the event, when navigation bar button is pressed in one of the child pages and send the event via MessagingCenter. We can do that but adding the following method in our MainActivity.cs class:
public override bool OnOptionsItemSelected(IMenuItem item)
{
// check if the current item id
// is equals to the back button id
if (item.ItemId == 16908332)
{
// retrieve the current xamarin forms page instance
var currentpage = Xamarin.Forms.Application.Current.MainPage.Navigation.NavigationStack.LastOrDefault();
var name = currentpage.GetType().Name;
if(name == "ChildA" || name == "ChildB" || name == "ChildC")
{
MessagingCenter.Send("1", "NavigationBack");
return false;
}
}
return base.OnOptionsItemSelected(item);
}
Now whenever we will press the navigation bar back button in one of the child pages (ChildA, ChildB, ChildC) nothing will happen. But the button will work as before on the rest of the pages. For the second part of solution we need to handle the message from MessagingCenter, so we need to subscribe to it in our ChildB page. We can subsribe to the message topic in OnAppearing method as follows:
MessagingCenter.Subscribe<string>(this, "NavigationBack", async (arg) => {
await HandleExit();
});
Be careful to unsubscribe to the topic in OnDisappearing() otherwise strange things could happen, since there will be references left to your ContentPage even if you pop it from your navigation stack.
Now that we have handled both requests for back navigation in our ChildB page, we also need to handle them in all of remaining child pages (ChildA, ChildC), so they will know if there are unsaved changes in ChildB page, even if it is currently not selected. So the solution is again compraised of handling the device back button, and navigation bar back button, but first we heed a way to check if ChildB has unsaved changes when we are on one of the remaining pages, so we again write HandleExit method but this time it is as follows:
private async Task HandleExit()
{
var root = (TabbedPage)this.Parent;
var editPage = root.Children.Where(x => x.GetType() == typeof(ChildB)).FirstOrDefault();
if(editPage != null)
{
var casted = editPage as ChildB;
if (casted.HasUnsavedChanges())
{
var action = await DisplayAlert("Alert", "There are unsaved changes, do you want to discard them?", "Discard changes", "Cancel");
if (!action)
{
return;
}
}
}
await Navigation.PopAsync();
}
The only thing that remains now is to handle both navigation back events inside remaing child pages. The code for them is the same as in the actual ChildB page.
A: Handling the device back button.
protected override bool OnBackButtonPressed()
{
Device.BeginInvokeOnMainThread(new Action(async () =>
{
await HandleExit();
}));
return true;
}
B: Subscribing to topic from MessagingCenter
MessagingCenter.Subscribe<string>(this, "NavigationBack", async (arg) => {
await HandleExit();
});
If everthing has been done correctly, we should now be prompted with a dialog on any of the child pages if there are unsaved changes on the ChildB page. I hope this will help somebody in the future :)
I have AVPlayer and when i move to next page, player continues playing and if i add observers they crash if i don't dispose them but i am unable to find a way to handle dispose because ContentView doesn't tell you when it is in background.
Please help ?
In Xamarin Forms there is no way for a ContentView to find out when it's hosting page is disappearing, unless it gets some help from the Page itself. So how I've achieved this previously is as follows:
Step 1) Define an OnDisappearing Method in your ContentView
In your ContentView define a method called OnDisappearing and inside it, do whatever you need to when the view disappears - in your case it sounds like you need to remove your observers and dispose your player. So it would look something like this:
public void OnDisappearing()
{
_playerPositionChangedObserver?.Dispose();
_player?.Dispose();
}
Defining this method in itself won't do anything, you need to actually call it from somewhere - that's where the page comes in...
Step 2) Override the OnDisappearing method in your page
The page will be told when it's appearing or disappearing and you can use that to then forward on to your other ContentView. In the example below MyPage is overriding the OnDisappearing method and then calling that method that I defined on my ContentView in step 1.
public class MyPage : ContentPage
{
public MyPage()
{
InitializeComponent();
}
/// <summary>
/// Performs page clean-up.
/// </summary>
protected override void OnDisappearing()
{
base.OnDisappearing();
contentView.OnDisappearing();
}
}
Right now, what I have is, when the user clicks on the page, the page will automatically send the user to a webtask which opens up a pdf file.
What's happening right now is when the user presses the back button, it goes back to the Original page for a split second, before being redirected back to the pdf as I have assigned it to (due to the onnavigateto function)
How would I make it so that, when the user clicks the back button in the pdf document, the app will take the user back to the main page?
Also, on the main page, how do I ensure that the backstack is cleared? (As the Application HAS to exit on the MainPage, so can't go back to the pdf.)
My Code so far, I have tried...
{
public partial class Page2 : PhoneApplicationPage
{
public Page2()
{
InitializeComponent();
}
//as soon as this page is opened, navigate/redirect it to the URL below
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
base.OnNavigatedTo(e);
WebBrowserTask task = new WebBrowserTask() { URL ="http://test.com/test.pdf"};
task.Show();
}
//when the user clicks the hardware back button, instead of taking them to the daily notices, which will send them back to brower
// send the user to the main page
protected override void OnBackKeyPress
(System.ComponentModel.CancelEventArgs e)
{
base.OnBackKeyPress(e);
new Uri("/MainPage.xaml", UriKind.Relative);
}
First of all, why you need the second page that only opens a WebBrowserTask? You can do this from main page.
If you still want to open from second page, you can move WebBrowserTask to constructor and surround it with Dispatcher. This approach is guaranteed that WebBrowserTask will be called only once after navigation to this page (maybe will be some problems with tombstoning). Or, you can save state to PhoneApplicationPage.State to handle where user was and what you should open next.
For clearing back stack you can use next code:
while (NavigationService.BackStack.Any())
{
NavigationService.RemoveBackEntry();
}
You will have to detect this on the application level, rather than the page level. When you 'redirect' the user to the PDF, your application becomes suspended. When they then navigate back, it is resumed.
The Visual Studio template provides a method that it invoked when the application resumes:
// Code to execute when the application is activated (brought to foreground)
// This code will not execute when the application is first launched
private void Application_Activated(object sender, ActivatedEventArgs e)
{
}
Within the above method, you could set a flag, that you then check when your page is navigated to,, that indicates a resume has occurred.
What i use:
a list of 10 webbrowsers
a tabIndex ( index of current webbrowser)
different pages that use NavigationService.GoBack() to get to the Mainpage.
the problem:
everytime i use GoBack() to get to the mainpage and navigate, the Navigated-event will be fired 1 time more.
Thats a huge performance issue after some surfing but i don't know why it's happening.
what i do in OnNavigatedTo:
fill the webbrowserlist if count != 10 (global list, only 1 time happening)
set Events for every browsers (maybe the problem, but can't imagine why)
thanks for your help.
If I understand your problem than it is that the webbrowsers Navigated event fires more and more time as you navigate back and forth between the pages.
Without seeing the code I would say that the problem is that you subscribe to the navigated event every time you navigate back to your main page. You could avoid this by:
1) Subscribing to the events in the main pages constructor, becuase it is getting called one time only
2) If you have to subscribe to the events in the pages OnNavigatedTo event than do this checking before:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
if (e.NavigationMode != NavigationMode.Back)
{
webbrowser.tap += someFunction;
}
}
if you need to register to the events every time you navigate to the page than to the following:
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
webbrowser.tap -= someFunction;
}