I use live title to launch a specific page:
Live tile in Home screen --launch--> P1 after a task and goto --> P2 --> MainPage
When you click back button in MainPage the app won't exit instead it goes to P2 in a loop fashion.
Here is the code:
try
{
ShellTile TileToFind = ShellTile.ActiveTiles.FirstOrDefault(x => x.NavigationUri.ToString().Contains("PageTakePic.xaml"));
if (TileToFind == null)
{
StandardTileData NewTileData = new StandardTileData
{
//BackgroundImage = new Uri("Red.jpg", UriKind.Relative),
//--front tile
Title = "Take Pic",
//Count = 12,
BackTitle = "Quick Access",
//--40 char
BackContent = "Take Pic",
//BackBackgroundImage = new Uri("Blue.jpg", UriKind.Relative)
};
// Create the Tile and pin it to Start. This will cause a navigation to Start and a deactivation of our application.
ShellTile.Create(new Uri("/PageTakePic.xaml", UriKind.Relative), NewTileData);
}
else
{
MessageBox.Show("A live title created for this service already.");
}
}
catch (Exception ex)
{
MessageBox.Show("Try again. Error encountered: " + ex.Message);
NavigationService.Navigate(new Uri("/MainPage.xaml", UriKind.Relative));
}
Update:
Live tile in Home screen --launch--> P1 (PageTakePic.xaml) --> P2 --> MainPage
using below method not working In MainPage. It still goes into a loop:
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
base.OnNavigatedTo(e);
NavigationService.RemoveBackEntry();
}
It's a common issue to consider with Deep Links from Live Tiles in Mango. What you need to do is remove the BackStack entries when you hit your Main Page so that hitting the back button will exit the app.
Here is a snippet that may help:
void ClearBackStack()
{
while (NavigationService.CanGoBack)
{
NavigationService.RemoveBackEntry();
}
}
If you handle errors by navigating to home page like in your example:
NavigationService.Navigate(new Uri("/MainPage.xaml", UriKind.Relative));
Then it is possible that at some point when you navigate back - you get an error and navigate to MainPage instead, so your live tile gets you to PageTakePic, then when you press back - navigation fails and you navigate forward instead to MainPage. Then the back button gets you back to PageTakePic and so on...
Related
I have a modal page that contains an absolute layout space in which I insert my widgets, in particular a scrollable list widget, which is a custom class derived from AbsoluteLayout:
public ListWidget(List<ListItem> items)
{
list_items = items;
BackgroundColor = Color.Transparent;
tap = new TapGestureRecognizer();
tap.Tapped += Tap_Handler;
list = new ListView()
{
BackgroundColor = Color.Transparent,
HasUnevenRows = true,
VerticalOptions = LayoutOptions.Center
};
list.ItemTapped += Tap_Handler;
list.ItemTemplate = new DataTemplate(typeof(CustomCell));
list.ItemsSource = list_items;
SetLayoutBounds(list, new Rectangle(0.0, 0.0, 1.0, 1.0));
SetLayoutFlags(list, AbsoluteLayoutFlags.All);
Children.Add(list);
}
Nothing fancy, as you see. Now, the page (a ContentPage) in question is loaded by a PushModalAsync() call, but here comes the problem:
If I swipe down on the list, the modal page gets hidden and I cannot get it back, since when I try I get this warning:
Warning: Attempt to present <Xamarin_Forms_Platform_iOS_ModalWrapper: 0x7f970601c580> on <Xamarin_Forms_Platform_iOS_PageRenderer: 0x7f9703f54b50> whose view is not in the window hierarchy!
Apparently, no PopModalAsync() is called in this case.
I have tried overriding this by using a gesture recognizer:
// in the constructor...
space.GestureRecognizers.Add(swipeDown);
listwidget.GestureRecognizers.Add(swipeDown);
}
// where my hopes crushed
void Swiper_Handler(object sender, SwipedEventArgs e)
{
System.Diagnostics.Debug.Print("SWIPED sender {0} args {1}", sender, e);
if (sender == listwidget)
{
System.Diagnostics.Debug.Print("LIST WIDGET! ABORT!");
return;
}
switch (e.Direction)
{
case SwipeDirection.Left:
System.Diagnostics.Debug.Print("Swiped to LEFT");
break;
case SwipeDirection.Right:
System.Diagnostics.Debug.Print("Swiped to RIGHT");
break;
case SwipeDirection.Up:
System.Diagnostics.Debug.Print("Swiped to UP");
break;
case SwipeDirection.Down:
System.Diagnostics.Debug.Print("Swiped to DOWN");
Navigation.PopModalAsync();
break;
}
}
But it won't work:
enabling swipe down on space will pop the page, but I cannot scroll the list
enabling swipe down on listwidget will hide the page, I cannot scroll the list, and I get the above error
removing gestures will hide the page, I cannot scroll the list, and I get the above error
enabling swipe on both space and listwidget will pop the page, but I cannot scroll the list
How can I make a scrollable list with a modal page coexist?
Any hints are greatly appreciated.
I have a custom WkWebView where I set the height of the view to the size of the html content.
This works great when I initialize it, but the problem comes when I change the source of the WkWebView to a shorter html.
I've already encountered this problem in the android version of my app and I fixed that by setting the HeightRequest to 0 before EvaluateJavaScriptAsync. In that way the view will always get bigger.
I tried the same thing with iOS but it keeps the highest content I had .
So for example :
If I set the source of the WkWebView to a html file that is 200px height and change it to a html file that is 1000px, it works fine and I can see all the content. BUT, if I try to go back to my 200px html file, I get a 800px blank space underneath and keep the 1000px height.
The goal is to always have the height of the WKWebView to adapt to the height of its content.
Here is the current version of the custom render
namespace MyNamespace.iOS.CustomControl
{
public class AutoHeightWebViewRenderer : WkWebViewRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
try
{
base.OnElementChanged(e);
if (NativeView != null)
{
var webView = (WKWebView)NativeView;
NavigationDelegate = new ExtendedWKWebViewDelegate(this);
}
}
catch (Exception ex)
{
//Log the Exception
}
}
}
class ExtendedWKWebViewDelegate : WKNavigationDelegate
{
AutoHeightWebViewRenderer webViewRenderer;
public ExtendedWKWebViewDelegate(AutoHeightWebViewRenderer _webViewRenderer = null)
{
webViewRenderer = _webViewRenderer ?? new AutoHeightWebViewRenderer();
}
public override async void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
try
{
var _webView = webViewRenderer.Element as AutoHeightWebView;
if (_webView != null)
{
_webView.HeightRequest = 0d;
await Task.Delay(300);
var result = await _webView.EvaluateJavaScriptAsync("(function(){return document.body.scrollHeight;})()");
_webView.HeightRequest = Convert.ToDouble(result);
}
}
catch (Exception ex)
{
//Log the Exception
}
}
}
}
EDIT 1 :
To be clearer, I'm able to change the height of the webview, but I dont know the height because it always returned the height of the largest html displayed so far. Nomatter if I use _webView.EvaluateJavaScriptAsync("(function(){return document.body.scrollHeight;})()") or webView.ScrollView.ContentSize.Height.
EDIT 2 :
Here a little sample to help understand the problem. I got two buttons and my custom webview (initialize with a 40 HeightRequest empty html).
The first button set the Source of the webview to a 70px long HTML. The second one set the Source to a 280px long HTML.
In this example, I click on the first button than the second one and finaly back on the first button again. You see the webview getting bigger on the first 2 clicks. But then then webview should get shrunk when I choose back the first html (passing from 280px to 70px) but it keeps the 280px long.
First button (70px long)
Second button (280px long)
Back to the first button (should be 70px long instead of 280px).
The problem occured on both simulator and iOS device.
You can change the Frame of webView to change the height.
public override async void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
try
{
var _webView = webViewRenderer.Element as WebView;
if (_webView != null)
{
webView.Frame = new CGRect(webView.Frame.Location, new CGSize(webView.Frame.Size.Width, 0));
var a = webView.ScrollView.ContentSize.Height;
var b = await _webView.EvaluateJavaScriptAsync("(function(){return document.body.scrollHeight;})()");
Console.WriteLine(a);
Console.WriteLine(b);
CGRect tempRect = webView.Frame;
// for test
webView.Frame = new CGRect(tempRect.Location.X, tempRect.Location.Y, tempRect.Size.Width, float.Parse(b));
}
}
catch (Exception ex)
{
//Log the Exception
}
}
I had the same problem and the CustomNavigationDelegate would also only show the same large size, which was the Device Display size.
I found that I had set this on the init part of the XAML and code behind, which somehow overrides the later content-based calculation.
See my fix here: https://stackoverflow.com/a/62409859/3443564
I am new to Xamarin, and mobile development, i have experience with winforms and .net, but threading and MVVM is not something i am very familiar with.
My code works if you click back, select ok in the dialogue, and then click back again. But i would like to improve it by either:
Programmatically call the backbutton to force the check again (with incremented counter)
Close the app directly if you click OK (preferred)
The code below overrides the backbutton on android, and if it returns true. The commented out parts are my fumblings from earlier.
/// <summary>
/// Gives the user a warning that they are about to close the main page
/// </summary>
public override bool OnAndroidBackButtonPressed()
{
if (this.CloseAppAttempts == 0)
{
try
{
this.device.BeginInvokeOnMainThread(async () =>
{
bool closeApp = await this.DisplayService.DisplayAlertAsync("Close app", "Click the OK button to close the app / or click the back button again to close the app", "Ok", "Cancel");
if (closeApp)
{
// this.DisplayService.CloseAsync();
// this.DisplayService.ExitAsync();
// this.DisplayService.pageStack.Pop();
// await IDisplayService.navigation.PopAsync();
this.CloseAppAttempts++;
// i would like to either call the backbutton programatically here, or close
}
});
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("===== Debug Message - Closing the app did not work - [ " + ex.Message + " ]");
}
return true;
}
else
{
return false;
}
}
Xamarin.Android
The following code overrides the back button and warns the user before going back:
public class MainActivity : Activity
{
. . .
bool pressed = false;
public override void OnBackPressed()
{
if (!pressed)
{
Toast.Show("Press again to exit"); //Implement the Toast correctly
pressed = true;
}
else
base.OnBackPressed();
}
}
(Add some logic to rearm this Toast, so if the user press the button after a while, it shows again)
The next snippet shows a message (I don't know how to show a message in Xamarin.Android, but the logic is there)
public class MainActivity : Activity
{
. . .
public override void OnBackPressed()
{
if (Alert("Exit?", "Yes", "No"))
base.OnBackPressed();
}
}
Xamarin.Forms
On Xamarin.Forms, you need to know what page is being shown, so you won't show the dialog at every page and subpage.
In this case, I used NavigationPage to control all the pages.
public class MainActivity : global::Xamarin.Forms.P . . .
{
. . .
bool pressed = false;
public override void OnBackPressed()
{
if (Xamarin.Forms.Application.Current.MainPage.Navigation.NavigationStack.Count == 1)
{
if (Xamarin.Forms.Application.Current.MainPage.Navigation.NavigationStack[0].DisplayAlert(
"Confirm exit",
"Do you really want to exit?",
"Yes", "No"))
base.OnBackPressed();
}
else
base.OnBackPressed();
}
}
You can close the app on android with this command:
public void CloseApplication()
{
var activity = (Activity)Forms.Context;
activity.FinishAffinity();
}
Using Xamarin.Forms you can call it through a dependency service.
I have a Prism based Xamarin Forms app that contains an edit page that is wrapped in a Navigation page so there is a back button at top left on both Android and iOS. To avoid the user accidentally losing an edit in progress by accidentally clicking the back button (in particular on Android) we want to prompt them to confirm that they definitely want to cancel.
Thing is, this seems like something that is not baked in to Xamarin forms. You can override OnBackButtonPressed in a navigation page, but that only gets called for the hardware/software back button on Android. There are articles detailing techniques to intercept the actual arrow button at the top left on Android (involving overriding OnOptionsItemSelected in the Android MainActivity), but on iOS I'm not sure it is even possible.
So I can't help but wonder if I am going about this the wrong way? Should I not be intercepting the top left / hardware / software back button in this way? Seems like a pretty common thing to do (e.g. press back when editing a new contact in the android built in Contacts app and you get a prompt) but it really feels like I am fighting the system here somehow.
There are previous questions around this, most relevant appears to be How to intercept Navigation Bar Back Button Clicked in Xamarin Forms? - but I am looking for some broad brush suggestions for an approach here. My objective is to show the user a page with the <- arrow at top left for Android, "Cancel" for iOS but I would like to get some views about the best way to go about it that does not involve me fighting against prism / navigation pages / xamarin forms and (where possible) not breaking the various "best practices" on Android and iOS.
After going down the same path as you and being told not to prevent users from going back, I decided on showing an alert after they tap the back button (within ContentPage.OnDisappearing()) that says something like Would you like to save your work?.
If you go with this approach, be sure to use Application.MainPage.DisplayAlert() instead of just this.DisplayAlert() since your ContentPage might not be visible at that point.
Here is how I currently handle saving work when they click the back button (I consolidated a good bit of code and changed some things):
protected override async void OnDisappearing() {
base.OnDisappearing();
// At this point the page is gone or is disappearing, but all properties are still available
#region Auto-save Check and Execution
/*
* Checks to see if any edits have been made and if a save is not in progress, if both are true, it asks if they want to save, if yes, it checks for validation errors.
* If it finds them, it marks it as such in the model before saving the model to the DB and showing an alert stating what was done
*/
if(!_viewModel.WorkIsEdited || _viewModel.SaveInProgress) { //WorkIsEdited changes if they enter/change data or focus on certain elements such as a Picker
return;
}
if(!await Application.Current.MainPage.DisplayAlert("ALERT", "You have unsaved work! Would you like to save now?", "Yes", "No")) {
return;
}
if(await _viewModel.SaveClaimErrorsOrNotAsync()) { //The return value is whether validation succeeds or not, but it gets saved either way
App.SuccessToastConfig.Message = "Work saved successfully. Try saving it yourself next time!";
UserDialogs.Instance.Toast(App.SuccessToastConfig);
} else if(await Application.Current.MainPage.DisplayAlert("ERROR", "Work saved successfully but errors were detected. Tap the button to go back to your work.", "To Work Entry", "OK")) {
await Task.Delay(200); //BUG: On Android, the alert above could still be displayed when the page below is pushed, which prevents the page from displaying //BUG: On iOS 10+ currently the alerts are not fully removed from the view hierarchy when execution returns (a fix is in the works)
await Application.Current.MainPage.Navigation.PushAsync(new WorkPage(_viewModel.SavedWork));
}
#endregion
}
What you ask for is not possible. The back button tap cannot be canceled on iOS even in native apps. You can do some other tricks like having a custom 'back' button, but in general you shouldn't do that - you should instead have a modal dialog with the Done and Cancel buttons (or something similar).
If you use xamarin forms that code it is work.
CrossPlatform source
public class CoolContentPage : ContentPage
{
public Action CustomBackButtonAction { get; set; }
public static readonly BindableProperty EnableBackButtonOverrideProperty =
BindableProperty.Create(nameof(EnableBackButtonOverride), typeof(bool), typeof(CoolContentPage), false);
public bool EnableBackButtonOverride{
get { return (bool)GetValue(EnableBackButtonOverrideProperty); }
set { SetValue(EnableBackButtonOverrideProperty, value); }
}
}
}
Android source
public override bool OnOptionsItemSelected(IMenuItem item)
{
if (item.ItemId == 16908332)
{
var currentpage = (CoolContentPage)
Xamarin.Forms.Application.
Current.MainPage.Navigation.
NavigationStack.LastOrDefault();
if (currentpage?.CustomBackButtonAction != null)
{
currentpage?.CustomBackButtonAction.Invoke();
return false;
}
return base.OnOptionsItemSelected(item);
}
else
{
return base.OnOptionsItemSelected(item);
}
}
public override void OnBackPressed()
{
var currentpage = (CoolContentPage)
Xamarin.Forms.Application.
Current.MainPage.Navigation.
NavigationStack.LastOrDefault();
if (currentpage?.CustomBackButtonAction != null)
{
currentpage?.CustomBackButtonAction.Invoke();
}
else
{
base.OnBackPressed();
}
}
iOS source
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
if (((CoolContentPage)Element).EnableBackButtonOverride)
{
SetCustomBackButton();
}
}
private void SetCustomBackButton()
{
var backBtnImage = UIImage.FromBundle("iosbackarrow.png");
backBtnImage = backBtnImage.ImageWithRenderingMode
(UIImageRenderingMode.AlwaysTemplate);
var backBtn = new UIButton(UIButtonType.Custom)
{
HorizontalAlignment =
UIControlContentHorizontalAlignment.Left,
TitleEdgeInsets =
new UIEdgeInsets(11.5f, 15f, 10f, 0f),
ImageEdgeInsets =
new UIEdgeInsets(1f, 8f, 0f, 0f)
};
backBtn.SetTitle("Back", UIControlState.Normal);
backBtn.SetTitleColor(UIColor.White, UIControlState.Normal);
backBtn.SetTitleColor(UIColor.LightGray, UIControlState.Highlighted);
backBtn.Font = UIFont.FromName("HelveticaNeue", (nfloat)17);
backBtn.SetImage(backBtnImage, UIControlState.Normal);
backBtn.SizeToFit();
backBtn.TouchDown += (sender, e) =>
{
// Whatever your custom back button click handling
if(((CoolContentPage)Element)?.
CustomBackButtonAction != null)
{
((CoolContentPage)Element)?.
CustomBackButtonAction.Invoke();
}
};
backBtn.Frame = new CGRect(
0,
0,
UIScreen.MainScreen.Bounds.Width / 4,
NavigationController.NavigationBar.Frame.Height);
var btnContainer = new UIView(
new CGRect(0, 0,
backBtn.Frame.Width, backBtn.Frame.Height));
btnContainer.AddSubview(backBtn);
var fixedSpace =
new UIBarButtonItem(UIBarButtonSystemItem.FixedSpace)
{
Width = -16f
};
var backButtonItem = new UIBarButtonItem("",
UIBarButtonItemStyle.Plain, null)
{
CustomView = backBtn
};
NavigationController.TopViewController.NavigationItem.LeftBarButtonItems = new[] { fixedSpace, backButtonItem };
}
using in xamarin forms
public Page2()
{
InitializeComponent();
if (EnableBackButtonOverride)
{
this.CustomBackButtonAction = async () =>
{
var result = await this.DisplayAlert(null, "Go back?" Yes go back", "Nope");
if (result)
{
await Navigation.PopAsync(true);
}
};
}
}
I have had a problem where on some instances in the emulator, when I click the back hardware button the back page loads with the constructor being called and some other time the constructor is not called.Why is this? Is this because its the emulator?
How are you performing navigation? Are you canceling the initial OnNavigatingFrom in order to perform an animation, then listening initiating navigation again after the animation completes?
protected override void OnNavigatingFrom(System.Windows.Navigation.NavigatingCancelEventArgs e)
{
if (_pendingNavigation == null)
{
VisualStateManager.GoToState(this, "LeavingPage", true);
_pendingNavigation = e.Uri;
e.Cancel = true;
}
base.OnNavigatingFrom(e);
}
void LeavingPage_Completed(object sender, EventArgs e)
{
var uri = _pendingNavigation;
NavigationService.Navigate(uri);
_pendingNavigation = null;
}
The bug occurs when you call NavigationService.Navigate(), which then adds a new page instance to the navigation stack. To fix this bug, you need to check and make sure the initial page navigation is a "New" navigation. Something like so:
if (e.NavigationMode == NavigationMode.New && _pendingNavigation == null)
{
VisualStateManager.GoToState(this, "LeavingPage", true);
_pendingNavigation = e.Uri;
e.Cancel = true;
}