Xamarin Forms WebView CanGoForward property is inaccurate on sites that utilize the History API - xamarin.forms

I am having an issue that relates to a post on GitHub. User nirbil summarizes it best:
If a website utilizes the historyApi then the WebView's CanGoBack CanGoForward are wrong.
Steps to reproduce -
Set the webview's source to a single page web application
Navigate within the application to a different url (should utilize
the pushstate)
Navigate back -> CanGoForward changes to true
Navigate within the application again by following some link ->
CanGoForward erroneously does not change to false
On windows this behavior can be traced to the webview's renderer - https://github.com/xamarin/Xamarin.Forms/blob/master/Xamarin.Forms.Platform.UAP/WebViewRenderer.cs
Here is the original post on GitHub https://github.com/xamarin/Xamarin.Forms/issues/7691
A lot of ideas are suggested on how to solve the problem however, my grasp of Xamarin Forms is somewhat lacking. Any ideas on how to solve the issue?

You could add the source of webview to use renderer. I use the code sample on GitHub. https://github.com/xamarin/Xamarin.Forms/issues/7691
The original:
Set the source of webview.
<WebView x:Name="webView" Source="https://www.google.com" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand"></WebView>
Now:
This question on GitHub has been collected. You could follow it on GitHub. It would be fixed soon.

I solved it using a custom Xamarin Forms WebView with custom CanGoBack/Forward properties.
You can find the it here:
https://github.com/nirbil/XF.CanGoWebView
Following are the main bits of the solution.
CustomWebView control with new bindable properties:
public class CustomWebView : WebView
{
public CustomWebView() {}
public static BindableProperty CustomCanGoForwardProperty =
BindableProperty.Create(
nameof(CustomCanGoForward),
typeof(bool),
typeof(CustomWebView),
false,
BindingMode.OneWayToSource);
public static BindableProperty CustomCanGoBackProperty =
BindableProperty.Create(
nameof(CustomCanGoBack),
typeof(bool),
typeof(CustomWebView),
defaultValue: false,
BindingMode.OneWayToSource);
public bool CustomCanGoForward
{
get => (bool)GetValue(CustomCanGoForwardProperty);
set => SetValue(CustomCanGoForwardProperty, value);
}
public bool CustomCanGoBack
{
get => (bool)GetValue(CustomCanGoBackProperty);
set => SetValue(CustomCanGoBackProperty, value);
}
}
Custom WebViewRenderer on Android:
public class CustomWebViewRenderer : WebViewRenderer
{
protected override WebViewClient GetWebViewClient()
{
CustomWebViewClient webViewClient = new CustomWebViewClient(this);
webViewClient.AddressChanged += AddressChanged;
return webViewClient;
}
private void AddressChanged(string url)
{
if (Element is CustomWebView customWebView && Control != null)
{
customWebView.CustomCanGoBack = Control.CanGoBack();
customWebView.CustomCanGoForward = Control.CanGoForward();
}
}
}
Android CustomWebViewClient that intercepts changes and alerts the renderer:
public class CustomWebViewClient: FormsWebViewClient
{
public delegate void AddressChangedEventHandler(string url);
public event AddressChangedEventHandler AddressChanged;
public CustomWebViewClient(WebViewRenderer renderer) : base(renderer) {}
public override void DoUpdateVisitedHistory(WebView view, string url, bool isReload)
{
base.DoUpdateVisitedHistory(view, url, isReload);
AddressChanged?.Invoke(view.Url);
}
}
Custom WebViewRenderer on UWP:
public class CustomWebViewRenderer : WebViewRenderer
{
bool _registeredControl = false;
long _goBackRegistrationToken = 0;
long _goForwardRegistrationToken = 0;
protected override void Dispose(bool disposing)
{
UnregisterControl();
base.Dispose(disposing);
}
protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
{
base.OnElementChanged(e);
RegisterControl();
}
private void UnregisterControl()
{
if (Control != null && _registeredControl)
{
_registeredControl = false;
Control.UnregisterPropertyChangedCallback(Windows.UI.Xaml.Controls.WebView.CanGoBackProperty, _goBackRegistrationToken);
Control.UnregisterPropertyChangedCallback(Windows.UI.Xaml.Controls.WebView.CanGoForwardProperty, _goForwardRegistrationToken);
}
}
private void RegisterControl()
{
if (Control != null && !_registeredControl)
{
_registeredControl = true;
_goBackRegistrationToken = Control.RegisterPropertyChangedCallback(Windows.UI.Xaml.Controls.WebView.CanGoBackProperty, new Windows.UI.Xaml.DependencyPropertyChangedCallback(OnWebViewCanGoBackChanged));
_goForwardRegistrationToken = Control.RegisterPropertyChangedCallback(Windows.UI.Xaml.Controls.WebView.CanGoForwardProperty, new Windows.UI.Xaml.DependencyPropertyChangedCallback(OnWebViewCanGoForwardChanged));
}
}
private void OnWebViewCanGoBackChanged(Windows.UI.Xaml.DependencyObject sender, Windows.UI.Xaml.DependencyProperty dp)
{
((CustomWebView)Element).CustomCanGoBack = Control.CanGoBack;
}
private void OnWebViewCanGoForwardChanged(Windows.UI.Xaml.DependencyObject sender, Windows.UI.Xaml.DependencyProperty dp)
{
((CustomWebView)Element).CustomCanGoForward = Control.CanGoForward;
}
}

Related

Xamarin forms Back Button Navigation

I'm working on a Xamarin Forms app and am using the MVVM Design.
the issue is when am navigating to another page using
Shell.Current.GoToAsync()
I disable the button to prevent Creating Multiple Pages or DB Operations.
but if I want to go back, I re-enable the buttons in the VM constructor, but the constructor never gets called which means the buttons are still disabled.
I tried to append the // in the Page route to remove the stack thinking that when I go back it will create a new instance Page and VM, but that did not work.
so can anyone help me resolving this problem.
thanks in advance.
Update:
VM Code
public RegisterViewModel()
{
Debug.WriteLine("Class Constructor", Class_Name);
//in case if disabled
RegisterButtonEnabled = true;
RegisterCommand = new Command(RegisterButtonOnClick);
}
public ICommand RegisterCommand { get; }
private bool registerButtonEnabled = true;
public bool RegisterButtonEnabled
{
get => registerButtonEnabled;
set
{
registerButtonEnabled = value;
OnPropertyChanged();
}
}
private async void RegisterButtonOnClick()
{
RegisterButtonEnabled = false;
//More Code
//and then go to Register Page
await Shell.Current.GoToAsync(nameof(RegisterPage));
}
and my xaml
<Button
Command="{Binding RegisterCommand}"
Text="{xct:Translate Register}"
Style="{StaticResource ButtonStyle}"
IsEnabled="{Binding RegisterButtonEnabled,Mode=OneWay}"/>
I had create a default shell project. And find something about the viewmodel. You can add the onappear and the ondisappear method to the viewmodel. Such as:
ViewModel:
public void OnAppearing()
{
RegisterButtonEnabled = true;
}
public void OnDisAppearing()
{
RegisterButtonEnabled = false;
}
Page.cs
ItemsViewModel _viewModel;
public ItemsPage()
{
InitializeComponent();
BindingContext = _viewModel = new ItemsViewModel();
}
protected override void OnAppearing()
{
base.OnAppearing();
_viewModel.OnAppearing();
}
protected override void OnDisappearing()
{
base.OnDisappearing();
_viewModel.OnDisAppearing();
}

Object reference crash in ViewWillAppear in PageRenderer xamarin forms ios

We customize the toolbar menu for navigation back button in xamarin forms ios. I am getting below crash in appcenter.
ToolbarMenuCustomRenderer.ViewWillAppear (System.Boolean animated)
SIGABRT: Object reference not set to an instance of an object
Code snippet below:
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
CustomToolbarContentPage page = Element as CustomToolbarContentPage;
if (page == null)
return;
#region for soft back button
UIViewController root = NavigationController.TopViewController;
if (!page.NeedOverrideSoftBackButton)
return;
string title = "<" + (string.IsNullOrEmpty(NavigationPage.GetBackButtonTitle(Element)) ? "" : NavigationPage.GetBackButtonTitle(Element));
root.NavigationItem.SetLeftBarButtonItem(
new UIBarButtonItem(title, UIBarButtonItemStyle.Plain, (sender, args) =>
{
page.OnSoftBackButtonPressed();
}), true);
#endregion
}
How to resolve this in xamarin forms ios?
I create a sample app with your code and works fine, you can check the code below to see if you miss something:
NewPage:
namespace My_Forms_test.Views
{
public partial class NewPage2 : ContentPage
{
public NewPage2()
{
InitializeComponent();
}
public void OnSoftBackButtonPressed()
{
Navigation.PopToRootAsync();
}
}
}
CustomRenderer:
[assembly:ExportRenderer(typeof(NewPage2),typeof(MyPageRenderer))]
namespace My_Forms_test.iOS
{
public class MyPageRenderer:PageRenderer
{
public MyPageRenderer()
{
}
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
NewPage2 page=Element as NewPage2;
UIViewController root = NavigationController.TopViewController;
string title = "<" + (string.IsNullOrEmpty(NavigationPage.GetBackButtonTitle(Element)) ? " " : NavigationPage.GetBackButtonTitle(Element));
root.NavigationItem.SetLeftBarButtonItem(
new UIBarButtonItem(title, UIBarButtonItemStyle.Plain, (sender, args) =>
{
page.OnSoftBackButtonPressed();
Console.WriteLine("This method is trigged"); }), true);
}
}
Here are ScreenShots:
You can also refer to this documents https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/custom-renderer/contentpage

Xamarin Forms:Prism:Android:MainActivity: Click on Push Notifications: PushAsync not supported globally on Android, please use a NavigationPage

I am trying to implement a basic push notification example using
Xamarin Forms with Prism MVVM, Azure & FCM.
I am receiving notification, but couldn't navigate to a specific page when clicked on the notification.
Trying basic functionality when the app is running or in the background (not closed).
It's throwing an exception "PushAsync not supported globally on Android, please use a NavigationPage" at
ExploreXam.App.Current.MainPage.Navigation.PushAsync(page);
[Activity(LaunchMode = LaunchMode.SingleTask, MainLauncher = true]
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
internal static readonly string CHANNEL_ID = "explore_xamarin";
internal static readonly int NOTIFICATION_ID = 1029;
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
CreateNotificationChannel();
global::Xamarin.Forms.Forms.Init(this, bundle);
LoadApplication(new App());
}
protected override void OnNewIntent(Intent intent)
{
base.OnNewIntent(intent);
Intent = intent;
NotificationClickedOn(intent);
}
private void NotificationClickedOn(Intent intent)
{
if (intent.Action == ExploreXamFirebaseMessagingService.ExploreXamNotification && intent.HasExtra("XamId"))
{
var page = new Xamarin.Forms.NavigationPage(new SpecificPage());
Xamarin.Forms.Application.Current.MainPage.Navigation.PushAsync(page);
ExploreXam.App.Current.MainPage.Navigation.PushAsync(page);
}
}
}
public partial class App : PrismApplication
{
public bool navigating;
public App(IPlatformInitializer initializer = null, bool shallNavigate=false) : base(initializer)
{
navigating = shallNavigate;
}
protected async override void OnInitialized()
{
BlobCache.ApplicationName = "ExploreXam";
InitializeComponent();
FlowListView.Init();
//await NavigationService.NavigateAsync("LoginPage");
await NavigationService.NavigateAsync("NavigationPage/LoginPage");
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
//mapping
}
}
Any idea that would help out, please?
You could access Prims's NavigationService instance to achieve what you're trying to do. But, it's a protected property. So, first you'd have to expose it through your App class as below :
public new INavigationService NavigationService => base.NavigationService;
Now, you can access the NavigationService from anywhere in your app by simply referencing it through your App as below:
(Xamarin.Forms.Application.Current as App).NavigationService.NavigateAsync("your/page/path");
So, your App class would look something like this:
public partial class App : PrismApplication
{
public new INavigationService NavigationService => base.NavigationService;
public bool navigating;
public App(IPlatformInitializer initializer = null, bool shallNavigate=false) : base(initializer)
{
navigating = shallNavigate;
}
protected async override void OnInitialized()
{
BlobCache.ApplicationName = "ExploreXam";
InitializeComponent();
FlowListView.Init();
//await NavigationService.NavigateAsync("LoginPage");
await NavigationService.NavigateAsync("NavigationPage/LoginPage");
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
//mapping
}
}
And your NotificationClickOn function would become something like :
private async void NotificationClickedOn(Intent intent)
{
if (intent.Action == ExploreXamFirebaseMessagingService.ExploreXamNotification && intent.HasExtra("XamId"))
{
var navigationService = (Xamarin.Forms.Application.Current as ContosoCookbook.App).NavigationService;
await navigationService.NavigateAsync("YourNavigationPage/SpecificPage");
}
}
The reason this is happening is because your Application.Current.MainPage is not a Navigation page but a ContentPage (i assume)
Wrap your initial MainPage in a NavigationPage as show below and it should work
In your App.xaml.cs
MainPage= new NavigationPage(new FirstPage());
I agree with #chaosifier. Create a public INavigationService in your App.xaml.cs file and then in the OnInitialized() method make the public property = the the base.NavigationService;
public INavigationService PrismNavigation { get; private set; }
protected override async void OnInitialized()
{
InitializeComponent();
PrismNavigation = base.NavigationService;
}
Then from the MainActivity.cs file you can navigate using something like this
(Xamarin.Forms.Application.Current as App).PrismNavigation.NavigateAsync(nameof(ShowAlertsDetailPage));
I hope this helps.

Xamarin Forms Entry Return Type not working as expected

I tried setting the new feature in Xamarin Forms 3 which is ReturnType and I have set it to Next. My form has multiple fields and I want to make that the next Entry is focused when the Next button is pressed. However it just closes the keyboard. I did read the documents however I could not find the way to focus it to the next Entry. Can someone please guide?
Thanks in advance.
Those who want to know how I implemented it, it is as follows:
I created a behavior which will handle the OnAttachedTo and OnDetachingFrom so that I can handle the Completed event to move the focus. Now for that, I need a BindableProperty. I created the following code out of the logic:
public class NextEntryBehavior : Behavior<Entry>
{
public static readonly BindableProperty NextEntryProperty = BindableProperty.Create(nameof(NextEntry), typeof(Entry), typeof(Entry), defaultBindingMode: BindingMode.OneTime, defaultValue: null);
public Entry NextEntry
{
get => (Entry)GetValue(NextEntryProperty);
set => SetValue(NextEntryProperty, value);
}
protected override void OnAttachedTo(Entry bindable)
{
bindable.Completed += Bindable_Completed;
base.OnAttachedTo(bindable);
}
private void Bindable_Completed(object sender, EventArgs e)
{
if (NextEntry != null)
{
NextEntry.Focus();
}
}
protected override void OnDetachingFrom(Entry bindable)
{
bindable.Completed -= Bindable_Completed;
base.OnDetachingFrom(bindable);
}
}
As you can see, there is a NextEntry property, we use it via XAML to focus on the desired entry field once the user marks it as complete using the Next button.
XAML:
<Entry ReturnType="Next">
<Entry.Behaviors>
<behaviors:NextEntryBehavior NextEntry="{x:Reference LastName}" />
</Entry.Behaviors>
</Entry>
In the above behavior, the LastName reference I used is the control to which the focus should go when the user taps on Next.
This way, it worked as expected and is reusable across the project.
the property ReturnType for Entry will only set graphically the Return Key in Keyboard to the specified type - Next in your case.
In order to set Focus for another Entry in the view you need to call Focus() from within the targeted Entry in the code-behind.
For Example:
private void txtUsername_OnCompleted(object sender, EventArgs e)
{
txtPassword.Focus();
}
if you're applying MVVM pattern. You will need a property in the Entry to Bind on for ViewModel property. One way to achieve this is by extending Entry control to add a bindable property called "IsActive" and create a Trigger that listens for changes on this property and calls Focus(), like below:
public class ExtendedEntry : Entry
{
public static readonly BindableProperty IsActiveProperty = BindableProperty.Create(
nameof(IsActive),
typeof(bool),
typeof(ExtendedEntry),
defaultBindingMode: BindingMode.TwoWay,
defaultValue: false);
public bool IsActive
{
get => (bool)GetValue(IsActiveProperty);
set => SetValue(IsActiveProperty, value);
}
private Trigger _isActiveTriger;
private EntryIsActiveAction _activeAction;
public ExtendedEntry()
{
InitTriggers();
}
private void InitTriggers()
{
InitIsActiveTrigger();
}
private void InitIsActiveTrigger()
{
_activeAction = new EntryIsActiveAction();
_isActiveTriger = new Trigger(typeof(ExtendedEntry))
{
Value = false,
Property = IsActiveProperty,
EnterActions = { _activeAction },
ExitActions = { _activeAction }
};
Triggers.Add(_isActiveTriger);
}
}
public class EntryIsActiveAction : TriggerAction<ExtendedEntry>
{
protected override void Invoke(ExtendedEntry sender)
{
if (sender.IsActive)
{
sender.Focus();
return;
}
sender.Unfocus();
}
}
Example Usage:
Xaml page:
<Entry x:Name="txtPassword" IsActive="{Binding IsPasswordActive}" />
ViewModel:
private bool _isPasswordActive;
public bool IsPasswordActive
{
get => _isPasswordActive;
set
{
_isPasswordActive = value;
OnPropertyChanged();
}
}

Page navigation through ViewModel using MVVMLight in windows 8

I just started developing my brand new windows 8 application last week using mvvm light.I am familiar with mvvmlight WP7 navigation. How i can achieve the same in windows 8. Can any one suggest a better method to achieve the same in windows 8. I found a solution, where we override onnavigated events in vm and handle navigate to other page. But i think that method is obsolete. Any one please guide me with the proper implementation. Thanks in advance.
I understand this is not the exact answer you may be looking for, but this may give you some ideas to explore.
In my case, I'm not using MVVMLight - but my own simple MVVM implementation. I use the BindableBase class (which comes with the default VS 2012 RC templates) for property notifications. I imagine, you could use MVVMLight to give you some of the infrastructure, which you can complement with something like the below.
For navigation, I define an interface that looks like:
public interface INavigationService
{
void Navigate(Type type);
void Navigate(Type type, object parameter);
void EnsureNavigated(Type pageType, object parameter);
bool CanGoBack { get; }
bool CanGoForward { get; }
void GoBack();
void GoForward();
IView CurrentView { get; }
}
And implement it as follows:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.UI.Xaml.Controls;
public class NavigationService : INavigationService
{
private readonly Frame _frame;
public NavigationService(Frame frame)
{
_frame = frame;
_frame.Navigated += OnFrameNavigated;
}
private void OnFrameNavigated(object sender, Windows.UI.Xaml.Navigation.NavigationEventArgs e)
{
var view = e.Content as IView;
if (view == null)
return;
var navMsg = new NavigationMessage()
{
Sender = this,
NewView = view,
Parameter = e.Parameter,
NavigationMode = (int)e.NavigationMode
};
EventManager.Current.Publish(navMsg);
//Anything that the parent needs to be notified should happen in of after this method
var viewModel = view.ViewModel;
if (viewModel != null)
viewModel.Initialise(e.Parameter);
}
public void Navigate(Type pageType)
{
DisposePreviousView();
_frame.Navigate(pageType);
}
public void Navigate(Type pageType, object parameter)
{
DisposePreviousView();
_frame.Navigate(pageType, parameter);
}
private void DisposePreviousView()
{
var currentView = this.CurrentView;
var currentViewDisposable = currentView as IDisposable;
if (currentViewDisposable != null)
{
currentViewDisposable.Dispose();
currentViewDisposable = null;
} //view model is disposed in the view implementation
}
public void EnsureNavigated(Type pageType, object parameter)
{
var currentView = this.CurrentView;
if (currentView == null || currentView.GetType() != pageType)
{
Navigate(pageType, parameter);
}
}
public IView CurrentView
{
get { return _frame.Content as IView; }
}
public bool CanGoBack
{
get { return _frame != null && _frame.CanGoBack; }
}
public void GoBack()
{
// Use the navigation frame to return to the previous page
if (_frame != null && _frame.CanGoBack) _frame.GoBack();
}
public bool CanGoForward
{
get { return _frame != null && _frame.CanGoForward; }
}
public void GoForward()
{
// Use the navigation frame to return to the previous page
if (_frame != null && _frame.CanGoForward) _frame.GoForward();
}
}
IView:
public interface IView : IDisposable
{
IViewModel ViewModel { get; }
void Refresh();
}
IViewModel:
public interface IViewModel : INotifyPropertyChanged, IDisposable
{
void Initialise(object parameter);
string ViewTitle { get; }
void Refresh();
}
Finally, in the XAML page, define a Frame element:
<Frame x:Name="ContentFrame" />
And in the code-behind of the page: (this in the only ugly part in my opinion - but its hopefully not too bad):
var _navigationService = new NavigationService(this.ContentFrame);
You can now pass the _navigationService to the viewmodel. In my case I create the viewmodel in the code-behind of the page:
public HomePage()
{
this.InitializeComponent();
var _navigationService = NavigationService.GetFor(this.ContentFrame);
DataContext = new HomePageViewModel(_navigationService);
}
Hope this helps.
Read the article published in MSDN Magazine just recently by Laurent Bugnion himself on working with the MVVM Light Toolkit and Windows 8.
Towards the end of the article he explains exactly how to setup the NavigationService you need.
http://msdn.microsoft.com/en-us/magazine/jj651572.aspx
The NavigationService that was in MVVMLight has been migrated in a new package called WinRTBehaviors. You can also get EventToCommand in Win8nl, both from nuget. See my blog posted here:
Getting Started w/ MVVM Light for Windows 8, EventToCommand and Behaviors
http://blog.tattoocoder.com/2012/08/getting-started-w-windows-8-mvvm-light.html

Resources