How To Pass a Query String in Xamarin WebView Source On Navigating - xamarin

I need to pass a query string into a WebView Xamarin Source on Navigating (www.websiteexmple.com?mobileapp=true).
In Android works perfect, but in IOS does not seems to work correctly sometimes . I am missing someting ?
I have notice it does not work in IOS when Net Core website needs to do some redirects : Ex: User Enter Email and click Next Step.
public void WebView_NavigatingAsync(object sender, WebNavigatingEventArgs args)
{
if (!args.Url.Contains(Source) && !args.Url.Contains("facebook") && !args.Url.Contains("google"))
{
var uri = new Uri(args.Url);
// Device.OpenUri(uri);
Launcher.OpenAsync(uri);
args.Cancel = true;
}
else
{
var src = args.Url;
string prefix = "?";
if (src.Contains("?"))
{
prefix = "&";
}
if (!args.Url.Contains("mobileapp=true"))
{
args.Cancel = true;
var webview = (WebView)sender;
webview.Source = src + prefix + "mobileapp=true";
}
}
}

I think there is a bug in WebView.Source for iOS, the question mark is coming encoded and the system didn't recognize the URL.
You can create a custom WebViewRender and replace %3F to ?
On iOS project create a CustomWebViewRender
[assembly: ExportRenderer(typeof(CustomWebView), typeof(CustomWebViewRenderer))]
namespace Test.iOS.Renders
{
public class CustomWebViewRenderer : WkWebViewRenderer
{
public override WKNavigation LoadRequest (NSUrlRequest request)
{
var url = request.Url.ToString().Replace("html%3F", "html?");
var dotnetURI = new System.Uri(url);
var idn = new System.Globalization.IdnMapping();
NSUrl nsURL = new NSUrl(dotnetURI.Scheme, idn.GetAscii(dotnetURI.DnsSafeHost), dotnetURI.PathAndQuery);
return base.LoadRequest(new NSUrlRequest(nsURL));
}
}
}
On Xamarin project use your custom WebView
cs file:
namespace Test.Renders
{
public class CustomWebView : WebView
{
}
}
Xaml file:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:test="clr-namespace:Test.Renders"
...>
<ContentPage.Content>
<StackLayout>
<test:CustomWebView x:Name="map"
HorizontalOptions="FillAndExpand"
VerticalOptions="FillAndExpand"/>
...

Related

Xamarin WebView GestureRecognizer not working

I have an awkward problem with GestureRecognizers on Xamarin WebView:
Although the documentation any some questions/answers here and in Xamarin Forum say that WebView GestureRecognizers should all work, I can't get it to fire any event.
My XAML code looks like this:
<StackLayout BackgroundColor="LightGray" >
<WebView x:Name="webView" VerticalOptions="FillAndExpand" >
<WebView.GestureRecognizers>
<SwipeGestureRecognizer Direction="Left" Swiped="onSwiped"/>
</WebView.GestureRecognizers>
<WebView.Source>
<HtmlWebViewSource Html="{Binding HTML}" />
</WebView.Source>
</WebView>
</StackLayout>
Alternatives treid so far:
Same GestureRecognizer on the Title of the same page: works
Same GestureRecognizer on a ListView of another page: works
Tried Nuget package Vapolia.XamarinGestures which also didn't work on the webview
Tried to put the GestureRecoginzer on the StackLayout around the WebView: didn't work either.
Tried it on iOS device and simulator. Normally iOS should be the easy part here...
What I actually want to achieve: with a swipe left move forward to another (programatically defined) web page.
I assume those gestures are somehow absorbed by the webview for regular navigation, but I was wondering why some examples would say that all gestures work on the webview.
An alternative could be to add that target webpage to the webview history stack on the "forward" path.. but not sure how to do that.
Anyone has some hints?
You could use Custom Renderer to add the swipe event on specific platform. And handle them in Forms .
in Forms
create a CustomWebView
public class CustomWebView : WebView
{
public event EventHandler SwipeLeft;
public event EventHandler SwipeRight;
public void OnSwipeLeft() =>
SwipeLeft?.Invoke(this, null);
public void OnSwipeRight() =>
SwipeRight?.Invoke(this, null);
}
in Android
using Android.Content;
using Android.Views;
using App11;
using App11.Droid;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;
[assembly: ExportRenderer(typeof(CustomWebView), typeof(MyWebViewRenderer))]
namespace App11.Droid
{
public class MyWebViewRenderer : WebViewRenderer
{
public MyWebViewRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
{
base.OnElementChanged(e);
Control.SetOnTouchListener(new MyOnTouchListener((CustomWebView)Element));
}
}
public class MyOnTouchListener : Java.Lang.Object, Android.Views.View.IOnTouchListener
{
float oldX;
float newX;
CustomWebView myWebView;
public MyOnTouchListener(CustomWebView webView)
{
myWebView = webView;
}
public bool OnTouch(Android.Views.View v, MotionEvent e)
{
if (e.Action == MotionEventActions.Down)
{
oldX = e.GetX(0);
}
if (e.Action == MotionEventActions.Up)
{
newX = e.GetX();
if (newX - oldX > 0)
{
myWebView.OnSwipeRight();
}
else
{
myWebView.OnSwipeLeft();
}
}
return false;
}
}
}
in iOS
using App11;
using App11.iOS;
using Foundation;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
using ObjCRuntime;
[assembly: ExportRenderer(typeof(CustomWebView), typeof(MyWebViewRenderer))]
namespace App11.iOS
{
public class MyWebViewRenderer:WkWebViewRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
if(e.NewElement!=null)
{
this.BackgroundColor = UIColor.Red;
UISwipeGestureRecognizer leftgestureRecognizer = new UISwipeGestureRecognizer(this,new Selector("SwipeEvent:"));
leftgestureRecognizer.Direction = UISwipeGestureRecognizerDirection.Left;
UISwipeGestureRecognizer rightgestureRecognizer = new UISwipeGestureRecognizer(this, new Selector("SwipeEvent:"));
rightgestureRecognizer.Direction = UISwipeGestureRecognizerDirection.Right;
leftgestureRecognizer.Delegate = new MyWebViewDelegate();
rightgestureRecognizer.Delegate = new MyWebViewDelegate();
this.AddGestureRecognizer(leftgestureRecognizer);
this.AddGestureRecognizer(rightgestureRecognizer);
}
}
[Export("SwipeEvent:")]
void SwipeEvent(UISwipeGestureRecognizer recognizer)
{
var webview = Element as CustomWebView;
if(recognizer.Direction == UISwipeGestureRecognizerDirection.Left)
{
webview.OnSwipeLeft();
}
else if(recognizer.Direction == UISwipeGestureRecognizerDirection.Right)
{
webview.OnSwipeRight();
}
}
}
public class MyWebViewDelegate: UIGestureRecognizerDelegate
{
public override bool ShouldRecognizeSimultaneously(UIGestureRecognizer gestureRecognizer, UIGestureRecognizer otherGestureRecognizer)
{
return false;
}
}
}
Now you just need to use it like
<local:CustomWebView x:Name="browser"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand"
SwipeLeft="browser_SwipeLeft"
SwipeRight="browser_SwipeRight">
There was an additional trick to make it finally work. All the above (correct) solution was ignored due to my Xamarin MasterDetailPage setup.
This was capturing all horizontal swipes and not putting them through to the HybridWebView.
MasterDetailPage.IsGestureEnabled = false;
finally fixed it and enabled the swipe gestures in my WebView.

Xamarin iOS webview underlap behind navigation bar

I am using xamarin custom webview to load my page in app. But facing issue that title of webpage hides behind navigation bar . Or sometimes bottom of page not shown. I have tried adding scrollbar to my layout but still facing issue. Same works perfectly on android. Is it due to custom webview? I just want my webview to start below navigation bar and load completely according to device size.
my custom webview code :
public class CustomWebView : WebView
{
public static readonly BindableProperty UriProperty = BindableProperty.Create(
propertyName: "Uri",
returnType: typeof(string),
declaringType: typeof(CustomWebView),
defaultValue: default(string));
public string Uri
{
get { return (string)GetValue(UriProperty); }
set { SetValue(UriProperty, value); }
}
}
Xaml Page :
<StackLayout Orientation="Vertical" HorizontalOptions="StartAndExpand" VerticalOptions="StartAndExpand">
<StackLayout>
<Label x:Name="type" Text="Loading..." FontSize="Medium"/>
</StackLayout>
<StackLayout VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand">
<ScrollView Orientation="Vertical" FlowDirection="MatchParent" HorizontalOptions="StartAndExpand" VerticalOptions="StartAndExpand" Visual="Material" VerticalScrollBarVisibility="Always">
<OnPlatform x:TypeArguments="View">
<On Platform="Android">
<WebView x:Name="dashboard_android" HeightRequest="1000" WidthRequest="1000" />
</On>
<On Platform="iOS">
<local:CustomWebView x:Name="dashboard_ios" VerticalOptions="StartAndExpand" HorizontalOptions="FillAndExpand" WidthRequest="1000" HeightRequest="1000"/>
</On>
</OnPlatform>
</ScrollView>
</StackLayout>
</StackLayout>
code behind :
dashboard_android.Source = url;
dashboard_ios.Uri = url;
Following are solutions i have tried but no success
Solution 1 :
I have tried adding two properties, but no use
this.EdgesForExtendedLayout = UIRectEdge.None;
this.ExtendedLayoutIncludesOpaqueBars = false;
Solution 2 :
Tried enabling this unsafe area property , still no success
ios:Page.UseSafeArea="true"
Solution 3 :
Tried setting webview height on content size dynamically , but no success
public override async void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
// base.DidFinishNavigation(webView, navigation);
var wv = _webViewRenderer.Element as CustomWebView;
if (wv != null)
{
await System.Threading.Tasks.Task.Delay(100); // wait here till content is rendered
wv.HeightRequest = (double)webView.Frame.Size.Height; // ScrollView.ContentSize.Height;
}
}
Updated Xaml Code :
<StackLayout VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand">
<local:CustomWebView x:Name="dashboard" HeightRequest="1000" WidthRequest="1000" />
</StackLayout>
Updated Code behind :
public partial class DashboardView : ContentPage
{
string url;
public DashboardView()
{
InitializeComponent();
url= ""; //adding url to load here
dashboard.Uri = url;
}
}
Custom WebView Renderer
[assembly: ExportRenderer(typeof(CustomWebView), typeof(MyCustomWebViewRenderer))]
namespace Report.iOS
{
public class MyCustomWebViewRenderer : ViewRenderer<CustomWebView, WKWebView>
{
WKWebView webView;
protected override void OnElementChanged(ElementChangedEventArgs<CustomWebView> e)
{
base.OnElementChanged(e);
if (Control == null)
{
webView = new WKWebView(Frame, new WKWebViewConfiguration());
webView.NavigationDelegate = new WebViewDelegate();
SetNativeControl(webView);
}
if (e.NewElement != null)
{
Control.LoadRequest(new NSUrlRequest(new NSUrl(Element.Uri)));
}
}
}
public class WebViewDelegate : WKNavigationDelegate, INSUrlConnectionDataDelegate
{
string uname = null;
string pass = null;
public override async void DidReceiveAuthenticationChallenge(WKWebView webView, NSUrlAuthenticationChallenge challenge, Action<NSUrlSessionAuthChallengeDisposition, NSUrlCredential> completionHandler)
{
try
{
uname = Xamarin.Forms.Application.Current.Properties.ContainsKey("Username") ? Convert.ToString(Xamarin.Forms.Application.Current.Properties["Username"]) : null;
pass = await SecureStorage.GetAsync("Password");
}
catch (Exception ex)
{
}
completionHandler(NSUrlSessionAuthChallengeDisposition.UseCredential, new NSUrlCredential(uname, pass, NSUrlCredentialPersistence.ForSession));
return;
}
}
}
Screenshot of webview screen :
Here i am loading this webpage(https://learn.microsoft.com/en-us/xamarin/essentials/device-display?tabs=android). As you can see half of footer is hidden and i am not able to scroll it.
Screenshot of app
The reason for it quite simple actually you have added the WebView inside a scrollView which is, in turn, causing the issue webview has its own scroll so all you have to do is something like:
<StackLayout VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand">
<local:CustomWebView x:Name="dashboard" />
</StackLayout>
Also, you do not need the on the platform you can directly use the below and the custom renderer you have created.
The Height/Width request & layout options are not needed Webview by default will capture the whole viewport, You could actually even remove the StackLayouts, But that's on you.
Also, you might wanna read more about the webview
Good luck
Feel free to get back if you have queries
You can use latest WkWebViewRenderer:
public class MyCustomWebViewRenderer : WkWebViewRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
//this.LoadUrl("https://learn.microsoft.com/en-us/xamarin/essentials/device-display?tabs=android");
this.NavigationDelegate = new WebViewDelegate();
}
}
In your code behind, you can directly set the source or set your binding:
dashboard.Source = "https://learn.microsoft.com/en-us/xamarin/essentials/device-display?tabs=android";
Also, start from xamarin.forms 4.5+, xamarin use WKWebview as the default control in iOS and that means you no longer need a custom renderer if you use xamarin.forms 4.5+. Refer:
UIWebView Deprecation and App Store Rejection (ITMS-90809)
I was facing that issue just beacuse i was using custom renderer.
My solution code is as follows :
Xaml Code :
<ContentPage.Content>
<StackLayout VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand">
<WebView x:Name="dashboard" HeightRequest="1000" WidthRequest="1000"/>
</StackLayout>
</ContentPage.Content>
Code Behind :
public partial class DashboardView : ContentPage
{
public DashboardView()
{
InitializeComponent();
dashboard.Source = "url";
}
}
Authentication Renderer iOS :
[assembly: ExportRenderer(typeof(WebView), typeof(Report.iOS.WebViewRenderer))]
namespace Report.iOS
{
class WebViewRenderer : WkWebViewRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
this.NavigationDelegate = new WebViewDelegate();
}
}
public class WebViewDelegate : WKNavigationDelegate, INSUrlConnectionDataDelegate
{
string uname = null;
string pass = null;
public override async void DidReceiveAuthenticationChallenge(WKWebView webView, NSUrlAuthenticationChallenge challenge, Action<NSUrlSessionAuthChallengeDisposition, NSUrlCredential> completionHandler)
{
try
{
uname = Xamarin.Forms.Application.Current.Properties.ContainsKey("Username") ? Convert.ToString(Xamarin.Forms.Application.Current.Properties["Username"]) : null;
pass = await SecureStorage.GetAsync("Password");
}
catch (Exception ex)
{
}
completionHandler(NSUrlSessionAuthChallengeDisposition.UseCredential, new NSUrlCredential(uname, pass, NSUrlCredentialPersistence.ForSession));
return;
}
}
}
Authentication Renderer Android :
[assembly: ExportRenderer(typeof(WebView), typeof(AuthWebViewRenderer))]
namespace Report.Droid
{
public class AuthWebViewRenderer : Xamarin.Forms.Platform.Android.WebViewRenderer
{
AuthWebViewClient _authWebClient = null;
public AuthWebViewRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<WebView> e)
{
base.OnElementChanged(e);
if (_authWebClient == null)
{
_authWebClient = new AuthWebViewClient();
}
Control.SetWebViewClient(_authWebClient);
}
}
public class AuthWebViewClient : WebViewClient
{
public AuthWebViewClient()
{
}
public override async void OnReceivedHttpAuthRequest(global::Android.Webkit.WebView view, HttpAuthHandler handler, string host, string realm)
{
string uname = null;
string pass = null;
try
{
uname = Application.Current.Properties.ContainsKey("Username") ? Convert.ToString(Application.Current.Properties["Username"]) : null;
pass = await SecureStorage.GetAsync("Password");
}
catch (Exception ex)
{
Log.Error("Apprise :", "Error Occurred while getting login credentials " + ex);
}
handler.Proceed(uname, pass);
}
}
}

Returning WebView from ContentView in Xamarin

I am trying to use a custom ContentView inside another page of my Xamarin Application. This custom ContentView is used for displaying HTML content (specifically gifs). When trying to load the ContentView from another page, it throws an error : "Value cannot be null. Parameter name thisActivity"
My code is based off the examples in here.
Am I missing a key step in getting a WebView to render via a custom ContentView?
//This is my Custom ContentView
public class GifView : ContentView
{
public GifView()
{
this.BuildPage();
}
public WebView BuildWebView()
{
var htmlSource = new HtmlWebViewSource
{
Html = #"<html><body><h1>This is a test</h1></body></html>"
};
WebView webView = new WebView
{
WidthRequest = 1000,
HeightRequest = 1000,
Source = htmlSource
};
return webView;
}
public void BuildPage()
{
var webView = this.BuildWebView();
this.Content = new StackLayout
{
Children =
{
webView
}
};
}
}
// This is the XAML where I am trying to use the custom view.
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:myproject"
x:Class="myproject.MainPage"
xmlns:views="clr-namespace:myproject.Views">
<views:GifView />
</ContentPage>
OK this is a very silly error. The solution deploys fine and works on an Android phone. I guess the Design feature of Visual Studio is bugged and is displaying an incorrect error.

How to open file .doc use "Word" app on Xamarin Forms?

Use Xamarin Forms and on iOS, how to check and open Word app when Open file by UrL?
Reference: https://learn.microsoft.com/en-us/office/client-developer/integration/integrate-with-office-from-ios-applications
This is my code, it isn't work:
Device.OpenUri(new Uri("ms-word:ofe|u|https://calibre-ebook.com/downloads/demos/demo.docx"));
Please help me!
Thanks!
What you are asking here can be done using a simple WebView in iOS:
First, create a custom WebView class that allows you to pick file uri:
public class CustomWebView : WebView
{
public static readonly BindableProperty UriProperty = BindableProperty.Create(propertyName: "Uri",
returnType: typeof(string),
declaringType: typeof(CustomWebView),
defaultValue: default(string));
public string Uri
{
get { return (string)GetValue(UriProperty); }
set { SetValue(UriProperty, value); }
}
}
Then in ios make a renderer for the same and do something like this:
[assembly: ExportRenderer (typeof(CustomWebView), typeof(CustomWebViewRenderer))]
namespace DisplayPDF.iOS
{
public class CustomWebViewRenderer : ViewRenderer<CustomWebView, UIWebView>
{
protected override void OnElementChanged (ElementChangedEventArgs<CustomWebView> e)
{
base.OnElementChanged (e);
if (Control == null) {
SetNativeControl (new UIWebView ());
}
if (e.OldElement != null) {
// Cleanup
}
if (e.NewElement != null) {
var customWebView = Element as CustomWebView;
string fileName = Path.Combine (NSBundle.MainBundle.BundlePath, string.Format ("Content/{0}", WebUtility.UrlEncode (customWebView.Uri)));
Control.LoadRequest (new NSUrlRequest (new NSUrl (fileName, false)));
Control.ScalesPageToFit = true;
}
}
}
}
Then use the custom control like this:
<local:CustomWebView Uri="FooPath" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand" />
Where FooPath is the path for the doc file.
I have solution for my project, I try it:
If you only open file .docx on iOS, you could write code at Share Code of iOS:
Check device of user has setup app (Word, Excel, PP, etc...)
public static bool HasSetupAppDocument(string extension)
{
if (string.IsNullOrEmpty(extension))
return false;
// Device has setup app?
var result = UIApplication.SharedApplication.CanOpenUrl(NSUrl.FromString($"{extension}"));
return result;
}
(Ex: extension is ms-word: or ms-excel: or ms-excel:
Reference: https://learn.microsoft.com/en-us/office/client-developer/integration/integrate-with-office-from-ios-applications#office-protocols)
Notes: Add source to Info.plist:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>ms-word</string>
<string>ms-excel</string>
<string>ms-powerpoint</string>
</array>
At Class Dependencies, open file with URL:
Device.OpenUri(new Uri($"{convertExtension}{url}"));
Note: url is link file share on Onedrive and Be sure that account Onedrive as same as account login Word app (if you had set security).
If file has mode Read-only, app Word will open file with mode Read-only.

Xamarin Forms - finding databound object from custom renderer while responding to a swipe

From an iOS swipe event, I am trying to figure out how to work my way back to the model databound to the ViewCell (model is my own Drive object, a simple POCO).
I am using a subclassed StackLayout ...
public class MainPageStackLayout : StackLayout { }
with a custom renderer...
[assembly: ExportRenderer(typeof(MainPageStackLayout), typeof(MainPageStackLayoutRenderer))]
namespace DriveLive.iOS
{
public class MainPageStackLayoutRenderer : VisualElementRenderer<StackLayout>
{
UISwipeGestureRecognizer swipeGestureRecognizer;
protected override void OnElementChanged(ElementChangedEventArgs<StackLayout> e)
{
base.OnElementChanged(e);
swipeGestureRecognizer = new UISwipeGestureRecognizer(() =>
{
//********************
Console.WriteLine("How to access the underlying model here?");
//********************
}) { Direction = UISwipeGestureRecognizerDirection.Left, NumberOfTouchesRequired = 1 };
if (e.NewElement == null)
{
if (swipeGestureRecognizer != null)
this.RemoveGestureRecognizer(swipeGestureRecognizer);
}
if (e.OldElement == null)
{
this.AddGestureRecognizer(swipeGestureRecognizer);
}
}
}
}
and the code that uses the MainPageStackLayout ...
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:DriveLive"
x:Class="DriveLive.Views.MainPage">
<ListView x:Name="___drives" HasUnevenRows="True">
<ListView.ItemTemplate />
</ListView>
</ContentPage>
C#
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
IDriveRespository repo = new DriveLive.Repository.Fakes.DriveRespository();
___drives.ItemsSource = repo.GetDrivesForUser(45); // returns a Drive list of objects
___drives.ItemTemplate = new DataTemplate(typeof(CustomViewCell));
___drives.SeparatorColor = Color.FromHex("#81C1B5");
}
}
public class CustomViewCell : ViewCell
{
bool _initialized = false;
StackLayout _cellStack;
public CustomViewCell()
{
_cellStack = new MainPageStackLayout()
{
Orientation = StackOrientation.Vertical,
HorizontalOptions = LayoutOptions.FillAndExpand
};
View = _cellStack;
var label = new Label() { FontAttributes = FontAttributes.Bold };
label.SetBinding(Label.TextProperty, new Binding("DestinationName"));
_cellStack.Children.Add(label);
}
}
From the handler for the UISwipeGestureRecognizer, how can I access the underlying Drive object which is databound to the ViewCell?
My issue is resolved by leveraging this piece of XForms.
ListView Interactivity - Context Actions
Credit goes to #skar's comment for pointing me in the right direction.

Resources