Need event when UIWebView scrolls and option to cancel the scroll - xamarin

We are building an app using xamarin.forms, which has a certain page that displays a webview. We use custom renderers for the webview to enable some features that are not implemented in the xamarin.forms webview. In the IOS version of the app, this webview is an UIWebView control. What I need, is a way to catch touch start, move and end events on the UIWebView control and be able to calculate the distance that the touch has moved. Furthermore, I need to be able to cancel the touchmove event from firing in the UIWebView, so the content doesn't scroll until I want it to.
I've already accomplished this in the android version by using the Touch eventhandler of the android webview. It can do exactly what i want, and the touch event has a "handled" property that can be used to avoid the event from triggering in the webview.
I've found all kinds ways to catch scroll events in IOS. The most promising seems to be implementing a gestureRecognizer to the UIWebView's scrollview. This way I can catch touch start, move and end events. However, I have a few problems with this implementation:
During touchmove, the events seem to stop firing. The touchend doesn't fire at all most of the time.
I cannot find a way to cancel the event when I want it to be canceled.
The way i currently attach the gesture recognizer:
var test = new MyRecognizer();
webView.ScrollView.AddGestureRecognizer(test);
The gesture recognizer:
public class MyRecognizer : UIGestureRecognizer
{
private nfloat _startY;
public MyRecognizer()
{
this.CancelsTouchesInView = true;
}
public override void TouchesBegan(NSSet touches, UIEvent evt)
{
base.TouchesBegan(touches, evt);
var location = (touches.AnyObject as UITouch).LocationInView(View);
_startY = location.Y;
}
public override void TouchesMoved(NSSet touches, UIEvent evt)
{
base.TouchesMoved(touches, evt);
var location = (touches.AnyObject as UITouch).LocationInView(View);
var intCurrentY = location.Y;
if(someCondition)
{
//cancel the event somehow to prevent the webview from scrolling
//a post mentioned this should do the trick, but it doesnt work:
this.Enabled = false;
this.Enabled = true;
}
}
public override void TouchesEnded(NSSet touches, UIEvent evt)
{
base.TouchesEnded(touches, evt);
}
}
Am I on the right track here? Why do the events stop firing? And how can I cancel the event to prevent the webview from scrolling in SOME cases (I don't want to ALWAYS disable scrolling on webview)

I figured it out myself. At least part of it. The events stopped firing because during the move, the default UIPanGestureRecognizer of the UIWebView's ScrollView captured the gesture. This is the code of my own recognizer that does the trick in capturing the events, and doesn't stop after a while:
public class TopBottomBarScrollRecognizer : UIPanGestureRecognizer
{
private nfloat _startY;
private nfloat _startX;
private HybridWebView _webView;
private UIWebView _nativeWebView;
public TopBottomBarScrollRecognizer()
{
this.DelaysTouchesBegan = false;
this.DelaysTouchesEnded = false;
this.CancelsTouchesInView = false;
//make sure the recognizer can work together with other recognizers
this.ShouldRecognizeSimultaneously = (a, b) => true;
}
public TopBottomBarScrollRecognizer(HybridWebView webView, UIWebView nativeWebView) : this()
{
_webView = webView;
_nativeWebView = nativeWebView;
}
public override void TouchesBegan(NSSet touches, UIEvent evt)
{
base.TouchesBegan(touches, evt);
var location = (touches.AnyObject as UITouch).LocationInView(_nativeWebView);
_startY = location.Y;
_startX = location.X;
}
public override void TouchesMoved(NSSet touches, UIEvent evt)
{
base.TouchesMoved(touches, evt);
var location = (touches.AnyObject as UITouch).LocationInView(_nativeWebView);
var deltaY = (float)(double)(location.Y - _startY);
var deltaX = (float)(double)(location.X - _startX);
_webView.scrollOccurred(deltaX, deltaY);
}
public override void TouchesEnded(NSSet touches, UIEvent evt)
{
base.TouchesEnded(touches, evt);
_webView.scrollEndOccurred();
}
}
The above solves part 1 of the question. The question on how to prevent the default recognizer to temporarily stop is not that important anymore, since the behavior as it is now seems to work fine.

Related

How to handle UITextField position while typing in Xamarin iOS?

Its very common problem in iOS mobile development and that is while you are done with your UI and It contains too many UITextFields, If you try to input value in UITextFields those are added center bottom of the screen; these fields hides behind the keyboard. How can we get rid of this general problem?
You could use the AddObserver method in NSNotificationCenter for when keyboard is visible and hidden.
sample code(FYI: Got the code below from another post sometime last year, I can't remember link to the post but it works fine.)
Call the AddObserver in your viewdidload method
// Keyboard popup NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.DidShowNotification, KeyBoardUpNotification);
// Keyboard Down NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillHideNotification, KeyBoardDownNotification);
you can add the methods below in your base controller base if you have one
public void KeyBoardUpNotification(NSNotification notification) {
CGRect keyboardSize = UIKeyboard.BoundsFromNotification(notification);
// Find what opened the keyboard
foreach (UIView view in this.View.Subviews) {
if (view.IsFirstResponder)
activeview = view;
}
bottom = (activeview.Frame.Y + activeview.Frame.Height + offset);
scrollamount = (keyboardSize.Height - (View.Frame.Size.Height - bottom));
if (scrollamount > 0) {
moveViewUp = true;
MoveView(moveViewUp);
} else {
moveViewUp = false;
}
}
public void KeyBoardDownNotification(NSNotification notification) {
if (moveViewUp) {
MoveView(false);
}
}
private void MoveView(bool move) {
UIView.BeginAnimations(string.Empty, IntPtr.Zero);
UIView.SetAnimationDuration(0.3);
CGRect frame = View.Frame;
if (move) {
frame.Y -= scrollamount;
} else {
frame.Y += scrollamount;
scrollamount = 0;
}
View.Frame = frame;
UIView.CommitAnimations();
}
I have used a nuget package to get rid of this problem. I have overrides two methods and initialized code inside these methods.
Download KeyboardHandler and use as following:
using KeyboardHandler;
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
this.yourScrollView.SubscribeKeyboardManaqger();
}
public override void ViewWillDisappear(bool animated)
{
base.ViewWillDisappear(animated);
this.yourScrollView.UnsubscribeKeyboardManaqger();
}

How to detect resize in NSView in Xamarin.Mac?

What is the correct way to detect when a NSView is resized ?.
I do not see any resize event available on the view or any delegate for the view.
I have added this hack, where I use the drawRect to detect the change in size, but I'm sure there must be a more correct way to do this.
CGRect m_resizeRect = CGRect.Empty;
public override void DrawRect(CGRect dirtyRect)
{
base.DrawRect(dirtyRect);
if (this.InLiveResize) {
if (m_resizeRect.Size != this.Bounds.Size) {
m_resizeRect = this.Bounds;
this.OnResize();
}
}
}
public override void ViewWillStartLiveResize()
{
m_resizeRect = this.Bounds;
base.ViewWillStartLiveResize();
}
public override void ViewDidEndLiveResize()
{
m_resizeRect = CGRect.Empty;
base.ViewDidEndLiveResize();
}
protected void OnResize() {
Console.WriteLine("OnResize " + this.Bounds.ToString() );
}
You can subscribe to the resize notifications.
Add observer to default notification center:
NSObject NSWindowDidResizeNotificationObject;
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
NSWindowDidResizeNotificationObject = NSNotificationCenter.DefaultCenter.AddObserver (new NSString ("NSWindowDidResizeNotification"), ResizeObserver, null);
}
NSNotification Action:
public void ResizeObserver (NSNotification notify)
{
var r = this.View.Frame;
Console.WriteLine ("{0}:{1}:{1}", notify.Name, r.Height, r.Width);
}
Remove observer (and release memory):
NSNotificationCenter.DefaultCenter.RemoveObserver (NSWindowDidResizeNotificationObject);
Sample Output:
NSWindowDidResizeNotification:740:740
NSWindowDidResizeNotification:715:715
NSWindowDidResizeNotification:681:681
NSWindowDidResizeNotification:642:642
You can override the setFrameSize method and do your own stuff every time the frame is updated.
class MyView: NSView {
...
override func setFrameSize(newSize: NSSize) {
super.setFrameSize(newSize)
Swift.print("new size is \(frame)")
}
...
}
The accepted answer only seems to respond to window size changes and not e.g. when the split-bar of a splitview causes the resize.
See the NSView postsBoundsChangedNotifications and postFrameChangedNotifications properties. You can set those and register for those notifications.
You can use NSViewController.viewDidLayout() overriden method:
class MyViewController: NSViewController {
...
override func viewDidLayout() {
Swift.print("view has been resize to \(self.view.frame)")
}
...
}

How to handle NSTextField keyDown in monoMac

I'm trying to handle the keyDown event for NSTextField using monoMac and Xamarin Studio.
I have created a NSTextFieldDelegate derived class and set the editors delegate to an instance of this class. However, I only see the Changed method being called, never the DoCommandBySelector
class TextPathDelegate : NSTextFieldDelegate {
public override void Changed(NSNotification notification)
{
Console.WriteLine("changed");
}
public override void DidChangeValue(string forKey)
{
Console.WriteLine("didchange = {0}", forKey);
}
public override void WillChangeValue(string forKey)
{
Console.WriteLine("willchange = {0}", forKey);
}
public override bool DoCommandBySelector(NSControl control, NSTextView textView, MonoMac.ObjCRuntime.Selector commandSelector)
{
Console.WriteLine("DoCommandBySelector = {0}", commandSelector.ToString());
return false;
}
}
Any idea what I might be missing ?
Edit-2:
I just noticed that DoCommandBySelector is being called if I use the arrow keys and page up/down. Still not able to catch the keydown event
Edit-1:
I tried to make a small project with Xamarin.mac, just 2 edit fields, with a derived edit control and the delegate.
This is the code
class TextPathDelegate : NSTextFieldDelegate {
string Id { get; set; }
public TextPathDelegate(string id) { Id = id; }
public override void Changed(NSNotification notification)
{
Console.WriteLine(Id + "changed");
}
public override bool DoCommandBySelector(NSControl control, NSTextView textView, ObjCRuntime.Selector commandSelector)
{
Console.WriteLine(Id + "DoCommandBySelector = {0}", commandSelector.ToString());
return false;
}
public override void EditingBegan(NSNotification notification)
{
Console.WriteLine(Id + "EditingBegan");
}
public override void EditingEnded(NSNotification notification)
{
Console.WriteLine(Id + "EditingEnded");
}
}
[Register("MyNSTextField")]
class MyNSTextField : NSTextField {
public MyNSTextField(IntPtr handle) : base(handle){}
public override void KeyDown(NSEvent theEvent)
{
Console.WriteLine("KeyDown");
base.KeyDown(theEvent);
}
public override void KeyUp(NSEvent theEvent)
{
Console.WriteLine("KeyUp");
base.KeyUp(theEvent);
}
}
and the output. No call to keyDown or DoCommandBySelector. I must be missing something very basic
1-EditingBegan
1-changed
KeyUp
1-changed
KeyUp
1-changed
KeyUp
1-EditingEnded
2-EditingBegan
2-changed
KeyUp
2-changed
KeyUp
2-changed
KeyUp
Thanks
Text editing is in Cocoa is really different than WPF ;-) NSTextField and like controls normally do not deal with the actual editing as a NSTextView field editor is used... Strange? Depends upon on how you look at it, for Windows' style controls yes, for Cocoa it is the norm:
Field editor is an NSTextView which is maintained by NSWindow. The field editor replaces any NSTextField or NSTextFieldCell in the window for editing purposes.
Semi-required Reading material : Text Fields, Text Views, and the Field Editor
NSTextField does not respond to a KeyDown as the field editor handles it and all the related functions that it provides (word completion, accent letter popups, etc..., and thus creating a custom NSTextField and overriding KeyDown and KeyUp, only KeyUp will be called:
public override void KeyDown(NSEvent theEvent)
{
throw new Exception("NSTextField KeyDown never gets called");
}
public override void KeyUp(NSEvent theEvent)
{
Console.WriteLine("NSTextField KeyUp");
}
Thus the same goes for NSTextFieldDelegate, you will never get a commandSelector for keydown/keyup... you will get insertNewline:, cancelOperation:, deleteBackward:, etc...
public override void Changed(NSNotification notification)
{
Console.WriteLine("changed"); // after the field editor is done
}
public override bool DoCommandBySelector(NSControl control, NSTextView textView, Selector commandSelector)
{
Console.WriteLine("DoCommandBySelector = {0}", commandSelector.Name);
return false;
}
What to do?
First do you really need keydown? Altering the normal Cocoa UIX is not the norm, but if you really do, one way is to use a custom NSTextView instead of a NSTextField and the KeyDown override will be called:
[Register("CustomTextView")]
public class CustomTextView : NSTextView
{
public CustomTextView() : base() {
Console.WriteLine("CustomTextView");
}
public CustomTextView(IntPtr handle) : base(handle) {
Console.WriteLine("CustomTextView IntPtr");
}
[Export("initWithCoder:")]
public CustomTextView(NSCoder coder) : base(coder) {
Console.WriteLine("CustomTextView initWithCoder");
}
public override void KeyDown(NSEvent theEvent) {
Console.WriteLine("CustomTextView KeyDown");
}
public override void KeyUp(NSEvent theEvent) {
Console.WriteLine("CustomTextView KeyUp");
}
}
If you are using .xib for your interface design, just create a NSTextView subclass in C#, register it and the .h will show up in Xcode and use it to replace the NSTextView with your class.
Depending on what you really need keyDown for, maybe editing begin/end will work for your NSTextField:
this.EditingBegan += (object sender, EventArgs e) => {
Console.WriteLine("EditingBegan");
};
this.EditingEnded += (object sender, EventArgs e) => {
Console.WriteLine("EditingEnded");
};
You can also hook that NSWindow's field editor and listen for all keyDown:, filter the controls and only react if it is a control that you are interested in...

Xamarin forms block user back key press

In my Xamarin forms application I want to show a confirmation message when user clicks the back button from Main-page. Is there any way to achieve this?
I overrided the OnBackButtonPressed method in my MainPage. But still the app is closing while back key press. Here is my code
protected override bool OnBackButtonPressed ()
{
//return base.OnBackButtonPressed ();
return false;
}
You can override OnBackButtonPressed for any Xamarin.Form Page. But it only will work for the physical button in Android and Windows Phone devices.
protected override bool OnBackButtonPressed () {
DisplayAlert("title","message","ok");
return true;
}
For the virtual one, you will need to create CustomRenderers and to intercept the click handler. In iOS it can be tricky because the user can go back doing other actions (e.g. the swipe gesture). Once you intercept it you just need to create your Confirmation Message (which I assume that you know how to do it).
For iOS you can do something like this:
[assembly: ExportRenderer (typeof (YourPage), typeof (YourPageRenderer))]
namespace YourNamespace {
public class YourPageRenderer : PageRenderer {
public override void ViewWillAppear (bool animated) {
base.ViewWillAppear (animated);
Action goBack = () => page.DisplayAlert("title","message","ok");
var backButton = new NavBackButton (goBack);
navigationItem.LeftBarButtonItem = new UIBarButtonItem (backButton);
}
}
public class NavBackButton : UIView {
public NavBackButton (Action onButtonPressed) {
SetButton (onButtonPressed);
}
UILabel text;
UIImageView arrow;
void SetButton(Action onButtonPressed){
arrow = new UIImageView(new CGRect(-25,0, 50, 50)) {
Image = new UIImage("Images/back").ImageWithRenderingMode(UIImageRenderingMode.AlwaysTemplate)
};
arrow.TintColor = Colors.DarkGreen.ToUIColor ();
text = new UILabel(new CGRect(arrow.Frame.Width + arrow.Frame.X -15, arrow.Frame.Height /2 - 10, 40, 20)) { Text = "Back" };
text.TextColor = Colors.DarkGreen.ToUIColor ();
Frame = new CGRect(0,0,text.Frame.Size.Width + arrow.Frame.Width, arrow.Frame.Height);
AddSubviews (new UIView[] { arrow, text });
var tapGesture = new UITapGestureRecognizer (onButtonPressed);
AddGestureRecognizer (tapGesture);
}
public override void TouchesBegan (Foundation.NSSet touches, UIEvent evt) {
base.TouchesBegan (touches, evt);
text.TextColor = UIColor.YourColor;
arrow.TintColor = UIColor.YourColor;
}
public override void TouchesEnded (Foundation.NSSet touches, UIEvent evt){
base.TouchesEnded (touches, evt);
arrow.TintColor = UIColor.YourColor;
text.TextColor = UIColor.YourColor;
}
}
}
PS You will need to provide an arrow image ("Images/back")

How to handle/cancel back navigation in Xamarin Forms

I tried to use the back navigation by overriding OnBackButtonPressed, but somehow it wasn't get called at all. I am using the ContentPage and the latest 1.4.2 release.
Alright, after many hours I figured this one out. There are three parts to it.
#1 Handling the hardware back button on android. This one is easy, override OnBackButtonPressed. Remember, this is for a hardware back button and android only. It will not handle the navigation bar back button. As you can see, I was trying to back through a browser before backing out of the page, but you can put whatever logic you need in.
protected override bool OnBackButtonPressed()
{
if (_browser.CanGoBack)
{
_browser.GoBack();
return true;
}
else
{
//await Navigation.PopAsync(true);
base.OnBackButtonPressed();
return true;
}
}
#2 iOS navigation back button. This one was really tricky, if you look around the web you'll find a couple examples of replacing the back button with a new custom button, but it's almost impossible to get it to look like your other pages. In this case I made a transparent button that sits on top of the normal button.
[assembly: ExportRenderer(typeof(MyAdvantagePage), typeof
(MyAdvantagePageRenderer))]
namespace Advantage.MyAdvantage.MobileApp.iOS.Renderers
{
public class MyAdvantagePageRenderer : Xamarin.Forms.Platform.iOS.PageRenderer
{
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
if (((MyAdvantagePage)Element).EnableBackButtonOverride)
{
SetCustomBackButton();
}
}
private void SetCustomBackButton()
{
UIButton btn = new UIButton();
btn.Frame = new CGRect(0, 0, 50, 40);
btn.BackgroundColor = UIColor.Clear;
btn.TouchDown += (sender, e) =>
{
// Whatever your custom back button click handling
if (((MyAdvantagePage)Element)?.
CustomBackButtonAction != null)
{
((MyAdvantagePage)Element)?.
CustomBackButtonAction.Invoke();
}
};
NavigationController.NavigationBar.AddSubview(btn);
}
}
}
Android, is tricky. In older versions and future versions of Forms once fixed, you can simply override the OnOptionsItemselected like this
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 = (MyAdvantagePage)
Xamarin.Forms.Application.
Current.MainPage.Navigation.
NavigationStack.LastOrDefault();
// check if the page has subscribed to
// the custom back button event
if (currentpage?.CustomBackButtonAction != null)
{
// invoke the Custom back button action
currentpage?.CustomBackButtonAction.Invoke();
// and disable the default back button action
return false;
}
// if its not subscribed then go ahead
// with the default back button action
return base.OnOptionsItemSelected(item);
}
else
{
// since its not the back button
//click, pass the event to the base
return base.OnOptionsItemSelected(item);
}
}
However, if you are using FormsAppCompatActivity, then you need to add onto your OnCreate in MainActivity this to set your toolbar:
Android.Support.V7.Widget.Toolbar toolbar = this.FindViewById<Android.Support.V7.Widget.Toolbar>(Resource.Id.toolbar);
SetSupportActionBar(toolbar);
But wait! If you have too old a version of .Forms or too new version, a bug will come up where toolbar is null. If this happens, the hacked together way I got it to work to make a deadline is like this. In OnCreate in MainActivity:
MobileApp.Pages.Articles.ArticleDetail.androdAction = () =>
{
Android.Support.V7.Widget.Toolbar toolbar = this.FindViewById<Android.Support.V7.Widget.Toolbar>(Resource.Id.toolbar);
SetSupportActionBar(toolbar);
};
ArticleDetail is a Page, and androidAction is an Action that I run on OnAppearing if the Platform is Android on my page. By this point in your app, toolbar will no longer be null.
Couple more steps, the iOS render we made above uses properties that you need to add to whatever page you are making the renderer for. I was making it for my MyAdvantagePage class that I made, which implements ContentPage . So in my MyAdvantagePage class I added
public Action CustomBackButtonAction { get; set; }
public static readonly BindableProperty EnableBackButtonOverrideProperty =
BindableProperty.Create(
nameof(EnableBackButtonOverride),
typeof(bool),
typeof(MyAdvantagePage),
false);
/// <summary>
/// Gets or Sets Custom Back button overriding state
/// </summary>
public bool EnableBackButtonOverride
{
get
{
return (bool)GetValue(EnableBackButtonOverrideProperty);
}
set
{
SetValue(EnableBackButtonOverrideProperty, value);
}
}
Now that that is all done, on any of my MyAdvantagePage I can add this
:
this.EnableBackButtonOverride = true;
this.CustomBackButtonAction = async () =>
{
if (_browser.CanGoBack)
{
_browser.GoBack();
}
else
{
await Navigation.PopAsync(true);
}
};
That should be everything to get it to work on Android hardware back, and navigation back for both android and iOS.
You are right, in your page class override OnBackButtonPressed and return true if you want to prevent navigation. It works fine for me and I have the same version.
protected override bool OnBackButtonPressed()
{
if (Condition)
return true;
return base.OnBackButtonPressed();
}
Depending on what exactly you are looking for (I would not recommend using this if you simply want to cancel back button navigation), OnDisappearing may be another option:
protected override void OnDisappearing()
{
//back button logic here
}
OnBackButtonPressed() this will be called when a hardware back button is pressed as in android. This will not work on the software back button press as in ios.
Additional to Kyle Answer
Set
Inside YOURPAGE
public static Action SetToolbar;
YOURPAGE OnAppearing
if (Device.RuntimePlatform == Device.Android)
{
SetToolbar.Invoke();
}
MainActivity
YOURPAGE.SetToolbar = () =>
{
Android.Support.V7.Widget.Toolbar toolbar =
this.FindViewById<Android.Support.V7.Widget.Toolbar>(Resource.Id.toolbar);
SetSupportActionBar(toolbar);
};
I use Prism libray and for handle the back button/action I extend INavigatedAware interface of Prism on my page and I implement this methods:
public void OnNavigatedFrom(INavigationParameters parameters)
{
if (parameters.GetNavigationMode() == NavigationMode.Back)
{
//Your code
}
}
public void OnNavigatedTo(INavigationParameters parameters)
{
}
Method OnNavigatedFrom is raised when user press back button from Navigation Bar (Android & iOS) and when user press Hardware back button (only for Android).
For anyone still fighting with this issue - basically you cannot intercept back navigation cross-platform. Having said that there are two approaches that effectively solve the problem:
Hide the NavigationPage back button with NavigationPage.ShowHasBackButton(this, false) and push a modal page that has a custom Back/Cancel/Close button
Intercept the back navigation natively for each platform. This is a good article that does it for iOS and Android: https://theconfuzedsourcecode.wordpress.com/2017/03/12/lets-override-navigation-bar-back-button-click-in-xamarin-forms/
For UWP you are on your own :)
Edit:
Well, not anymore since I did it :) 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);
}
}
protected override bool OnBackButtonPressed()
{
base.OnBackButtonPressed();
return true;
}
base.OnBackButtonPressed() returns false on click of hardware back button.
In order to prevent operation of back button or prevent navigation to previous page. the overriding function should be returned as true. On return true, it stays on the current xamarin form page and state of page is also maintained.
The trick is to implement your own navigation page that inherits from NavigationPage. It has the appropriate events Pushed, Popped and PoppedToRoot.
A sample implementation could look like this:
public class PageLifetimeSupportingNavigationPage : NavigationPage
{
public PageLifetimeSupportingNavigationPage(Page content)
: base(content)
{
Init();
}
private void Init()
{
Pushed += (sender, e) => OpenPage(e.Page);
Popped += (sender, e) => ClosePage(e.Page);
PoppedToRoot += (sender, e) =>
{
var args = e as PoppedToRootEventArgs;
if (args == null)
return;
foreach (var page in args.PoppedPages.Reverse())
ClosePage(page);
};
}
private static void OpenPage(Page page)
{
if (page is IPageLifetime navpage)
navpage.OnOpening();
}
private static void ClosePage(Page page)
{
if (page is IPageLifetime navpage)
navpage.OnClosed();
page.BindingContext = null;
}
}
Pages would implement the following interface:
public interface IPageLifetime
{
void OnOpening();
void OnClosed();
}
This interface could be implemented in a base class for all pages and then delegate it's calls to it's view model.
The navigation page and could be created like this:
var navigationPage = new PageLifetimeSupportingNavigationPage(new MainPage());
MainPage would be the root page to show.
Of course you could also just use NavigationPage in the first place and subscribe to it's events without inheriting from it.
Maybe this can be usefull, You need to hide the back button, and then replace with your own button:
public static UIViewController AddBackButton(this UIViewController controller, EventHandler ev){
controller.NavigationItem.HidesBackButton = true;
var btn = new UIBarButtonItem(UIImage.FromFile("myIcon.png"), UIBarButtonItemStyle.Plain, ev);
UIBarButtonItem[] items = new[] { btn };
controller.NavigationItem.LeftBarButtonItems = items;
return controller;
}
public static UIViewController DeleteBack(this UIViewController controller)
{
controller.NavigationItem.LeftBarButtonItems = null;
return controller;
}
Then call them into these methods:
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
this.AddBackButton(DoSomething);
UpdateFrames();
}
public override void ViewWillDisappear(Boolean animated)
{
this.DeleteBackButton();
}
public void DoSomething(object sender, EventArgs e)
{
//Do a barrel roll
}
Another way around is to use Rg.Plugins.Popup Which allows you to implement nice popup. It uses another NavigationStack => Rg.Plugins.Popup.Services.PopupNavigation.Instance.PopupStack. So your page won't be wrap around the NavigationBar.
In your case I would simply
Create a full page popup with opaque background
Override ↩️ OnBackButtonPressed for Android on ⚠️ParentPage⚠️ with something like this:
protected override bool OnBackButtonPressed()
{
return Rg.Plugins.Popup.Services.PopupNavigation.Instance.PopupStack.Any();
}
Since the back-button affect the usual NavigationStack your parent would pop out whenever the user try to use it while your "popup is showing".
Now what? Xaml what ever you want to properly close your popup with all the check you want.
💥 Problem solved for these targets💥
[x] Android
[x] iOS
[-] Windows Phone (Obsolete. Use v1.1.0-pre5 if WP is needed)
[x] UWP (Min Target: 10.0.16299)

Resources