Memory leaks with NavigationController and multiple UIViewController - ios

In my application I manage 4 UIViewController :
First one for handle user login
Second one that display the menu
Third one for displaying the action available depending on the user entry in previous view.
Fourth one that show details infos about previous selected item on third view.
Third and Four are displaying a UIViewTable with item to select that are passed to the next UIViewController to display info.
So I made attention to not using hard link to each other. Parameters are passed when UIViewControlle are instantiate.
Here is the probleme that I'm dealing with :
When I pass from Third to Fourth, and then go back to Fourth, memory is never been released. Worth, when I return to Fourth it take the same amount of memory again.
After 3-4 round-trip the device memory is full. (Another strange behavior here with UIImageView that take 12Mo of memory any picture, no mater the picture size is 12Ko or 120Ko...)
Here is the code that is use to switch between controllers :
//First UIViewController, tagged as Root in storyboard
public partial class LoginController
{
//....some logic code here
//User ask for login by pressed a button , after an echange with a server
//for credential verification, if the server reply "ok" then I call the second
//UIViewController like this
void PushHomeScreen()
{
HomeController homeController = Storyboard.InstantiateViewController("home") as HomeController;
NavigationController.PushViewController(homeController, false);
}
}
public partial class HomeController
{
//....some logic code here
//The view displaying 3 UIButton that root to 2 different UIViewController.
//First one is deadend, and user can only go back. It will be used one time to select a dataset to download and use after.
//The two next use the same UIViewController, only the way that the controller handle the nexts user action is different :
//If user came from the Second then it display screen for retrieving user attendance
//If user came from the Third then it display screen for retrievin user survey.
//So here are the code behind:
private void BtnDownload_TouchUpInside(object sender, EventArgs e)
{
DownloadController downloadTrainingController = Storyboard.InstantiateViewController("downloadTraining") as DownloadController;
NavigationController.PushViewController(downloadTrainingController, false);
}
private void BtnSurvey_TouchUpInside(object sender, EventArgs e)
{
TrainingAndReviewListController trakningController = Storyboard.InstantiateViewController("trakning") as TrainingAndReviewListController;
trakningController.backgroundColor = "violet";
NavigationController.PushViewController(trakningController, false);
}
private void BtnTraining_TouchUpInside(object sender, EventArgs e)
{
TrainingAndReviewListController trakningController = Storyboard.InstantiateViewController("trakning") as TrainingAndReviewListController;
trakningController.backgroundColor = "red";
NavigationController.PushViewController(trakningController, false);
}
}
//I will not show you the DownloadController as it is not pertinent to the current probleme.
public partial class TrainingAndReviewListController
{
//To handle the back action, so user go from this to HomeController
void BtnHome_TouchUpInside(object sender, EventArgs e)
{
NavigationController.PopViewController(false);
}
private void Tablesource_OnRowSelected(object sender, DownloadTableSource.RowSelectedEventArgs e)
{
serviceController = Storyboard.InstantiateViewController("service") as ServiceController;
serviceController.backgroundColor = backgroundColor;
serviceController.training = e.training;
NavigationController.PushViewController(serviceController, false);
}
}
//Next are the final UIViewController on the storyboard. It consume a lot of memory in inspector due to 20 UIViewImage on it who each consume 12Mo of memory.
//When I come back from this Controller to the previous memory are never released, and if I return on it, it consume the same amount extra again
//One solution to prevent this is to instanciate him once in TrainingAndReviewListController as class attribute, and then reuse it. But it is not supposed to be the role
//of the NavigationController?
public partial class ServiceController
{
public string backgroundColor = "red";
public Training training;
//To handle the user action for going to the Home controller, this is not the action that cause memory leaks, altoutgh it will cause the same behavior I think.
void BtnHome_TouchUpInside(object sender, EventArgs e)
{
foreach (UIViewController uiviewcontroller in NavigationController.ViewControllers) {
if (uiviewcontroller.GetType() == typeof(HomeController)) {
NavigationController.PopToViewController(uiviewcontroller as HomeController, false);
}
}
}
void BtnGoBack_TouchUpInside(object sender, EventArgs e)
{
Console.WriteLine(NavigationController.ViewControllers.Count()); //It always log the same count, as if the controller is really be poped when i come back and pop again.
//But profilage show me that memory still used by this instance.
NavigationController?.PopViewController(false);
}
}

Related

When are .NET MAUI (Xamarin Forms) pages/custom views destroyed?

I'm new to Xamarin/.NET MAUI application development. I started to develop a sample .NET MAUI application for Android device.
I'm trying to understand how/when a page and my custom view are destroyed (disposed of). I read some web pages but I can't really understand how things work in .NET MAUI (or Xamarin).
I have three pages: MainPage, SecondPage, TestMapPage.
SecondPage has a button that navigates to TestMapPage. It instantiates a TestMapPage object and passes it to Navigation.PushAsync().
TestMapPage contains a custom view TestMapView, which is rendered by my custom view renderer TestMapViewRenderer. I create a MapView object (from Naxam.Mapbox.Droid) in the renderer and show the map in TestMapPage. The map appears on the emulator and it works fine.
I thought that SecondPage, TestMapPage and TestMapView (and all the objects in TestMapViewRenderer) will be destroyed when I navigate back to MainPage. However, when I set a break point on Dispose() in the renderer and navigate back to SecondPage or MainPage in , it never gets hit.
My questions:
Are the SecondPage, TestMapPage, TestMapView and all the other objects in the view and view renderer like MapboxMap kept somewhere when I go back to MainPage?
When are pages and views destroyed/disposed of?
If those page objects are kept somewhere until the application shuts down, is it normal behaviour?
If not normal behaviour, how do I fix it?
I'm worried about memory leak...
MainPage.xaml.cs
public partial class MainPage : ContentPage
{
// ...
private async void OnGoToSecondPageClicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new SecondPage());
}
}
SecondPage.xaml.cs
public partial class SecondPage : ContentPage
{
// ...
private async void OnMapShowClicked(object sender, EventArgs e)
{
await Navigation.PushAsync(new TestMapPage());
}
}
TestMapPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MapTest"
x:Class="MapTest.TestMapPage">
<StackLayout Margin="5">
<local:TestMapView
x:Name="map"
VerticalOptions="FillAndExpand"
HorizontalOptions="CenterAndExpand"/>
</StackLayout>
</ContentPage>
TestMapView.cs
public class TestMapView : View { }
TestMapViewRenderer.cs
public partial class TestMapViewRenderer : ViewRenderer<TestMapView, Android.Views.View>
{
private MapboxMap map;
public TestMapViewRenderer(Context context) : base(context) {}
protected override void OnElementChanged(ElementChangedEventArgs<TestMapView> e)
{
base.OnElementChanged(e);
// ...
if (Control == null)
{
var mapView = new MapView(Context);
SetNativeControl(mapView);
mapView.GetMapAsync(this);
}
}
public void OnMapReady(MapboxMap map)
{
this.map = map;
this.map.SetStyle(Resources.GetString(Resource.String.mapbox_style_satellite), this);
}
protected override void Dispose(bool disposing)
{
// A breakpoint never hits on this line. Why?
base.Dispose(disposing);
}
// ...
}
as far as I know;
No, on each navigation you create new page instances, so, the page is created and kept in memory by .NET itself, once garbage collection hits a certain threshold, the memory will be free. Until then, the pages are in memory, but not meant to use somewhen in the future.
When GC decides, the Xamarin/MAUI doesn't care about views.
Unfortunately, yes.
It's normal, you can overcome by using what #ToolmakerSteve refers.

ICommand not always firing when tab selected

I have a simple ActionBar with 3 tabs attached. When a tab is clicked, the fragment is inflated and the view shows. The tab being click event is fired using an event. Initially, the first fragment is inflated, but the others respond and inflate if clicked.
If I change the event being fired to an ICommand, only the last fragment is inflated and then if I click on the first tab, that and the last are inflated. Never the second.
My code is this
ICommand TabClicked
{
get
{
return new RelayCommand(() =>
{
tab.TabSelected += (object sender, ActionBar.TabEventArgs e) => TabOnTabSelected(sender, e);
});
}
}
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.Main);
ActionBar.NavigationMode = ActionBarNavigationMode.Tabs;
fragments.Add(new TODFragment());
fragments.Add(new ConditionsFragment());
fragments.Add(new ResultsFragment());
AddTabToActionBar("Time", Resource.Drawable.crucifix_colour);
AddTabToActionBar("Conditions", Resource.Drawable.weather_colour);
AddTabToActionBar("Results", Resource.Drawable.tod_colour);
}
void AddTabToActionBar(string text, int iconResourceId)
{
tab = ActionBar.NewTab().SetTag(text).SetText(text).SetIcon(iconResourceId);
/* uncomment and comment out one of the two below to see the difference in operation */
tab.TabSelected += TabOnTabSelected;
//tab.SetCommand<ActionBar.TabEventArgs>("TabSelected", TabClicked);
ActionBar.AddTab(tab);
}
void TabOnTabSelected(object sender, ActionBar.TabEventArgs tabEventArgs)
{
var tabNo = sender as ActionBar.Tab;
var frag = fragments[tabNo.Position];
tabEventArgs.FragmentTransaction.Replace(Resource.Id.frameLayout1, frag);
}
Am I missing something fundamental here in the difference between ICommands and Events or is it something else?
I'm using Xam.Android and MVVMLight
I found the answer. When I create the partial class I define the UI objects like this (or something like this at least)
EditText myEditText;
EditText MyEditText = myEditText ?? (view.FindViewById<EditText>(Resources.Id.myEdit);
This is fine, but it does mean that once defined, it doesn't get redefined.
Not a problem if the UI is not really going to change, but every time an action tab is pressed, the fragment is refreshed. Only problem is the Id isn't changing as myEditText is not null.
The answer is add a method in the UI definition code that nulls the objects then in the main code, when the UI disappears, call the nulling method. Everything works then

How to open custom dialog box / popup using Xamarin.Forms?

I am newbie to Xamarin.Forms and stuck with a situation where I want to open up a popup box with my control details [e.g. View Employee Details] on click of parent page.
How can I open custom dialog box / popup using Xamarin.Forms?
Any example code will be appreciated?
Thanks in advance!
If you still want to have your popup's code in its own Page you can set up some custom renderers along the following logic.
1. A ModalPage & corresponding renderer
public class ModalPage : ContentPage { }
public class ModalPageRenderer : PageRenderer {
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
this.View.BackgroundColor = UIColor.Clear;
this.ModalPresentationStyle = UIModalPresentationStyle.OverCurrentContext;
}
public override void ViewDidLayoutSubviews()
{
base.ViewDidLayoutSubviews();
SetElementSize (new Size (View.Bounds.Width, View.Bounds.Height));
}
}
2. HostPage
public class ModalHostPage : ContentPage, IModalHost
{
#region IModalHost implementation
public Task DisplayPageModal(Page page)
{
var displayEvent = DisplayPageModalRequested;
Task completion = null;
if (displayEvent != null)
{
var eventArgs = new DisplayPageModalRequestedEventArgs(page);
displayEvent(this, eventArgs);
completion = eventArgs.DisplayingPageTask;
}
// If there is no task, just create a new completed one
return completion ?? Task.FromResult<object>(null);
}
#endregion
public event EventHandler<DisplayPageModalRequestedEventArgs> DisplayPageModalRequested;
public sealed class DisplayPageModalRequestedEventArgs : EventArgs
{
public Task DisplayingPageTask { get; set;}
public Page PageToDisplay { get; }
public DisplayPageModalRequestedEventArgs(Page modalPage)
{
PageToDisplay = modalPage;
}
}
}
3. HostPage renderer
public class ModalHostPageRenderer: PageRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
if(e.OldElement as ModalHostPage != null)
{
var hostPage = (ModalHostPage)e.OldElement;
hostPage.DisplayPageModalRequested -= OnDisplayPageModalRequested;
}
if (e.NewElement as ModalHostPage != null)
{
var hostPage = (ModalHostPage)e.NewElement;
hostPage.DisplayPageModalRequested += OnDisplayPageModalRequested;
}
}
void OnDisplayPageModalRequested(object sender, ModalHostPage.DisplayPageModalRequestedEventArgs e)
{
e.PageToDisplay.Parent = this.Element;
var renderer = RendererFactory.GetRenderer (e.PageToDisplay);
e.DisplayingPageTask = this.PresentViewControllerAsync(renderer.ViewController, true);
}
}
Then it is as simple as calling
await ModalHost.DisplayPageModal(new PopUpPage());
from your host page or in this particular case from the ViewModel behind.
What Pete said about PushModalAsync / PopModalAsync still remains valid for this solution too (which in my opinion is not a disadvantage), but your popup would appear with transparent background.
The main advantage of this approach, in my opinion, is that you can have your popup XAML/code definition separate from the host page and reuse it on any other page where you wish to show that popup.
The general purpose of what you are trying to achieve can be accomplished by using the PushModalAsync and PopModalAsync methods of Xamarin.Forms Navigation object.
The chances are that this is good enough for what you are needing - However - this isn't truely modal. I will explain after a small code snippet:-
StackLayout objStackLayout = new StackLayout()
{
};
//
Button cmdButton_LaunchModalPage = new Button();
cmdButton_LaunchModalPage.Text = "Launch Modal Window";
objStackLayout.Children.Add(cmdButton_LaunchModalPage);
//
cmdButton_LaunchModalPage.Clicked += (async (o2, e2) =>
{
ContentPage objModalPage = new ContentPage();
objModalPage.Content = await CreatePageContent_Page2();
//
await Navigation.PushModalAsync(objModalPage);
//
// Code will get executed immediately here before the page is dismissed above.
});
//
return objStackLayout;
private async Task<StackLayout> CreatePageContent_Page2()
{
StackLayout objStackLayout = new StackLayout()
{
};
//
Button cmdButton_CloseModalPage = new Button();
cmdButton_CloseModalPage.Text = "Close";
objStackLayout.Children.Add(cmdButton_CloseModalPage);
//
cmdButton_CloseModalPage.Clicked += ((o2, e2) =>
{
this.Navigation.PopModalAsync();
});
//
return objStackLayout;
}
The problem with the above is that the
await Navigation.PushModalAsync(objModalPage);
will immediately return after the animation.
Although you can't interact with the previous page, as we are displaying a new NavigationPage with a Close button shown - the parent Navigation Page is still executing behind the scenes in parallel.
So if you had any timers or anything executing these still would get called unless you stopped those.
You could also use the TaskCompletionSource approach as outlined in the following post also How can I await modal form dismissal using Xamarin.Forms?.
Note - that although you can now await the 2nd page displaying and then when that page is dismissed allowing code execution to continue on the next line - this is still not truely a modal form. Again timers or anything executing still will get called on the parent page.
Update 1:-
To have the content appear over the top of existing content then simply include it on the current page, however make this section of content invisible until you need it.
If you use an outer container such like a Grid that supports multiple child controls in the same cell, then you will be able to achieve what you want.
You will also want to use something like a filled Box with transparency that will cover the entire page also, to control the visible, see through section, that surrounds your inner content section.
I followed above approach and found it impossible to run on iOS 7.
I found this library BTProgressHUD which you can modify and use.
I Use its methods by Dependency service.
Actual library for popups.
https://github.com/nicwise/BTProgressHUD
Following example uses BTProgressHUD library internally.
https://github.com/xximjasonxx/ScorePredictForms

Wait for PresentModalViewController to finish (monotouch)

I would like to wait for a controller being displayed with PresentModalViewController() to finish its job before resuming execution (like modal dialogs in WinForms). How can this be done with monotouch?
I know there is a similar question on SO but the answer is for Objective-C and, frankly, I don't get it.
Many thanks.
EDIT
Here's the first setup I tried and which didn't seem to work:
Create a new Project (Single View Application);
Add two controllers (iPhone View Controller): FirstController and SecondController; the first controller overrides DismissModalViewControllerAnimated and fires a OnFirstFinished event just after being dismissed;
In the main controller:
...
public partial class TestModalViewController : UIViewController
{
private UIButton button;
private FirstController first;
private SecondController second;
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
button = UIButton.FromType(UIButtonType.RoundedRect);
button.Frame = new RectangleF(0, 0, 100, 50);
button.SetTitle("Test", UIControlState.Normal);
button.TouchUpInside += PresentFirstController;
View.Add(button);
PresentFirstController(null, null);
}
void PresentFirstController (object sender, EventArgs e)
{
bool firstFinished = false;
first = new FirstController();
first.OnFirstFinished += delegate(object s, EventArgs args) {
firstFinished = true;
};
this.PresentModalViewController(first, true);
do
{
NSRunLoop.Current.RunUntil (NSDate.FromTimeIntervalSinceNow (0.5));
} while (!firstFinished);
second = new SecondController();
this.PresentModalViewController(second, true);
}
// ...
}
In FirstController:
...
public override void DismissModalViewControllerAnimated (bool animated)
{
base.DismissModalViewControllerAnimated (animated);
if(null != OnFirstFinished)
{
OnFirstFinished(this, null);
}
}
In this setup the execution blocks (black loading screen) and the first controller isn't loaded.
If the call to PresentFirstController() is removed from ViewDidLoad(), the main controller loads fine and when clicking the "Test" button the first controller is loaded. However, after the first controller is dismissed, the second controller is NOT loaded - iOS doesn't seem to like presenting a modal controller right after dismissing another one. This can be solved by adding a small delay (but how small is still safe?) like below:
public override void DismissModalViewControllerAnimated (bool animated)
{
base.DismissModalViewControllerAnimated (animated);
NSRunLoop.Current.RunUntil (NSDate.FromTimeIntervalSinceNow(0.2));
if(null != OnFirstFinished)
{
OnFirstFinished(this, null);
}
}
In general, the code pattern that you want to use on iOS is to chain these actions. For example, your view controller would likely have a login/password field, and a button to do the login.
What you would do is connect an action to the Login button that contacts the server, validates the user and if the credentials are OK, dismiss the dialog view controller and at that point resume execution.
That said, you could present the view controller and run the UI main loop manually and wait for some event to trigger before resuming execution.
do {
NSRunLoop.Current.RunUntil (NSDate.FromTimeIntervalSinceNow (0.5));
} while (!done);
You can save yourself some pain and structure your code with the UIKit patterns instead of trying to fight them. You will end up saving time.

BlackBerry java detecting screen foreground event

In my BlackBerry application, I have a home screen. The user can then navigate to a settings screen. When the user goes back to the home screen, is there no method that is called on the home screen indicating that the screen has come to the foreground?
I have tried onFocus() with no avail.
Thanks!
Unfortunately, hooking on the onExposed is not enough. I found that in Blackberry dialogs are also screens and even context menus are screens too. They are pushed on top of your screen so you receive onExposed callback when they are dismissed.
Though it's OK in many cases, in other cases it poses a problem - e.g. if I must refresh the screen's content only when the user returns to it, but not after menus/dialogs, then how do I do that? My case is, unfortunately, one of those.
I found no documented way of detecting "covered"/"uncovered" events. Here is my approach. onCovered/onUncovered callbacks are called when the current screen is covered/uncovered by another screen of the app, but not by dialogs/menus/virtual keyboard:
public class MyAppScreen extends MainScreen {
private boolean isCovered;
protected void onExposed() {
Log.d("onExposed");
super.onExposed();
if (isCovered) {
onUncovered();
isCovered = false;
}
}
protected void onObscured() {
Log.d("onObscured");
super.onObscured();
final Screen above = getScreenAbove();
if (above != null) {
if (isMyAppScreen(above)) {
isCovered = true;
onCovered();
}
}
}
private boolean isMyAppScreen(final Screen above) {
return (above instanceof MyAppScreen);
}
protected void onUncovered() {
Log.d("onUncovered");
}
protected void onCovered() {
Log.d("onCovered");
}
protected void onUiEngineAttached(final boolean attached) {
if (attached) {
Log.d("UI Engine ATTACHED");
} else {
Log.d("UI Engine DETACHED");
}
super.onUiEngineAttached(attached);
}
protected void onFocusNotify(final boolean focus) {
if(focus){
Log.d("focus GAINED");
} else {
Log.d("focus LOST");
}
super.onFocusNotify(focus);
}
}
And a test. Try various combinations and see what events you receive in the log.
public class TestLifecycle extends MyAppScreen implements FieldChangeListener {
private final ABNTextEdit txt1;
private final ButtonField btn1;
private final ButtonField btn2;
public TestLifecycle() {
final Manager manager = getMainManager();
txt1 = new ABNTextEdit();
manager.add(txt1);
btn1 = new ButtonField("Dialog", ButtonField.CONSUME_CLICK);
btn1.setChangeListener(this);
manager.add(btn1);
btn2 = new ButtonField("Screen", ButtonField.CONSUME_CLICK);
btn2.setChangeListener(this);
manager.add(btn2);
}
public void fieldChanged(final Field field, final int context) {
if (field == btn1) {
Dialog.alert("Example alert");
} else if (field == btn2) {
UiApplication.getUiApplication().pushScreen(new TestLifecycle());
}
}
}
Update:
This method has a limitation: if a new screen is pushed when a dialog or the soft keyboard has focus your current screen will not receive onCovered/onUncovered notification.
Example A: if you have an input field of fixed size and you push a new screen when the user completes it, your current screen will not receive the notification if the user types very quickly. This happens because in the moment between you call push(newScreen) and it is actually pushed the user clicks on a letter on soft KB and it grabs the focus. So only onObscured is called, but not onCovered.
Solution: explicitly hide the soft keyboard before the push(newScreen).
Example B: if you have a customized dialog which pushes new screen and then dismisses itself, your current screen will not receive the notification. This happens because your customized dialog is not recognized as a screen, so only onObscured is called, but not onCovered.
Solution: dismiss the dialog in the first place returning a result value, and let your screen push the new screen based on that value. -OR- override isMyAppScreen() to return true also for your customized dialog.
You should be able to use protected void onExposed() to detect when it is displayed again.

Resources