What is the difference between WKNavigationDelegate and WKUIDelegate - xamarin

Can I use both of them in a project?
I need to override WKUIDelegate's CreateWebView method in order to open target=_blank links:
public override WKWebView CreateWebView(WKWebView webView, WKWebViewConfiguration configuration, WKNavigationAction navigationAction, WKWindowFeatures windowFeatures)
{
var url = navigationAction.Request.Url;
if (navigationAction.TargetFrame == null)
{
webView.LoadRequest(navigationAction.Request);
}
return null;
}
When I use WKUIDelegate in a demo it works (opens target _blank). But in real project they used WKNavigationDelegate too. And applying WKUIDelegate CreateWebView doesn't work.
OnElementChange in the renderer is like this:
var config = new WKWebViewConfiguration { };
webView = new WKWebView(Frame, config);
// Set the delegate here
webView = new WKWebView(this.Frame, new WKWebViewConfiguration());
webView.ScrollView.ScrollEnabled = true;
webView.ScrollView.Bounces = true;
webView.NavigationDelegate = new DisplayLinkWebViewDelegate();
webView.UIDelegate = MyWkWebViewDelegate();
SetNativeControl(webView);

WKNavigationDelegate : It helps you implement custom behaviors that are triggered during a web view's process of accepting, loading, and completing a navigation request.
And the WKUIDelegate class provides methods for presenting native user interface elements on behalf of a webpage.
The webpage here is not the webview ,but the html which been loaded on webview.
As we can see in following image
The method in WKUIDelegate are all associated with JS.
For more details about the two protocols you can check https://developer.apple.com/documentation/webkit/wkuidelegate?language=objc
and
https://developer.apple.com/documentation/webkit/wknavigationdelegate?language=objc
if you want to do something when the webview finished loading, you can implement the method DidFinishNavigation in WKNavigationDelegate .
public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
if(!webView.IsLoading)
{
// do some thing you want
}
}

Related

MvvmCross migration causing a Xamarin Custom iOS View Presenter issue

While creating a CustomIosViewPresenter (of type MvxIosViewPresenter), in MVVMCross 5.x, there was a Show override that I was able to use to get the IMvxIosView so as to update the UIViewController presentation style using the PresentationValues from the ViewModel.
I had this code and it worked:
// Worked before
public override void Show(IMvxIosView view, MvvmCross.ViewModels.MvxViewModelRequest request)
{
if (request.PresentationValues != null)
{
if (request.PresentationValues.ContainsKey("NavigationMode") &&
request.PresentationValues["NavigationMode"] == "WrapInModalWithNavController")
{
var vc = view as IModalPresentation;
vc.ModalPresentationAttribute = new MvxModalPresentationAttribute
{
WrapInNavigationController = true,
ModalPresentationStyle = UIModalPresentationStyle.OverFullScreen,
ModalTransitionStyle = UIModalTransitionStyle.CoverVertical
};
}
}
base.Show(view, request);
}
But after migrating to MvvmCross 7.1, the older override doesn't work anymore and I have to use this instead, but there is no view passed into the Show override, how do I get it?
I tried this code below, but view is null and it's not able to cast it this way var view = viewType as IMvxIosView;
// Doesn't work now
public override Task<bool> Show(MvxViewModelRequest request)
{
if (request.PresentationValues != null)
{
if (request.PresentationValues.ContainsKey("NavigationMode") &&
request.PresentationValues["NavigationMode"] == "WrapInModalWithNavController")
{
var viewsContainer = Mvx.IoCProvider.Resolve<IMvxViewsContainer>();
var viewType = viewsContainer.GetViewType(request.ViewModelType);
var view = viewType as IMvxIosView;
var vc = view as IModalPresentation;
vc.ModalPresentationAttribute = new MvxModalPresentationAttribute
{
WrapInNavigationController = true,
ModalPresentationStyle = UIModalPresentationStyle.OverFullScreen,
ModalTransitionStyle = UIModalTransitionStyle.CoverVertical
};
}
}
return base.Show(request);
}
The reason I need this is because without this function when I close the special flow of view controllers that need this, its not closing all the view controllers in that flow, it closes only one of them at a time.
What you would normally do with MvvmCross if you want to navigate within a Modal ViewController is firstly add a MvxModalPresentationAttribute to the modal that will host the rest of the navigation where you set WrapInNavigationController to true.
For the children, it would just be regular child navigation, no attributes needed.
If you then want to control how the modal is popping you would create your own MvxPresentationHint and register it in your presenter using AddPresentationHintHandler.
Then you would in your ViewModel where you want to change the presentation call NavigationService.ChangePresentation(your hint).
As for the Presentation Hint, it should probably just call CloseModalViewControllers and that would probably do what you want.
TLDR: Feel for the developers that will come after you and build stuff the right way
So I dug into the MvvmCross MvxIosViewPresenter source code and was able to use this new override CreateOverridePresentationAttributeViewInstance()
I needed the request object to see the presentation values so I updated the Show function that gets called before the other override as follows:
MvxViewModelRequest _request;
public override Task<bool> Show(MvxViewModelRequest request)
{
_request = request;
return base.Show(request);
}
And I was able to get the ViewController this way, in order to selectively present it as a modal:
{
var view = base.CreateOverridePresentationAttributeViewInstance(viewType);
if (_request.PresentationValues.ContainsKey("NavigationMode") &&
_request.PresentationValues["NavigationMode"] == "WrapInModalWithNavController")
{
var vc = view as IModalPresentation;
vc.ModalPresentationAttribute = new MvxModalPresentationAttribute
{
WrapInNavigationController = true,
ModalPresentationStyle = UIModalPresentationStyle.OverFullScreen,
ModalTransitionStyle = UIModalTransitionStyle.CoverVertical
};
return vc;
}
return view;
}
And then the closing of the modal was another challenge, that I was able to figure out using the TryCloseViewControllerInsideStack and ChangePresentation overrides

How to handle Xamarin Webview where a link opens a new tab?

I'm using a Xamarin Webview to display a website which has buttons with linked pdf files. Browsers will open the pdf file in a new tab but webview doesn't support tabs and just does nothing on click of the button. How can I handle this?
The most preferable solution would be to show the new tab in the same webview, just as if it wouldn't open a new tab. I tried to implement a custom renderer which inherits from android webview, but even there I found no possibility to handle it or to just get the URL where the button redirects to.
Thanks in advance for your help.
WebView does not support tabs if you want a tabbed browser UI you need to implement it yourself.
1.Setting SetSupportMultipleWindows to true
2.Implementing WebChromeClient and override it's OnCreateWindow method.
public class MyWebViewRenderer : WebViewRenderer
{
public MyWebViewRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
if(Control!=null)
{
Control.Settings.SetSupportMultipleWindows(true);
Control.Settings.JavaScriptEnabled = true;
Control.SetWebChromeClient(new MyWebChromeClient());
}
}
}
public class MyWebChromeClient: WebChromeClient
{
public override bool OnCreateWindow(Android.Webkit.WebView view, bool isDialog, bool isUserGesture, Message resultMsg)
{
if(!isDialog)
{
return true;
}
return base.OnCreateWindow(view, isDialog, isUserGesture, resultMsg);
}
}
}

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)

How to show iOS add contact screen from Xamarin Forms?

I'm trying to show the iOS add contact screen using Xamarin Forms. From what I can see Xamarin Forms does not support this out of the box but Xamarin iOS does. Unfortunately I can't get them to work together. What I mean by "together" is that I need get access to NavigationController from Xamarin Forms Page.
Can this be done?
I have a sample solution that demonstrates the problem here: https://github.com/pawelpabich/XamarinFormsContacts. I also put the most important code below.
public void ShowContact(NavigationPage page)
{
var newPersonController = new ABNewPersonViewController();
var person = new ABPerson();
person.FirstName = "John";
person.LastName = "Doe";
newPersonController.DisplayedPerson = person;
var controller = page.CreateViewController();
//!!!!---> controller.NavigationController is null !!!!!<----
controller.NavigationController.PushViewController(newPersonController, true);
}
I updated the repo and it now contains code that works.
There is a UINavigationController when using Xamarin.Forms (when using a NavigationPage), but you have to search for it. This was the only way I could get a hold of it. Those other methods, CreateViewController and RendererFactory actually create a new ViewController which isn't what you wanted.
public void ShowContact(NavigationPage page)
{
var newPersonController = new ABNewPersonViewController();
var person = new ABPerson();
person.FirstName = "John";
person.LastName = "Doe";
newPersonController.Title = "This is a test";
newPersonController.DisplayedPerson = person;
UINavigationController nav = null;
foreach (var vc in
UIApplication.SharedApplication.Windows[0].RootViewController.ChildViewControllers)
{
if (vc is UINavigationController)
nav = (UINavigationController)vc;
}
nav.PresentModalViewController(new UINavigationController (newPersonController), true);
}
I also attempted to Create a PersonPage and PersonPageRenderer, as that would be the cleanest solution, but I couldn't get it working. This could work if you spent some time.
[assembly: ExportRenderer(typeof(PersonPage), typeof(PersonPageRenderer))]
public class PersonPageRenderer : ABNewPersonViewController, IVisualElementRenderer, IDisposable, IRegisterable
Pawel, the problem is that when you use Xamarin.Forms no NavigationController is created (as I know at least in X.F 1.3+, maybe Michael will prove me wrong). If you want to create new address boo element you can use this approach - How do you add contacts to the iPhone Address book with monotouch?
Because iOS Add Contact screen is a Native iOS API and your application logic is in a PCL you need to use a DependancyService.
1) To do this in the PCL create a Interface which provides the functionality, like
public interface ILocalAddContact
{
void DisplayContactScreen(Contact contact)
}
2) Implement the Interface in the Native Applications:
public class LocalAddContactiOS : ILocalAddContact
{
public void DisplayContactScreen(Contact contact)
{
//... do iOS Magic
}
}
3) Register the Dependancy in the Top of the Native File
[assembly: Xamarin.Forms.Dependency(typeof(LocalAddContactiOS))]
4) Obtain the Dependancy from the iOS Project from the
var addContact = DependencyService.Get<ILocalAddContact> ();
addContact.DisplayContactScreen (contact);
If you take a look at this sample application on github, it's very similar (but is used for CreateCalendar).
Ok, this is how I finally implemented. I created UINavigationController manually and use it for navigations outside Xamarin.Forms.
using System;
using System.Collections.Generic;
using System.Linq;
using Foundation;
using UIKit;
using Xamarin.Forms;
using AddressBookUI;
using AddressBook;
namespace TestContacts.iOS
{
[Register ("AppDelegate")]
public partial class AppDelegate : UIApplicationDelegate
{
UIWindow window;
public override bool FinishedLaunching (UIApplication app, NSDictionary options)
{
global::Xamarin.Forms.Forms.Init ();
window = new UIWindow(UIScreen.MainScreen.Bounds);
var nav =
new UINavigationController(new App ().MainPage.CreateViewController());
ContactsShared.Instance = new TouchContacts (nav);
window.RootViewController = nav;
window.MakeKeyAndVisible();
return true;
}
}
public class TouchContacts : IContactsShared {
UINavigationController nav;
public TouchContacts(UINavigationController nav){
this.nav = nav;
}
public void Show() {
var newPersonController = new ABNewPersonViewController();
newPersonController.NewPersonComplete +=
(object sender, ABNewPersonCompleteEventArgs e) =>
{
nav.PopViewController(true);
};
var person = new ABPerson();
person.FirstName = "John";
person.LastName = "Doe";
newPersonController.DisplayedPerson = person;
nav.PushViewController(newPersonController, true);
}
}
}

Open a webpage without using webview

I'd like to open a webpage using the webbrowser on the device. Right now I use a WebView, but I want to let the user choose between Chrome, Safari or any other webbrowser currently on the device.
Is there any way to do this?
var url = "http://www.google.com";
Device.OpenUri(new Uri(url));
And this uses the default browser to open the url.
Source: https://forums.xamarin.com/discussion/comment/94202#Comment_94202
API Docs: Xamarin.Forms.Device.OpenUri
I'm using this code:
var uri = Android.Net.Uri.Parse ("http://www.google.com");
var intent = new Intent (Intent.ActionView, uri);
StartActivity (intent);
And the compact version:
StartActivity (new Intent (Intent.ActionView, Android.Net.Uri.Parse ("http://www.google.com")));
simple solution ,
WebView web_view;
protected override void OnCreate (Bundle bundle)
{
base.OnCreate (bundle);
SetContentView (Resource.Layout.Social);
web_view = FindViewById<WebView> (Resource.Id.webView);
web_view.Settings.JavaScriptEnabled = true;
web_view.SetWebViewClient (new HelloWebViewClient ());
web_view.Settings.LoadWithOverviewMode = true;
web_view.Settings.UseWideViewPort = true;
web_view.LoadUrl ("http://www.facebook.com");
}
public class HelloWebViewClient : WebViewClient
{
public override bool ShouldOverrideUrlLoading (WebView view, string url)
{
view.LoadUrl (url);
return true;
}
}
I think it's the default web browser who open the webpage that you want.
Try to see this way.

Resources