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.
Related
My RootViewController is a UINavigationController. The navigation stack starts with ViewController which then pushes CustomViewController.
public class ViewController : UIViewController
{
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
View.BackgroundColor = UIColor.Red;
NavigationController.PushViewController(new CustomViewController(), true);
}
}
public class CustomViewController : UIViewController
{
public override void ViewDidLoad()
{
base.ViewDidLoad();
View.BackgroundColor = UIColor.DarkGray;
var button = new UIButton
{
TranslatesAutoresizingMaskIntoConstraints = false
};
View.AddSubview(button);
button.SetTitle("Click me", UIControlState.Normal);
button.WidthAnchor.ConstraintEqualTo(250).Active = true;
button.HeightAnchor.ConstraintEqualTo(50).Active = true;
button.CenterXAnchor.ConstraintEqualTo(View.CenterXAnchor).Active = true;
button.CenterYAnchor.ConstraintEqualTo(View.CenterYAnchor).Active = true;
button.AddTarget(async (sender, e) =>
{
await Task.Delay(3000);
NavigationController.PushViewController(new CustomViewController(), true);
}, UIControlEvent.TouchUpInside);
}
}
The problem I'm having is when the user clicks the button and pops its containing view controller immediately afterwards, the async event handler keeps running. After 3 seconds, it will attempt to push the same view controller, but NavigationController is set to null.
When a page is dismissed / popped off the stack, is there a way to cancel all running async methods? Note that my actual use case involves more complex interactions. I tried using a CancellationToken (ThrowIfCancellationRequested()) which helps with REST calls, but it is still possible that the user might go back to the previous page after the API call is done and before the async handler finishes.
I'm trying to find a generic solution to this because my app will eventually have dozens of async event handlers.
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);
}
}
In my Windows 10 UWP app I use the MainPage for a navigation hamburger menu. All other pages are loaded into a Frame on MainPage called MyFrame. With the following code, back navigation within this frame works perfectly:
private void MainPage_BackRequested(object sender, BackRequestedEventArgs e)
{
if (MyFrame.CanGoBack)
{
MyFrame.GoBack();
e.Handled = true;
}
}
However one of those pages contains a WebView. When this page is loaded, i want the webview instead of the frame to perform the GoBack Event.
Is it somehow possible to access the WebView's CanGoBack property from MainPage?
edit: maybe something like this works?
private void MainPage_BackRequested(object sender, BackRequestedEventArgs e)
{
if (MyFrame.Content == typeof(Web))
{
get state of the Webview "SearchURI" here and handle the GoBack event
}
else if (MyFrame.CanGoBack)
{
MyFrame.GoBack();
e.Handled = true;
}
}
WebView already has CanGoBack Property and GoBack Method for you to use. Using these methods, you can use BackButton and control your backkeyprocess.
Below is how I would do it.
In your Web.xaml.cs Declare a public variable
public WebView myWebView { get; private set; }
and in your page constructor, assign the actual WebView reference to this variable.
public Web()
{
this.InitializeComponent();
myWebView = this.webView;
}
Now in you MainPage.xaml.cs, The button where you use to navigate, Subscribe to BackRequested
SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = AppViewBackButtonVisibility.Visible;
SystemNavigationManager.GetForCurrentView().BackRequested += OnBackRequested;
And now your OnnackRequested will show something like below.
private void OnBackRequested(object sender, BackRequestedEventArgs e)
{
if (MyFrame.Content.GetType() == typeof(Web))
{
Web web = MyFrame.Content as Web;
if (web.myWebView.CanGoBack)
{
web.myWebView.GoBack();
}
else
{
if (MyFrame.CanGoBack)
{
e.Handled = true;
MyFrame.GoBack();
}
}
}
}
Unless the webview navigates back to your initial page, you will not be able to go back to the previous page that is loaded in your MyFrame
Good Luck.
I have used IMvXModelTouchView for a custom popup screen animation. And, I have a close button on this popup view. What is the proper way to switch back to a previous view?
Here is my code look like:
public class PopupView
: MvxViewController, IMvxModalTouchView
{
public PopupView()
{
ModalPresentationStyle = UIModalPresentationStyle.PageSheet;
}
public override void ViewDidLoad()
{
Title = "Map";
base.ViewDidLoad();
var closeButton = new UIButton(new RectangleF(0, 0, 50, 30));
closeButton.TouchUpInside += CloseButtonClicked();
Add(closeButton);
}
private EventHandler CloseButtonClicked()
{
return (sender, args) => NavigationController.DismissViewController(true, null);
}
}
It worked, the first time when I click this close button, but it crash when I tried to pop up this view again.
I suspect you are probably using the standard MvxModalSupportTouchViewPresenter which only expects one modal view/viewModel to be displayed at a time, and which expects that view/viewModel to be cleared using Close(this) from the ViewModel.
See: MvxModalSupportTouchViewPresenter.cs#L29
If you have strong ideas about your UI, then (in my opinion) your best bet is to write your own custom presenter - then you can open/close/hide/show whatever you want. For more on writing custom presenters, see some of the links and videos from: http://slodge.blogspot.com/2013/06/presenter-roundup.html
I'm trying to get a basic flip animation transition working when I push a controller inside a navigation. The code below flips the view, however the view appears first (each element fades in), and then the flip occurs. Is it possible to do a flip animation with a UINavigationController?
Any pointers would be great, the examples I've found for Monotouch are performing animations on Views inside another view.
void ToolbarButtonClick()
{
InformationController controller = new InformationController();
NavigationController.PushViewController(controller,true);
}
public class InformationController : UIViewController
{
public override void ViewDidLoad ()
{
UIView.BeginAnimations("Flip");
UIView.SetAnimationDuration(1.0);
UIView.SetAnimationTransition(UIViewAnimationTransition.FlipFromRight,View,true);
base.ViewDidLoad ();
Title = "Information";
}
public override void ViewWillAppear (bool animated)
{
base.ViewWillAppear (animated);
}
public override void ViewDidAppear (bool animated)
{
base.ViewDidAppear (animated);
UIView.CommitAnimations();
}
}
I was sort of there, but the View needs to be taken from the NavigationController:
// Push the controller first or the Title doesn't animate
NavigationController.PushViewController(controller,false);
UIView.BeginAnimations(null,IntPtr.Zero);
UIView.SetAnimationDuration(1);
UIView.SetAnimationTransition(UIViewAnimationTransition.FlipFromLeft,
NavigationController.View,true);
UIView.CommitAnimations();
I'm not an expert, but I'm wondering about that TRUE on the PushViewcontroller which indicates that it is going to be animated. It has me wondering if that is making the NavigationController do the initial amount of animation work, which is then followed by yours. When you set it to false, what happens? I know we tend to automatically put the TRUE in there without thinking.