I'm trying to port an app from Swift (iOS only) to C# in Visual Studio - and it's going (slightly) well. I'm having some Android troubles though (many of them - but only one for this question!)
The page loads correctly in the webview of the Android version of the app - but the Javascript doesn't execute until after the page has rendered, the result being that the App Store advert is displayed briefly before it disappears.
My iOS app works correctly - the source code for the iOS version is here:
public class PortalViewRenderer : ViewRenderer<PortalView, WKWebView>, IWKScriptMessageHandler, IWKNavigationDelegate {
private class NavigationDelegate : WKNavigationDelegate {
private readonly WeakReference<PortalViewRenderer> _webView;
public NavigationDelegate(PortalViewRenderer webView) {
_webView = new WeakReference<PortalViewRenderer>(webView);
}
public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation) {
}
public override void DidStartProvisionalNavigation(WKWebView webView, WKNavigation navigation) {
NSUrl currentURL = webView.Url;
var current = Connectivity.NetworkAccess;
if (current != NetworkAccess.Internet) {
if (!(currentURL.AbsoluteString.Contains("file://"))) {
string noConnectionPath = Path.Combine(NSBundle.MainBundle.BundlePath, "Common/NoInternet.html");
webView.LoadRequest(new NSUrlRequest(NSUrl.FromString(noConnectionPath)));
}
}
}
public override void DidFailNavigation(WKWebView webView, WKNavigation navigation, NSError error) {
}
public override void DecidePolicy(WKWebView webView, WKNavigationAction navigationAction, Action<WKNavigationActionPolicy> decisionHandler) {
NSUrl url = navigationAction.Request.Url;
if (url != null) {
if (url.Host == "<website url>" || url.AbsoluteString.Contains("file://")) {
decisionHandler(WKNavigationActionPolicy.Allow);
} else if (url.AbsoluteString.Contains("<website url>/mydavylamp/timeout")) {
decisionHandler(WKNavigationActionPolicy.Cancel);
webView.LoadRequest(new NSUrlRequest(NSUrl.FromString(Element.Uri)));
} else {
UIApplication.SharedApplication.OpenUrl(url);
}
}
}
}
WKUserContentController userController;
protected override void OnElementChanged (ElementChangedEventArgs<PortalView> e) {
base.OnElementChanged (e);
var javaScriptFunction = System.IO.File.ReadAllText("Common/HideAppStoreAds.js");
if (Control == null) {
userController = new WKUserContentController();
var script = new WKUserScript(new NSString(javaScriptFunction), WKUserScriptInjectionTime.AtDocumentStart, false);
userController.AddUserScript(script);
userController.AddScriptMessageHandler(this, "invokeAction");
var config = new WKWebViewConfiguration { UserContentController = userController };
var webView = new WKWebView(Frame, config);
webView.BackgroundColor = UIKit.UIColor.FromRGB(0x11, 0x25, 0x43);
webView.ScrollView.BackgroundColor = webView.BackgroundColor;
webView.CustomUserAgent = "headbanger.davylamp.ios";
webView.ScrollView.ScrollEnabled = true;
webView.ScrollView.Bounces = false;
webView.AllowsBackForwardNavigationGestures = false;
webView.ContentMode = UIKit.UIViewContentMode.ScaleToFill;
webView.NavigationDelegate = new NavigationDelegate(this);
SetNativeControl(webView);
}
if (e.OldElement != null) {
userController.RemoveAllUserScripts();
userController.RemoveScriptMessageHandler("invokeAction");
var portalView = e.OldElement as PortalView;
portalView.Cleanup();
}
if (e.NewElement != null) {
Control.LoadRequest(new NSUrlRequest(NSUrl.FromString(Element.Uri)));
}
}
public void DidReceiveScriptMessage (WKUserContentController userContentController, WKScriptMessage message) {
Element.InvokeAction (message.Body.ToString ());
}
}
The Android version is as follows (it doesn't do so much yet, because I haven't worked out how to set policies etc. yet) but my main concern is that I can't get the Javascript to run at the correct time (as set on the iOS version using WKUserScriptInjectionTime which doesn't seem to have an Android equivalent)
public class JavascriptWebViewClient : WebViewClient {
string _javascript;
public JavascriptWebViewClient(string javascript) {
_javascript = javascript;
}
public override void OnPageFinished(WebView view, string url) {
base.OnPageFinished(view, url);
view.EvaluateJavascript(_javascript, null);
}
}
public class PortalViewRenderer : ViewRenderer<PortalView, Android.Webkit.WebView> {
Context _context;
public PortalViewRenderer(Context context) : base(context) {
_context = context;
}
protected override void OnElementChanged(ElementChangedEventArgs<PortalView> e) {
string javascriptFunction;
Android.Content.Res.AssetManager assets = _context.Assets;
using (StreamReader sr = new StreamReader(assets.Open("Common/HideAppStoreAds.js"))) {
javascriptFunction = sr.ReadToEnd();
}
base.OnElementChanged(e);
if (Control == null) {
var webView = new Android.Webkit.WebView(_context);
webView.Settings.JavaScriptEnabled = true;
webView.SetWebViewClient(new JavascriptWebViewClient($"javascript: {javascriptFunction}"));
SetNativeControl(webView);
}
if (e.OldElement != null) {
Control.RemoveJavascriptInterface("jsBridge");
var portalView = e.OldElement as PortalView;
portalView.Cleanup();
}
if (e.NewElement != null) {
Control.LoadUrl($"{Element.Uri}");
}
}
}
The (common) Javascript is as follows:
var styleTag = document.createElement("style");
styleTag.textContent = '.mobile-apps {display:none;}';
document.documentElement.appendChild(styleTag);
I've googled for the magic spell, but I can't seem to find any guides on how to build a webview for Android in C# - and particularly not for iOS developers!
As always, any help that anyone can provide will be gratefully received.
The answer, for my use case, seems to be as follows.
The Android documentation (https://developer.android.com/reference/android/webkit/WebViewClient) for onPageCommitVisible says that:
This callback can be used to determine the point at which it is safe to make a recycled WebView visible, ensuring that no stale content is shown. It is called at the earliest point at which it can be guaranteed that WebView#onDraw will no longer draw any content from previous navigations. The next draw will display either the WebView#setBackgroundColor of the WebView, or some of the contents of the newly loaded page.
To my mind, this means that the HTML has loaded (although not necessarily any other resources) and the page might start to be rendered (although, crucially, it won't be rendered until this callback completes.)
I used the following code:
public override void OnPageCommitVisible(WebView view, string url) {
view.EvaluateJavascript(_javascript, null);
base.OnPageCommitVisible(view, url);
}
and it seems to work correctly. I hope that this helps anyone else.
You need just to override OnPageCommitVisible method:
public override void OnPageCommitVisible(AWebView view, string url)
{
//Insert javascript injection first
view.EvaluateJavascript(_javascript, null);
base.OnPageCommitVisible(view, url);
}
It fires Certainly.
Have a good code!
Related
I am migrating our app from Xamarin to MAUI, and I am a bit struggling with migrating the code that handles JS/.NET interactions in a WebView on both Android and iOS. Let's focus on Android. It's especially about calling .NET code from JS in the WebView.
In Xamarin, we could do something like this (basically according to this tutorial https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/custom-renderer/hybridwebview):
protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
Control.RemoveJavascriptInterface("jsBridge");
}
if (e.NewElement != null)
{
Control.SetWebViewClient(new JavascriptWebViewClient(this, $"javascript: {JavascriptFunction}"));
Control.AddJavascriptInterface(new JsBridge(this), "jsBridge");
}
}
and
public class JavascriptWebViewClient : FormsWebViewClient
{
private readonly string javascript;
public JavascriptWebViewClient(HybridWebViewRenderer renderer, string javascript) : base(renderer)
{
this.javascript = javascript;
}
public override void OnPageFinished(WebView view, string url)
{
base.OnPageFinished(view, url);
view.EvaluateJavascript(javascript, null);
}
}
In .NET 6 with MAUI, this is deprecated. I tried to build it with handlers, but then the OnPageFinished is never called. The lack of examples is making it difficult to figure out what I miss.
Microsoft.Maui.Handlers.WebViewHandler.Mapper.AppendToMapping("MyCustomization", (handler, view) =>
{
#if ANDROID
handler.PlatformView.SetWebViewClient(new JavascriptWebViewClient($"javascript: {JavascriptFunction}"));
handler.PlatformView.AddJavascriptInterface(new JsBridge(this), "jsBridge");
#endif
});
with
public class JavascriptWebViewClient : WebViewClient
{
private readonly string javascript;
public JavascriptWebViewClient(string javascript) : base()
{
this.javascript = javascript;
}
public override void OnPageFinished(WebView view, string url)
{
base.OnPageFinished(view, url);
view.EvaluateJavascript(javascript, null);
}
}
Where should I put this code? Is this the correct way? What am I missing? I now put this in a subclassed WebView, but probably that's not the right way.
Update: I developed a work-around for Windows. See below.
TL;DR -
https://github.com/nmoschkin/MAUIWebViewExample
I have come up with a MAUI solution that work for both iOS and Android, using the new Handler pattern as described in:
Porting Custom Renderers To Handlers
The above documentation was somewhat poor, and did not feature an implementation for the iOS version. I provide that, here.
This adaptation also makes the Source property a BindableProperty. Unlike the example in the above link, I do not actually add the property to the PropertyMapper in the platform handler in the traditional way. Rather, we will be listening for an event to be fired by the property changed notification method of the bindable property.
This example implements a 100% custom WebView. If there are additional properties and methods you would like to port over from the native components, you will have to add that additional functionality, yourself.
Shared Code:
In the shared code file, you want to create your custom view by implementing the classes and interface as described in the above link in the following way (with additional classes provided for events that we will provide to the consumer):
public class SourceChangedEventArgs : EventArgs
{
public WebViewSource Source
{
get;
private set;
}
public SourceChangedEventArgs(WebViewSource source)
{
Source = source;
}
}
public class JavaScriptActionEventArgs : EventArgs
{
public string Payload { get; private set; }
public JavaScriptActionEventArgs(string payload)
{
Payload = payload;
}
}
public interface IHybridWebView : IView
{
event EventHandler<SourceChangedEventArgs> SourceChanged;
event EventHandler<JavaScriptActionEventArgs> JavaScriptAction;
void Refresh();
WebViewSource Source { get; set; }
void Cleanup();
void InvokeAction(string data);
}
public class HybridWebView : View, IHybridWebView
{
public event EventHandler<SourceChangedEventArgs> SourceChanged;
public event EventHandler<JavaScriptActionEventArgs> JavaScriptAction;
public HybridWebView()
{
}
public void Refresh()
{
if (Source == null) return;
var s = Source;
Source = null;
Source = s;
}
public WebViewSource Source
{
get { return (WebViewSource)GetValue(SourceProperty); }
set { SetValue(SourceProperty, value); }
}
public static readonly BindableProperty SourceProperty = BindableProperty.Create(
propertyName: "Source",
returnType: typeof(WebViewSource),
declaringType: typeof(HybridWebView),
defaultValue: new UrlWebViewSource() { Url = "about:blank" },
propertyChanged: OnSourceChanged);
private static void OnSourceChanged(BindableObject bindable, object oldValue, object newValue)
{
var view = bindable as HybridWebView;
bindable.Dispatcher.Dispatch(() =>
{
view.SourceChanged?.Invoke(view, new SourceChangedEventArgs(newValue as WebViewSource));
});
}
public void Cleanup()
{
JavaScriptAction = null;
}
public void InvokeAction(string data)
{
JavaScriptAction?.Invoke(this, new JavaScriptActionEventArgs(data));
}
}
Then you would have to declare the handler for each platform, as follows:
Android Implementation:
public class HybridWebViewHandler : ViewHandler<IHybridWebView, Android.Webkit.WebView>
{
public static PropertyMapper<IHybridWebView, HybridWebViewHandler> HybridWebViewMapper = new PropertyMapper<IHybridWebView, HybridWebViewHandler>(ViewHandler.ViewMapper);
const string JavascriptFunction = "function invokeCSharpAction(data){jsBridge.invokeAction(data);}";
private JSBridge jsBridgeHandler;
public HybridWebViewHandler() : base(HybridWebViewMapper)
{
}
private void VirtualView_SourceChanged(object sender, SourceChangedEventArgs e)
{
LoadSource(e.Source, PlatformView);
}
protected override Android.Webkit.WebView CreatePlatformView()
{
var webView = new Android.Webkit.WebView(Context);
jsBridgeHandler = new JSBridge(this);
webView.Settings.JavaScriptEnabled = true;
webView.SetWebViewClient(new JavascriptWebViewClient($"javascript: {JavascriptFunction}"));
webView.AddJavascriptInterface(jsBridgeHandler, "jsBridge");
return webView;
}
protected override void ConnectHandler(Android.Webkit.WebView platformView)
{
base.ConnectHandler(platformView);
if (VirtualView.Source != null)
{
LoadSource(VirtualView.Source, PlatformView);
}
VirtualView.SourceChanged += VirtualView_SourceChanged;
}
protected override void DisconnectHandler(Android.Webkit.WebView platformView)
{
base.DisconnectHandler(platformView);
VirtualView.SourceChanged -= VirtualView_SourceChanged;
VirtualView.Cleanup();
jsBridgeHandler?.Dispose();
jsBridgeHandler = null;
}
private static void LoadSource(WebViewSource source, Android.Webkit.WebView control)
{
try
{
if (source is HtmlWebViewSource html)
{
control.LoadDataWithBaseURL(html.BaseUrl, html.Html, null, "charset=UTF-8", null);
}
else if (source is UrlWebViewSource url)
{
control.LoadUrl(url.Url);
}
}
catch { }
}
}
public class JavascriptWebViewClient : WebViewClient
{
string _javascript;
public JavascriptWebViewClient(string javascript)
{
_javascript = javascript;
}
public override void OnPageStarted(Android.Webkit.WebView view, string url, Bitmap favicon)
{
base.OnPageStarted(view, url, favicon);
view.EvaluateJavascript(_javascript, null);
}
}
public class JSBridge : Java.Lang.Object
{
readonly WeakReference<HybridWebViewHandler> hybridWebViewRenderer;
internal JSBridge(HybridWebViewHandler hybridRenderer)
{
hybridWebViewRenderer = new WeakReference<HybridWebViewHandler>(hybridRenderer);
}
[JavascriptInterface]
[Export("invokeAction")]
public void InvokeAction(string data)
{
HybridWebViewHandler hybridRenderer;
if (hybridWebViewRenderer != null && hybridWebViewRenderer.TryGetTarget(out hybridRenderer))
{
hybridRenderer.VirtualView.InvokeAction(data);
}
}
}
iOS Implementation:
public class HybridWebViewHandler : ViewHandler<IHybridWebView, WKWebView>
{
public static PropertyMapper<IHybridWebView, HybridWebViewHandler> HybridWebViewMapper = new PropertyMapper<IHybridWebView, HybridWebViewHandler>(ViewHandler.ViewMapper);
const string JavaScriptFunction = "function invokeCSharpAction(data){window.webkit.messageHandlers.invokeAction.postMessage(data);}";
private WKUserContentController userController;
private JSBridge jsBridgeHandler;
public HybridWebViewHandler() : base(HybridWebViewMapper)
{
}
private void VirtualView_SourceChanged(object sender, SourceChangedEventArgs e)
{
LoadSource(e.Source, PlatformView);
}
protected override WKWebView CreatePlatformView()
{
jsBridgeHandler = new JSBridge(this);
userController = new WKUserContentController();
var script = new WKUserScript(new NSString(JavaScriptFunction), WKUserScriptInjectionTime.AtDocumentEnd, false);
userController.AddUserScript(script);
userController.AddScriptMessageHandler(jsBridgeHandler, "invokeAction");
var config = new WKWebViewConfiguration { UserContentController = userController };
var webView = new WKWebView(CGRect.Empty, config);
return webView;
}
protected override void ConnectHandler(WKWebView platformView)
{
base.ConnectHandler(platformView);
if (VirtualView.Source != null)
{
LoadSource(VirtualView.Source, PlatformView);
}
VirtualView.SourceChanged += VirtualView_SourceChanged;
}
protected override void DisconnectHandler(WKWebView platformView)
{
base.DisconnectHandler(platformView);
VirtualView.SourceChanged -= VirtualView_SourceChanged;
userController.RemoveAllUserScripts();
userController.RemoveScriptMessageHandler("invokeAction");
jsBridgeHandler?.Dispose();
jsBridgeHandler = null;
}
private static void LoadSource(WebViewSource source, WKWebView control)
{
if (source is HtmlWebViewSource html)
{
control.LoadHtmlString(html.Html, new NSUrl(html.BaseUrl ?? "http://localhost", true));
}
else if (source is UrlWebViewSource url)
{
control.LoadRequest(new NSUrlRequest(new NSUrl(url.Url)));
}
}
}
public class JSBridge : NSObject, IWKScriptMessageHandler
{
readonly WeakReference<HybridWebViewHandler> hybridWebViewRenderer;
internal JSBridge(HybridWebViewHandler hybridRenderer)
{
hybridWebViewRenderer = new WeakReference<HybridWebViewHandler>(hybridRenderer);
}
public void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
{
HybridWebViewHandler hybridRenderer;
if (hybridWebViewRenderer.TryGetTarget(out hybridRenderer))
{
hybridRenderer.VirtualView?.InvokeAction(message.Body.ToString());
}
}
}
As you can see, I'm listening for the event to change out the source, which will then perform the platform-specific steps necessary to change it.
Also note that in both implementations of JSBridge I am using a WeakReference to track the control. I am not certain of any situations where disposal might deadlock, but I did this out of an abundance of caution.
Windows Implementation:
So. According to various articles I read, the current WinUI3 iteration of WebView2 for MAUI is not yet allowing us to invoke AddHostObjectToScript. They plan this for a future release.
But, then I remembered it was Windows, so I created a work-around that most certainly emulates the same behavior and achieves the same result, with a somewhat unorthodox solution: by using an HttpListener.
internal class HybridSocket
{
private HttpListener listener;
private HybridWebViewHandler handler;
bool token = false;
public HybridSocket(HybridWebViewHandler handler)
{
this.handler = handler;
CreateSocket();
}
private void CreateSocket()
{
listener = new HttpListener();
listener.Prefixes.Add("http://localhost:32000/");
}
public void StopListening()
{
token = false;
}
private void SendToNative(string json)
{
handler.VirtualView.InvokeAction(json);
}
public void Listen()
{
var s = listener;
try
{
token = true;
s.Start();
while (token)
{
HttpListenerContext ctx = listener.GetContext();
using HttpListenerResponse resp = ctx.Response;
resp.AddHeader("Access-Control-Allow-Origin", "null");
resp.AddHeader("Access-Control-Allow-Headers", "content-type");
var req = ctx.Request;
Stream body = req.InputStream;
Encoding encoding = req.ContentEncoding;
using (StreamReader reader = new StreamReader(body, encoding))
{
var json = reader.ReadToEnd();
if (ctx.Request.HttpMethod == "POST")
{
SendToNative(json);
}
}
resp.StatusCode = (int)HttpStatusCode.OK;
resp.StatusDescription = "Status OK";
}
CreateSocket();
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
}
}
}
public class HybridWebViewHandler : ViewHandler<IHybridWebView, WebView2>
{
public static PropertyMapper<IHybridWebView, HybridWebViewHandler> HybridWebViewMapper = new PropertyMapper<IHybridWebView, HybridWebViewHandler>(ViewHandler.ViewMapper);
const string JavascriptFunction = #"function invokeCSharpAction(data)
{
var http = new XMLHttpRequest();
var url = 'http://localhost:32000';
http.open('POST', url, true);
http.setRequestHeader('Content-type', 'application/json');
http.send(JSON.stringify(data));
}";
static SynchronizationContext sync;
private HybridSocket jssocket;
public HybridWebViewHandler() : base(HybridWebViewMapper)
{
sync = SynchronizationContext.Current;
jssocket = new HybridSocket(this);
Task.Run(() => jssocket.Listen());
}
~HybridWebViewHandler()
{
jssocket.StopListening();
}
private void OnWebSourceChanged(object sender, SourceChangedEventArgs e)
{
LoadSource(e.Source, PlatformView);
}
protected override WebView2 CreatePlatformView()
{
sync = sync ?? SynchronizationContext.Current;
var webView = new WebView2();
webView.NavigationCompleted += WebView_NavigationCompleted;
return webView;
}
private void WebView_NavigationCompleted(WebView2 sender, CoreWebView2NavigationCompletedEventArgs args)
{
var req = new EvaluateJavaScriptAsyncRequest(JavascriptFunction);
PlatformView.EvaluateJavaScript(req);
}
protected override void ConnectHandler(WebView2 platformView)
{
base.ConnectHandler(platformView);
if (VirtualView.Source != null)
{
LoadSource(VirtualView.Source, PlatformView);
}
VirtualView.SourceChanged += OnWebSourceChanged;
}
protected override void DisconnectHandler(WebView2 platformView)
{
base.DisconnectHandler(platformView);
VirtualView.SourceChanged -= OnWebSourceChanged;
VirtualView.Cleanup();
}
private static void LoadSource(WebViewSource source, WebView2 control)
{
try
{
if (control.CoreWebView2 == null)
{
control.EnsureCoreWebView2Async().AsTask().ContinueWith((t) =>
{
sync.Post((o) => LoadSource(source, control), null);
});
}
else
{
if (source is HtmlWebViewSource html)
{
control.CoreWebView2.NavigateToString(html.Html);
}
else if (source is UrlWebViewSource url)
{
control.CoreWebView2.Navigate(url.Url);
}
}
}
catch { }
}
}
Finally, you will need to initialize the MAUI application by adding ConfigureMauiHandlers to the app builder:
Initialize the MAUI Application in MauiProgram.cs
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
})
.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler(typeof(HybridWebView), typeof(HybridWebViewHandler));
});
return builder.Build();
}
Add The Control To XAML
<controls:HybridWebView
x:Name="MyWebView"
HeightRequest="128"
HorizontalOptions="Fill"
Source="{Binding Source}"
VerticalOptions="FillAndExpand"
WidthRequest="512"
/>
Finally, I have added all of the above to a full example MAUI project in a repository on GitHub:
https://github.com/nmoschkin/MAUIWebViewExample
The GitHub repo example also includes a ViewModel that contains the WebViewSource to which the control is bound in markup.
OK, figured it out. Adding information for those looking to the same problem.
What you need to do:
Override WebView client.
Generic:
public partial class CustomWebView : WebView
{
partial void ChangedHandler(object sender);
partial void ChangingHandler(object sender, HandlerChangingEventArgs e);
protected override void OnHandlerChanging(HandlerChangingEventArgs args)
{
base.OnHandlerChanging(args);
ChangingHandler(this, args);
}
protected override void OnHandlerChanged()
{
base.OnHandlerChanged();
ChangedHandler(this);
}
public void InvokeAction(string data)
{
// your custom code
}
}
Android:
public partial class CustomWebView
{
const string JavascriptFunction = "function invokeActionInCS(data){jsBridge.invokeAction(data);}";
partial void ChangedHandler(object sender)
{
if (sender is not WebView { Handler: { PlatformView: Android.Webkit.WebView nativeWebView } }) return;
nativeWebView.SetWebViewClient(new JavascriptWebViewClient($"javascript: {JavascriptFunction}"));
nativeWebView.AddJavascriptInterface(new JsBridge(this), "jsBridge");
}
partial void ChangingHandler(object sender, HandlerChangingEventArgs e)
{
if (e.OldHandler != null)
{
if (sender is not WebView { Handler: { PlatformView: Android.Webkit.WebView nativeWebView } }) return;
nativeWebView.RemoveJavascriptInterface("jsBridge");
}
}
}
Add this custom view to your XAML
<views:CustomWebView x:Name="CustomWebViewName"/>
Modify the JS Bridge
public class JsBridge : Java.Lang.Object
{
private readonly HarmonyWebView webView;
public JsBridge(HarmonyWebView webView)
{
this.webView = webView;
}
[JavascriptInterface]
[Export("invokeAction")]
public void InvokeAction(string data)
{
webView.InvokeAction(data);
}
}
I solved it finnaly. It's not completely solution however its work well.
First, we define 2 variables globally on the javascipt side and define 2 functions waiting for each other.
var _flagformobiledata = false;
var _dataformobiledata = "";
const waitUntilMobileData = (condition, checkInterval = 100) => {
return new Promise(resolve => {
let interval = setInterval(() => {
if (!condition()) return;
clearInterval(interval);
resolve();
}, checkInterval)
})
}
async function mobileajax(functionName, params) {
window.location.href = "https://runcsharp." + functionName + "?" + params;
await waitUntilMobileData(() => _flagformobiledata == true);
_flagformobiledata = false;
return _dataformobiledata;
}
function setmobiledata(aData) {
_dataformobiledata = aData;
_flagformobiledata = true;
}
Then in MainPage.xaml.cs file define a function named WebViewNavigation
private async void WebViewNavigation(object sender, WebNavigatingEventArgs e)
{
var urlParts = e.Url.Split("runcsharp.");
if (urlParts.Length == 2)
{
Console.WriteLine(urlParts);
var funcToCall = urlParts[1].Split("?");
var methodName = funcToCall[0];
var funcParams = funcToCall[1];
e.Cancel = true;
if (methodName.Contains("login"))
{
var phonenumber = funcParams.Split("=");
var ActualPhoneNumber = "";
if (phonenumber.Length == 2)
{
ActualPhoneNumber = Regex.Replace(phonenumber[1].Replace("%20", "").ToString(), #"[^\d]", "");
}
var response = _authService.GetMobileLicanceInfo(ActualPhoneNumber);
if (response.status == 200)
{
PhoneGlobal = ActualPhoneNumber;
string maui = "setmobiledata('" + "OK" + "')"; // this is function to set global return data
await CustomWebView.EvaluateJavaScriptAsync(maui); // then run the script
}
else
{
await DisplayAlert("Error", response.message, "OK");
}
}
}
}
Then your Mainpage.xaml file:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="DinamoMobile.MainPage">
<WebView Navigating="WebViewNavigation" x:Name="CustomWebView">
</WebView>
</ContentPage>
After all that coding usage must be like:
<script>const login = document.querySelector(".js-login-btnCls");
login.addEventListener("click", logincallapi);
async function logincallapi() {
var phone = $("#phone").val();
if (phone == "") {
alert("Phone is required");
return;
}
var isOK = await mobileajax("login", "phone=" + phone);
if (isOK == "OK") {
window.location.href = "verification.html";
}
else {
alert("Invalid Phone Number.");
}
}</script>
Algorithm:
Write an asynchronous function that waits for data on the javascript side.
Go with the URL of the desired function on the Javascript side.
Set data to global variable with EvulateJavascript function.
The function waiting for the data will continue in this way.
I want to develop Salesforce chatbot with xamarin forms application. I am not able to find what sdk or nuget package, I should use. There is no information seem to available regarding this.
This salesforce article, I have found, where they do it for Android and iOS.
I have tried it by using .jar files of android libraries but that doesn't work, for that iOS development also required separately. I have found this article but not sure what package and which platform they are developing.
How to do Salesforce Einstein Chatbot integration with Xamarin forms. Can anyone please help me regarding this?
This is how I managed to integrate Salesforce chatbot
public class SfChatbotViewModel : BaseViewModel
{
public SfChatbotViewModel()
{
}
}
<controls:HybridWebView Margin="0" x:Name="webView"
VerticalOptions="FillAndExpand"
HorizontalOptions="FillAndExpand" />
This is HybridWebView Webview control in Shared(common) project
public class HybridWebView : Xamarin.Forms.WebView
{
Action<string> action;
public static readonly BindableProperty UriProperty = BindableProperty.Create(
propertyName: "Uri",
returnType: typeof(string),
declaringType: typeof(HybridWebView),
defaultValue: default(string));
public string Uri
{
get { return (string)GetValue(UriProperty); }
set { SetValue(UriProperty, value); }
}
public void RegisterAction(Action<string> callback)
{
action = callback;
}
public void Cleanup()
{
action = null;
}
public void InvokeAction(string data)
{
if (action == null || data == null)
{
return;
}
action.Invoke(data);
}
}
Below `Webview renderer` in Android project
public class HybridWebViewRenderer : WebViewRenderer
{
const string JavascriptFunction = "function initESW(data){jsBridge.invokeAction(data);}";
Context _context;
public HybridWebViewRenderer(Context context) : base(context)
{
_context = context;
}
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
global::Android.Webkit.WebView.SetWebContentsDebuggingEnabled(true);
if (e.OldElement != null)
{
Control.RemoveJavascriptInterface("jsBridge");
((HybridWebView)Element).Cleanup();
}
if (e.NewElement != null)
{
CookieManager.Instance.SetAcceptCookie(true);
CookieManager.Instance.SetAcceptThirdPartyCookies(Control, true);
Control.SetWebViewClient(new JavascriptWebViewClient(this, $"javascript: {JavascriptFunction}"));
Control.AddJavascriptInterface(new JSBridge(this), "jsBridge");
Control.Settings.AllowFileAccess = true;
Control.Settings.JavaScriptEnabled = true;
Control.Settings.AllowFileAccessFromFileURLs = true;
Control.Settings.AllowUniversalAccessFromFileURLs = true;
Control.Settings.AllowContentAccess = true;
Control.Settings.DomStorageEnabled = true;
Control.SetWebChromeClient(new WebChromeClient());
var content_UAT = LoadData("ChatUAT.html");
Control.LoadDataWithBaseURL("https://service.force.com/embeddedservice/5.0/esw.min.js",
content_UAT,
"text/html;",
null,
"https://service.force.com/embeddedservice/5.0/esw.min.js");
}
}
public class JavascriptWebViewClient : FormsWebViewClient
{
string _javascript;
public JavascriptWebViewClient(HybridWebViewRenderer renderer, string javascript) : base(renderer)
{
_javascript = javascript;
}
public override void OnPageFinished(Android.Webkit.WebView view, string url)
{
base.OnPageFinished(view, url);
view.EvaluateJavascript("document.cookie", null);
}
public override void OnReceivedSslError(Android.Webkit.WebView view, SslErrorHandler handler, SslError error)
{
Console.WriteLine("Salesforce webview error: ", error);
base.OnReceivedSslError(view, handler, error);
}
public override void OnReceivedHttpError(Android.Webkit.WebView view, IWebResourceRequest request, WebResourceResponse errorResponse)
{
base.OnReceivedHttpError(view, request, errorResponse);
}
public override void OnLoadResource(Android.Webkit.WebView view, string url)
{
base.OnLoadResource(view, url);
}
}
public class JSBridge : Java.Lang.Object
{
readonly WeakReference<HybridWebViewRenderer> hybridWebViewRenderer;
public JSBridge(HybridWebViewRenderer hybridRenderer)
{
hybridWebViewRenderer = new WeakReference<HybridWebViewRenderer>(hybridRenderer);
}
[JavascriptInterface]
[Export("invokeAction")]
public void InvokeAction(string data)
{
HybridWebViewRenderer hybridRenderer;
if (hybridWebViewRenderer != null && hybridWebViewRenderer.TryGetTarget(out hybridRenderer))
{
((HybridWebView)hybridRenderer.Element).InvokeAction(data);
}
}
}
//this methods html file content from assets folder
public string LoadData(string inFile)
{
Stream stream = Android.App.Application.Context.Assets.Open(inFile);
StreamReader sr = new StreamReader(stream);
string text = sr.ReadToEnd();
sr.Close();
return text;
}
}
This is iOS renderer
[assembly: ExportRenderer(typeof(HybridWebView), typeof(HybridWebViewRenderer))]
namespace SfChatbot.iOS
{
public class HybridWebViewRenderer : WkWebViewRenderer, IWKScriptMessageHandler
{
const string JavascriptFunction = "function initESW(data){jsBridge.invokeAction(data);}";
//const string JavaScriptFunction = "function invokeCSharpAction(data){window.webkit.messageHandlers.invokeAction.postMessage(data);}";
WKUserContentController userController;
public HybridWebViewRenderer() : this(new WKWebViewConfiguration())
{
}
public HybridWebViewRenderer(WKWebViewConfiguration config) : base(config)
{
userController = config.UserContentController;
var script = new WKUserScript(new NSString(JavascriptFunction), WKUserScriptInjectionTime.AtDocumentEnd, false);
userController.AddUserScript(script);
userController.AddScriptMessageHandler(this, "invokeAction");
}
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
userController.RemoveAllUserScripts();
userController.RemoveScriptMessageHandler("invokeAction");
HybridWebView hybridWebView = e.OldElement as HybridWebView;
hybridWebView.Cleanup();
}
if (e.NewElement != null)
{
base.OnElementChanged(e);
var assembly = IntrospectionExtensions.GetTypeInfo(typeof(SfChatbot.iOS.HybridWebViewRenderer)).Assembly;
Stream stream = assembly.GetManifestResourceStream("SfChatbot.iOS.Resources.ChatUAT.html");
string content1 = "";
using (var reader = new System.IO.StreamReader(stream))
{
content1 = reader.ReadToEnd();
}
LoadSimulatedRequest(new NSUrlRequest( new NSUrl("https://service.force.com/embeddedservice/5.0/esw.min.js")), content1);
}
}
public void DidReceiveScriptMessage(WKUserContentController userContentController, WKScriptMessage message)
{
((HybridWebView)Element).InvokeAction(message.Body.ToString());
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
((HybridWebView)Element).Cleanup();
}
base.Dispose(disposing);
}
//not being used
public string LoadUATData()
{
var uri = new UriBuilder("https://run.mocky.io/v3/37c8-1f4501adeeff").Uri;
ServicePointManager.ServerCertificateValidationCallback = new
RemoteCertificateValidationCallback
(
delegate { return true; }
);
var client = new WebClient();
var content = client.DownloadString(uri);
//SMS UAT
var content1 = content.Replace("t1", "https.my.salesforce.com").
Replace("t2", "https://.cs81.force.com").
Replace("t3", "00D260009").
Replace("t4", "5260000004C").
Replace("t5", "573200004").
Replace("t6", "EmbeddedServiceLiveAgent_Parent04I260000000AIbca0f71c").
Replace("t7", "https://salesforce.com/embeddedservice/5.0/esw.min.js").
Replace("path", "src");
return content1;
}
}
Salesforce providing javacript file, we need to convert that into html format a nd keep in Assets and Resource folder respectivly for android and iOS. If any page is not having CSS applied(after executing, in output), you need to add Lightings CSS styles in head tag of html file.
If there is any issue with keyboard is is hiding screen while typing, take help from this. Get the format for Salesforce chat file, from this question.
Please assist
I have been struggling in showing a trip on my carpooling app much like the one on an Uber. That is with two info windows for both start and end locations open with their respective addresses.
I have managed to render a custom info window the way I want to but can not make all the info windows open. How can I do one of the following:
Override the current logic that prevents google Maps to open more than one info window at a time.
Draw over the map to display a custom view right next to the Map pin.
I am using 'Xamarin.Forms.GoogleMaps'
Visual goal(Only top part of screen)
Xamarin.Android Custom Render Class
[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))]
namespace CoTripApp.Droid.CustomRennders
{
public class CustomMapRenderer : MapRenderer, GoogleMap.IInfoWindowAdapter
{
protected override void OnElementChanged(ElementChangedEventArgs<Map> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
//nativemap.infowindowclick -= oninfowindowclick;
}
if (e.NewElement != null)
{
var formsmap = (CustomMap)e.NewElement;
}
}
protected override void OnMapReady(GoogleMap nativeMap, Xamarin.Forms.GoogleMaps.Map map)
{
base.OnMapReady(nativeMap, map);
NativeMap.SetInfoWindowAdapter(this);
}
protected override void OnMarkerCreated(Pin pin, Marker marker)
{
marker.Position = new LatLng(pin.Position.Latitude, pin.Position.Longitude);
marker.Title = pin.Label;
marker.Snippet = pin.Address;
if (pin.Type.Equals(PinType.Generic))
{
marker.SetIcon(Android.Gms.Maps.Model.BitmapDescriptorFactory.FromResource(Resource.Drawable.circle));
}
else
{
marker.SetIcon(Android.Gms.Maps.Model.BitmapDescriptorFactory.FromResource(Resource.Drawable.circle_closed));
}
marker.ShowInfoWindow();
}
public Android.Views.View GetInfoContents(Marker marker)
{
Android.Views.View view;
var inflater = Android.App.Application.Context.GetSystemService(Context.LayoutInflaterService) as Android.Views.LayoutInflater;
if (inflater != null)
{
view = inflater.Inflate(Resource.Layout.CustomMapInfoWindow, null);
view.FindViewById<TextView>(Resource.Id.waypointNo).Text = marker.Title;
view.FindViewById<TextView>(Resource.Id.address).Text = marker.Snippet;
return view;
}
return null;
}
public Android.Views.View GetInfoWindow(Marker marker)
{
return null;
}
}
}
I am developing an inventory management app in xamarin forms using Zebra TC20 model. Right now I am able to retrieve barcode data using datawedge. Now I am trying to disable and enable barcode scanner using datawedge as well. I have surfed and found out we can broadcast to datawedge for enable and disable the barcode scanner. Now I am having the issue when I want to enable or disable the the scanner from a page I am retrieving an error message Java.Lang.NullPointerException: 'Attempt to invoke virtual method 'void android.content.Context.sendBroadcast(android.content.Intent)' on a null object reference . The scanner functions are implemented in Android file. I am using dependency service to call the scanner functions. I have attached the code I have done.
MainActivity.cs
public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity, IKeyboardListener, IScannerConnection
{
private static string ACTION_DATAWEDGE_FROM_6_2 = "com.symbol.datawedge.api.ACTION";
private static string EXTRA_CREATE_PROFILE = "com.symbol.datawedge.api.CREATE_PROFILE";
private static string EXTRA_SET_CONFIG = "com.symbol.datawedge.api.SET_CONFIG";
private static string EXTRA_PROFILE_NAME = "Barcode Scan";
private DataWedgeReceiver _broadcastReceiver = null;
protected override void OnCreate(Bundle bundle)
{
base.Window.RequestFeature(WindowFeatures.ActionBar);
// Name of the MainActivity theme you had there before.
// Or you can use global::Android.Resource.Style.ThemeHoloLight
base.SetTheme(Resource.Style.MainTheme);
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
base.OnCreate(bundle);
DependencyService.Register<ToastNotification>(); // Register your dependency
ToastNotification.Init(this);
var container = new SimpleContainer(); // Create a SimpleCOntainer
container.Register<IGeolocator, Geolocator>(); // Register the Geolocator
container.Register<IDevice>(t => AndroidDevice.CurrentDevice); // Register the Device
Resolver.ResetResolver(); // Reset the resolver
Resolver.SetResolver(container.GetResolver()); // Resolve it
FFImageLoading.Forms.Platform.CachedImageRenderer.Init(enableFastRenderer: true);
UserDialogs.Init(this);
Rg.Plugins.Popup.Popup.Init(this, bundle);
global::Xamarin.Forms.Forms.Init(this, bundle);
global::ZXing.Net.Mobile.Forms.Android.Platform.Init();
_broadcastReceiver = new DataWedgeReceiver();
App inventoryApp = new App(OnSaveSignature);
_broadcastReceiver.scanDataReceived += (s, scanData) =>
{
MessagingCenter.Send<App, string>(inventoryApp, "ScanBarcode", scanData);
};
CreateProfile();
//DependencyService.Register<ToastNotification>();
//ToastNotification.Init(this, new PlatformOptions() { SmallIconDrawable = Android.Resource.Drawable.IcDialogInfo });
LoadApplication(inventoryApp);
}
public override void OnBackPressed()
{
if (Rg.Plugins.Popup.Popup.SendBackPressed(base.OnBackPressed))
{
// Do something if there are some pages in the `PopupStack`
}
else
{
// Do something if there are not any pages in the `PopupStack`
}
}
public override bool OnOptionsItemSelected(IMenuItem item)
{
// check if the current item id
// is equals to the back button id
if (item.ItemId == 16908332 && !RFIDStockCountViewModel.IsPosted && RFIDStockCountViewModel.IsStockCounting)
{
return false;
/*
if (Xamarin.Forms.Application.Current.MainPage.Navigation.NavigationStack.Count > 0)
{
//LIFO is the only game in town! - so send back the last page
int index = Xamarin.Forms.Application.Current.MainPage.Navigation.NavigationStack.Count - 1;
var currPage = (CustomContentPage)Xamarin.Forms.Application.Current.MainPage.Navigation.NavigationStack[index];
// check if the page has subscribed to
// the custom back button event
if (currPage?.CustomBackButtonAction != null)
{
// invoke the Custom back button action
currPage?.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);
}
}
private async Task<bool> OnSaveSignature(Stream bitmap, string filename)
{
var path = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryPictures).AbsolutePath;
var file = Path.Combine(path, "signature.png");
using (var dest = File.OpenWrite(file))
{
await bitmap.CopyToAsync(dest);
}
return true;
}
//public override void OnBackPressed()
//{
// if (Xamarin.Forms.Application.Current.MainPage.Navigation.NavigationStack.Count > 0)
// {
// //LIFO is the only game in town! - so send back the last page
// int index = Xamarin.Forms.Application.Current.MainPage.Navigation.NavigationStack.Count - 1;
// var currentpage = (CustomContentPage)Xamarin.Forms.Application.Current.MainPage.Navigation.NavigationStack[index];
// // check if the page has subscribed to
// // the custom back button event
// if (currentpage?.CustomBackButtonAction != null)
// {
// currentpage?.CustomBackButtonAction.Invoke();
// }
// else
// {
// base.OnBackPressed();
// }
// }
//}
public override bool OnKeyDown(Keycode keyCode, KeyEvent e)
{
if (e.KeyCode.GetHashCode() == 139 || e.KeyCode.GetHashCode() == 280)
{
MessagingCenter.Send<IKeyboardListener, string>(this, "KeyboardListener", "TRUE");
}
return base.OnKeyDown(keyCode, e);
}
public override bool OnKeyUp(Keycode keyCode, KeyEvent e)
{
if (e.KeyCode.GetHashCode() == 139 || e.KeyCode.GetHashCode() == 280)
{
MessagingCenter.Send<IKeyboardListener, string>(this, "KeyboardListener", "FALSE");
}
return base.OnKeyDown(keyCode, e);
}
protected override void OnResume()
{
base.OnResume();
if (null != _broadcastReceiver)
{
// Register the broadcast receiver
IntentFilter filter = new IntentFilter(DataWedgeReceiver.IntentAction);
filter.AddCategory(DataWedgeReceiver.IntentCategory);
Android.App.Application.Context.RegisterReceiver(_broadcastReceiver, filter);
}
}
protected override void OnPause()
{
if (null != _broadcastReceiver)
{
// Unregister the broadcast receiver
Android.App.Application.Context.UnregisterReceiver(_broadcastReceiver);
}
base.OnStop();
}
private void CreateProfile()
{
String profileName = EXTRA_PROFILE_NAME;
SendDataWedgeIntentWithExtra(ACTION_DATAWEDGE_FROM_6_2, EXTRA_CREATE_PROFILE, profileName);
// Now configure that created profile to apply to our application
Bundle profileConfig = new Bundle();
profileConfig.PutString("PROFILE_NAME", EXTRA_PROFILE_NAME);
profileConfig.PutString("PROFILE_ENABLED", "true"); // Seems these are all strings
profileConfig.PutString("CONFIG_MODE", "CREATE_IF_NOT_EXIST");
Bundle barcodeConfig = new Bundle();
barcodeConfig.PutString("PLUGIN_NAME", "BARCODE");
//barcodeConfig.PutString("RESET_CONFIG", "true"); // This is the default but never hurts to specify
Bundle barcodeProps = new Bundle();
barcodeProps.PutString("scanner_input_enabled", "true"); // This is the default but never hurts to specify
barcodeProps.PutString("scanner_selection_by_identifier", "AUTO ");
barcodeProps.PutString("scanner_selection", "auto ");
barcodeProps.PutString("aim_mode", "off ");
barcodeProps.PutString("illumination_mode", "off ");
barcodeConfig.PutBundle("PARAM_LIST", barcodeProps);
profileConfig.PutBundle("PLUGIN_CONFIG", barcodeConfig);
Bundle appConfig = new Bundle();
appConfig.PutString("PACKAGE_NAME", this.PackageName); // Associate the profile with this app
appConfig.PutStringArray("ACTIVITY_LIST", new String[] { "*" });
profileConfig.PutParcelableArray("APP_LIST", new Bundle[] { appConfig });
SendDataWedgeIntentWithExtra(ACTION_DATAWEDGE_FROM_6_2, EXTRA_SET_CONFIG, profileConfig);
// You can only configure one plugin at a time, we have done the barcode input, now do the intent output
profileConfig.Remove("PLUGIN_CONFIG");
Bundle intentConfig = new Bundle();
intentConfig.PutString("PLUGIN_NAME", "INTENT");
intentConfig.PutString("RESET_CONFIG", "true");
Bundle intentProps = new Bundle();
intentProps.PutString("intent_output_enabled", "true");
intentProps.PutString("intent_action", DataWedgeReceiver.IntentAction);
intentProps.PutString("intent_delivery", "2");
intentConfig.PutBundle("PARAM_LIST", intentProps);
profileConfig.PutBundle("PLUGIN_CONFIG", intentConfig);
SendDataWedgeIntentWithExtra(ACTION_DATAWEDGE_FROM_6_2, EXTRA_SET_CONFIG, profileConfig);
}
private void SendDataWedgeIntentWithExtra(String action, String extraKey, Bundle extras)
{
Intent dwIntent = new Intent();
dwIntent.SetAction(action);
dwIntent.PutExtra(extraKey, extras);
SendBroadcast(dwIntent);
}
private void SendDataWedgeIntentWithExtra(String action, String extraKey, String extraValue)
{
Intent dwIntent = new Intent();
dwIntent.SetAction(action);
dwIntent.PutExtra(extraKey, extraValue);
SendBroadcast(dwIntent);
}
public void SendScannerEnable()
{
SendDataWedgeIntentWithExtra(ACTION_DATAWEDGE_FROM_6_2, EXTRA_SET_CONFIG, "ENABLE_PLUGIN");
}
public void SendScannerDisable()
{
SendDataWedgeIntentWithExtra(ACTION_DATAWEDGE_FROM_6_2, EXTRA_SET_CONFIG, "DISABLE_PLUGIN");
}
}
}
Snip from Page.cs
protected override void OnAppearing()
{
DependencyService.Get<IScannerConnection>().SendScannerEnable();
MessagingCenter.Subscribe<App, string>(this, "ScanBarcode", (sender, arg) =>
{
ScanBarcode(arg);
});
base.OnAppearing();
}
I have searched through internet but I didn't get any resource on how to fix it.
Thank you.
The url below helped me to fix my problem. I had to create a new scanner class which enables and disables datawedge profile by triggering events. https://developer.zebra.com/community/home/blog/2018/07/11/xamarinforms-freshmvvm-datawedge-take-2
I'm new to Xamarin, I'm using this example as a base for my on application. I just noticed when I click in a link to download files, it doesnt do anything, but i can see in the consoloe the next error
mSecurityInputMethodService is null
I saw this as an approach, but it didnt do anything at all
private void WebView_OnNavigating(object sender, WebNavigatingEventArgs e)
{
if (e.Url.ToLower().Contains("forcesave=true") || e.Url.ToLower().Contains("pdf"))
{
var uri = new Uri(e.Url);
Device.OpenUri(uri);
e.Cancel = true;
}
}
Can you please give me some advice?
Create a custom renderer for webview.
[assembly: ExportRenderer(typeof(Xamarin.Forms.WebView),
typeof(CustomWebViewRenderer))]
namespace TwoCollen.Droid
{
public class CustomWebViewRenderer : ViewRenderer<Xamarin.Forms.WebView, global::Android.Webkit.WebView>
{
public CustomWebViewRenderer(Context context) : base(context) { }
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
if (this.Control == null)
{
var webView = new global::Android.Webkit.WebView(this.Context);
webView.SetWebViewClient(new WebViewClient());
webView.SetWebChromeClient(new WebChromeClient());
WebSettings webSettings = webView.Settings;
webSettings.JavaScriptEnabled=true;
webView.SetDownloadListener(new CustomDownloadListener());
this.SetNativeControl(webView);
var source = e.NewElement.Source as UrlWebViewSource;
if (source != null)
{
webView.LoadUrl(source.Url);
}
}
}
}
public class CustomDownloadListener : Java.Lang.Object, IDownloadListener
{
public void OnDownloadStart(string url, string userAgent, string contentDisposition, string mimetype, long contentLength)
{
DownloadManager.Request request = new DownloadManager.Request(Android.Net.Uri.Parse(url));
request.AllowScanningByMediaScanner();
request.SetNotificationVisibility(DownloadVisibility.VisibleNotifyCompleted);
request.SetDestinationInExternalFilesDir(Forms.Context, Android.OS.Environment.DirectoryDownloads, "mydeddp.pdf");
DownloadManager dm = (DownloadManager)Android.App.Application.Context.GetSystemService(Android.App.Application.DownloadService);
dm.Enqueue(request);
}
}
Here is running GIF.