How to get a callback from a custom renderer in xamarin forms - xamarin

I Have a custom hybridWebView for android and ios to load the URL. What I required is to pass a callback to the content page once the URL has completed the loading. Code as below, help would much appreciate.
Content Page
public partial class ConveyancingLeadPage : ContentPage
{
DashboardViewModel viewmodel;
StorageService storage = new StorageService();
public ConveyancingLeadPage()
{
InitializeComponent();
GetUserAvatar();
}
protected async override void OnAppearing()
{
// I need the callback to be execute here
customView.weblink = viewmodel.BrokerData.config.conveyancing.listing_webview;
}
}
Android HybridView
[assembly: ExportRenderer(typeof(HCHybridWebview), typeof(HCHybridWebviewRendererAndroid))]
namespace HashChing.Droid.CustomRenderers
{
public class HCHybridWebviewRendererAndroid : ViewRenderer<HCHybridWebview, Android.Webkit.WebView>
{
Context _context;
public HCHybridWebviewRendererAndroid(Context context) : base(context)
{
_context = context;
}
protected override void OnElementChanged(ElementChangedEventArgs<HCHybridWebview> e)
{
base.OnElementChanged(e);
const string JavascriptFunction = "function invokeCSharpAction(data){jsBridge.invokeAction(data);}";
if (Control == null)
{
//Do something
if (e.NewElement != null)
{
//Load URL
Control.AddJavascriptInterface(new JSBridge(this), "jsBridge");
var hybridWebView = e.NewElement as HCHybridWebview;
if (hybridWebView != null)
{
hybridWebView.RefreshView += LoadUrl;
}
}
Load URL
public void LoadUrl(object sender, EventArgs e)
{
Control.LoadUrl(webView.weblink, headers);
}
Once the URL been loaded it will navigate to this method in the same class, and this is where I want to pass a callback TO my content page once the loading is completed inside the "OnPageFinished" method. Help would much appreciate.
public class JavascriptWebViewClient : WebViewClient
{
string _javascript;
public JavascriptWebViewClient(string javascript)
{
_javascript = javascript;
}
public override void OnPageFinished(Android.Webkit.WebView view, string url)
{
base.OnPageFinished(view, url);
view.EvaluateJavascript(_javascript, null);
}
}

you could use MessagingCenter to send and get the callback:
in your ContentPage,for example Page1.xaml.cs
public Page1 ()
{
InitializeComponent ();
//here you could get the callback,and arg = "this is call back"
MessagingCenter.Subscribe<Page1,string>(this,"callback", (send, arg) =>
{
Console.WriteLine(arg);
});
}
then in your OnPageFinished method:
public override void OnPageFinished(Android.Webkit.WebView view, string url)
{
base.OnPageFinished(view, url);
view.EvaluateJavascript(_javascript, null);
//send the callback content,Parameters can be defined by yourself
MessagingCenter.Send<Page1,string>(new Page1(), "callback","this is call back");
}
more information:MessagingCenter

create your own event LoadCompleted in your customrender and invoke it from your custom class once load is finish.
And in your JavascriptWebViewClient class you can subscribe to that event and do whatever you want do do at that time.
in case you need it: Events in c#
public partial class ConveyancingLeadPage : ContentPage
{
protected async override void OnAppearing()
{
customView.weblink = viewmodel.BrokerData.config.conveyancing.listing_webview;
// I need the callback to be execute here
customView.LoadCompleted+=LoadCompleted;
}
}

Related

Xamarin.macOS Navigated event not fire for CustomWebView

I have created custom renderer for my WebView because I need to set property CustomUserAgent. But for my CustomWebView event Navigated is not fired. As I understand I need set NavigationDelegate but can't find any working example. Here is my code:
[assembly: ExportRenderer(typeof(CustomWebView), typeof(CustomWebViewRenderer))]
namespace DesktopClient.MacOS.CustomRenderers
{
public class CustomWebViewRenderer : ViewRenderer<CustomWebView, WKWebView>
{
private WKWebView wkWebView;
protected override void OnElementChanged(ElementChangedEventArgs<CustomWebView> e)
{
base.OnElementChanged(e);
if (this.Control == null)
{
WKWebViewConfiguration config = new();
this.wkWebView = new WKWebView(this.Frame, config)
{
NavigationDelegate = new WKNavigationDelegate()
};
this.SetNativeControl(this.wkWebView);
WKWebViewContainer.WKWebView = this.wkWebView;
}
if (e.NewElement != null)
{
UrlWebViewSource source = (UrlWebViewSource)e.NewElement.Source;
if (source != null)
{
this.Control.LoadRequest(new NSUrlRequest(new NSUrl(source.Url)));
}
this.Control.CustomUserAgent = e.NewElement.UserAgent;
this.wkWebView.NavigationDelegate = new WKNavigationDelegate();
}
}
}
}
Create a new class inherit from WKNavigationDelegate and assign to Webview.NavigationDelegate ,and then you can override the method like DidFinishNavigation in the new class .
Sample code
NavigationDelegate = new MyDelegate();
public class MyDelegate : WKNavigationDelegate {
public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
}
}
However , I found the method DidFinishNavigation does not trigger at initial load ,it can only be triggered when we navigate to other web page.

Setting current url in a custom xamarin forms web view

I'm starting with Xamarin.Forms and what I'm trying to do is simply setting a CurrentUrl property on a custom Webview in Xamarin.Forms
In other words: When OnPageFinished method is called, I need to set the CurrentUrl property of MyWebView to the new Url.
Anyone have an idea?
Here's my main Webview:
public class MyWebView: Xamarin.Forms.WebView
{
public static readonly BindableProperty UrlProperty = BindableProperty.Create(
propertyName: "CurrentUrl",
returnType: typeof(string),
declaringType: typeof(MyWebView),
defaultValue: default(string));
public string CurrentUrl
{
get { return (string)GetValue(UrlProperty); }
set { SetValue(UrlProperty, value); }
}
}
And here's my Renderer in Project.Droid:
[assembly: ExportRenderer(typeof(MyWebView), typeof(MyProject.Droid.WebViewRenderer))]
namespace Manateq.Droid
{
public class WebViewRenderer : Xamarin.Forms.Platform.Android.WebViewRenderer
{
public WebViewRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
Control.SetWebViewClient(new Callback(Plugin.CurrentActivity.CrossCurrentActivity.Current.Activity));
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
MyWebView myWebView = sender as MyWebView;
if (e.PropertyName == "CurrentUrl")
{
}
}
}
public class Callback : WebViewClient
{
Activity _context;
public Callback(Activity _context)
{
this._context = _context;
}
public override bool ShouldOverrideUrlLoading(Android.Webkit.WebView view, string url)
{
//view.LoadUrl(url);
Intent i = new Intent(Intent.ActionView, Uri.Parse(url));
_context.StartActivity(i);
return true;
}
public override void OnPageStarted(Android.Webkit.WebView view, string url, Android.Graphics.Bitmap favicon)
{
base.OnPageStarted(view, url, favicon);
DependencyService.Get<ILoadingIndicator>().Show();
}
public override void OnPageFinished(Android.Webkit.WebView view, string url)
{
base.OnPageFinished(view, url);
//element.CurrentUrl = url;
DependencyService.Get<ILoadingIndicator>().Dismiss();
}
}
}
And here's I'm using the custom web view in xaml:
<customControls:MyWebView VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand" x:Name="webView"/>
You can got the value of CurrentUrl by MyWebView myWebView = e.NewElement as MyWebView; var currentUrl=myWebView.CurrentUrl; in OnElementChanged; Then you can transfer this value to Callback's constructor. In the end, you can set it in the OnPageFinished method by element.CurrentUrl = currenturl; like following code.
[assembly: ExportRenderer(typeof(MyWebView), typeof(WebViewRenderer))]
namespace WebviewDemo.Droid
{
public class WebViewRenderer : Xamarin.Forms.Platform.Android.WebViewRenderer
{
public WebViewRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
MyWebView myWebView = e.NewElement as MyWebView;
var currentUrl=myWebView.CurrentUrl;
Control.SetWebViewClient(new Callback(Plugin.CurrentActivity.CrossCurrentActivity.Current.Activity, currentUrl));
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
MyWebView myWebView = sender as MyWebView;
if (e.PropertyName == "CurrentUrl")
{
}
}
}
public class Callback : WebViewClient
{
Activity _context;
string currenturl;
public Callback(Activity _context,string currenturl)
{
this._context = _context;
this.currenturl = currenturl;
}
public override bool ShouldOverrideUrlLoading(Android.Webkit.WebView view, string url)
{
//view.LoadUrl(url);
Intent i = new Intent(Intent.ActionView, Uri.Parse(url));
_context.StartActivity(i);
return true;
}
public override void OnPageStarted(Android.Webkit.WebView view, string url, Android.Graphics.Bitmap favicon)
{
base.OnPageStarted(view, url, favicon);
DependencyService.Get<ILoadingIndicator>().Show();
}
public override void OnPageFinished(Android.Webkit.WebView view, string url)
{
base.OnPageFinished(view, url);
element.CurrentUrl = currenturl;
DependencyService.Get<ILoadingIndicator>().Dismiss();
}
}
}
Do you want to achieve the result like this GIF?
If so, you can load the current url in the OnPageFinished method.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Webkit;
using Android.Widget;
using WebviewDemo;
using WebviewDemo.Droid;
using Xamarin.Forms;
[assembly: ExportRenderer(typeof(MyWebView), typeof(WebViewRenderer))]
namespace WebviewDemo.Droid
{
public class WebViewRenderer : Xamarin.Forms.Platform.Android.WebViewRenderer
{
public WebViewRenderer(Context context) : base(context)
{
}
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
MyWebView myWebView = e.NewElement as MyWebView;
var currentUrl=myWebView.CurrentUrl;
Control.SetWebViewClient(new Callback(Plugin.CurrentActivity.CrossCurrentActivity.Current.Activity, currentUrl));
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
MyWebView myWebView = sender as MyWebView;
if (e.PropertyName == "CurrentUrl")
{
}
}
}
public class Callback : WebViewClient
{
Activity _context;
string currenturl;
public Callback(Activity _context,string currenturl)
{
this._context = _context;
this.currenturl = currenturl;
}
public override bool ShouldOverrideUrlLoading(Android.Webkit.WebView view, string url)
{
//if (!loadingFinished)
//{
// redirect = true;
//}
//loadingFinished = false;
//view.LoadUrl(currenturl);
//Console.WriteLine("Loading web view...");
return true;
}
public override void OnPageStarted(Android.Webkit.WebView view, string url, Android.Graphics.Bitmap favicon)
{
base.OnPageStarted(view, url, favicon);
}
public override void OnPageFinished(Android.Webkit.WebView view, string url)
{
if (url.Equals("https://www.google.com/"))
{
view.LoadUrl(currenturl);
}
}
//public override bool ShouldOverrideUrlLoading(Android.Webkit.WebView view, string url)
//{
// //view.LoadUrl(url);
// // Intent i = new Intent(Intent.ActionView, Uri.Parse(url));
// // _context.StartActivity(i);
// view.LoadUrl(url);
// isRedirected = true;
// return true;
//}
//public override void OnPageStarted(Android.Webkit.WebView view, string url, Android.Graphics.Bitmap favicon)
//{
// base.OnPageStarted(view, url, favicon);
// if (!isRedirected)
// {
// //Do something you want when starts loading
// }
// isRedirected = false;
// // DependencyService.Get<ILoadingIndicator>().Show();
//}
//public override void OnPageFinished(Android.Webkit.WebView view, string url)
//{
// base.OnPageFinished(view, url);
// if (!isRedirected)
// {
// //Do something you want when finished loading
// }
// // element.CurrentUrl = currenturl;
// // DependencyService.Get<ILoadingIndicator>().Dismiss();
//}
}
}
In the xaml. You can bind the value for CurrentUrl, Or just set it directly.
<customControls:MyWebView VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand" x:Name="webView"/>
Xaml background code.
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
webView.Source = "https://www.google.com";
webView.CurrentUrl = "https://www.baidu.com";
}
}
The answer provided by Leon is good. But I found an easier way to do it. Here's what I've done.
In the Callback class that extends WebviewClient I've edited OnPageStarted and OnPageFinished to be as following:
public override void OnPageStarted(Android.Webkit.WebView view, string url, Android.Graphics.Bitmap favicon)
{
DependencyService.Get<ILoadingIndicator>().Show();
base.OnPageStarted(view, url, favicon);
var args = new WebNavigatingEventArgs(WebNavigationEvent.NewPage, new UrlWebViewSource { Url = url }, url);
_renderer.ElementController.SendNavigating(args);
}
public override void OnPageFinished(Android.Webkit.WebView view, string url)
{
DependencyService.Get<ILoadingIndicator>().Dismiss();
base.OnPageFinished(view, url);
var source = new UrlWebViewSource { Url = url };
var args = new WebNavigatedEventArgs(WebNavigationEvent.NewPage, source, url, WebNavigationResult.Success);
_renderer.ElementController.SendNavigated(args);
}
As for _rendered its: ElementController => Element; and it exist in the WebViewRenderer class.
When adding these line of codes, the Navigated event is being hit normally. So when that happens we can simply use the url parameter that exist in Navigated event.

Open app from another app with Xamarin.Forms and MvvmCross UWP

Opening app from another app can be done like:
public async Task<bool> LaunchUriAsync(string objectNumber)
{
var option = new LauncherOptions();
option.UI.PreferredPlacement = Placement.Right;
option.DesiredRemainingView = ViewSizePreference.UseMore;
return await Launcher.LaunchUriAsync(buildObjectAccessUri(objectNumber), option);
}
private static Uri buildObjectAccessUri(string objectId)
{
return new Uri(String.Format("{0}://{1}/{2}", "myapp", "data", objectId), UriKind.Absolute);
}
in a Command I do:
await MyLauncherService.LaunchUriAsync("whatever");
and in opened application I have a custom start
public class CustomAppStart : MvxAppStart
{
protected override async Task NavigateToFirstViewModel(object hint = null)
{
// hint here is type of `Windows.ApplicationModel.Activation.ProtocolActivatedEventArgs`
}
}
Seems that object hint is type of Windows.ApplicationModel.Activation.ProtocolActivatedEventArgs
The App.xaml.cs of UWP project:
sealed partial class App
{
public App()
{
InitializeComponent();
}
}
public abstract class UwpApp : MvxApplication<Setup<Core.App>, Core.App>
{
private ILoggerService mLoggerService;
public UwpApp()
{
UnhandledException += (sender, args) =>
{
mLoggerService = Mvx.IoCProvider.Resolve<ILoggerService>();
if (mLoggerService == null)
{
return;
}
mLoggerService.Fatal("Unhandled exception!", args.Exception);
};
DebugSettings.BindingFailed += (sender, args) => Debug.WriteLine(args.Message);
TaskScheduler.UnobservedTaskException += (sender, args) =>
{
mLoggerService = Mvx.IoCProvider.Resolve<ILoggerService>();
if (mLoggerService == null)
{
return;
}
foreach (var ex in args.Exception.Flatten().InnerExceptions)
{
mLoggerService.Error(ex.Message + " StackTrace:" + ex.StackTrace);
}
args.SetObserved();
};
Suspending += OnSuspending;
ApplicationView.PreferredLaunchWindowingMode = ApplicationViewWindowingMode.Auto;
}
protected override void OnSuspending(object sender, SuspendingEventArgs e)
{
var deferral = e.SuspendingOperation.GetDeferral();
deferral.Complete();
}
}
Why I got that ?
I expected to be string ...
Where is the mistake ?
I'm using latest version of xamarin and mvvm cross
In your MvxApplication you can override GetAppStartHint to process the object coming into that method and return a string as you expect.
Something like:
public abstract class UwpApp : MvxApplication<Setup<Core.App>, Core.App>
{
protected override object GetAppStartHint(object hint = null)
{
// process hint
//return string
}
}

How to pass json data in xamarin forms web view android side

I am using xamarin forms webview and want to pass username and password json body with url like below.
I Implement Hybrid webview in xamarin forms
My PCL Code:
public class HybridWebView : WebView
{
Action<string> action;
public EventHandler<bool> WebNavigating;
public void RegisterAction (Action<string> callback)
{
action = callback;
}
public void Cleanup ()
{
action = null;
}
public void InvokeAction (string data)
{
if (action == null || string.IsNullOrEmpty(data)) {
return;
}
action.Invoke (data);
}
}
My Android HybridWebViewRenderer Code :
public class HybridWebViewRenderer : ViewRenderer<HybridWebView, Android.Webkit.WebView>
{
Context _context;
public HybridWebViewRenderer(Context context) : base(context)
{
_context = context;
}
protected override void OnElementChanged(ElementChangedEventArgs<HybridWebView> e)
{
base.OnElementChanged(e);
if (Control == null)
{
var webView = new Android.Webkit.WebView(_context);
webView.Settings.JavaScriptEnabled = true;
webView.SetWebViewClient(new MyWebViewClient(webView,e,this));
SetNativeControl(webView);
}
}
}
public class MyWebViewClient : WebViewClient
{
const string JavaScriptFunction = "function invokeCSharpAction(data){jsBridge.invokeAction(data);}";
public Android.Webkit.WebView _webView;
public Android.Webkit.WebView WebView
{
get
{
return _webView;
}
}
public ElementChangedEventArgs<HybridWebView> _element;
public MyWebViewClient(Android.Webkit.WebView view, ElementChangedEventArgs<HybridWebView> element, HybridWebViewRenderer x)
{
_webView = view;
_element = element;
if (_element.OldElement != null)
{
_webView.RemoveJavascriptInterface("jsBridge");
var hybridWebView = _element.OldElement as HybridWebView;
hybridWebView.Cleanup();
}
if (_element.NewElement != null)
{
_webView.AddJavascriptInterface(new JSBridge(x), "jsBridge");
string url = "https://Myspecific.com/Account/MobileLogin";
string postData = "{\"UserName\":\"User1\",\"Password\":\"pass123\",\"RememberMe\":false}";
_webView.PostUrl(url, Encoding.UTF8.GetBytes(postData));
}
}
output:
It shows wrong username and password
Please help...
Thanks in advance...

Caliburn.Micro 3.0 equivalent to Xamarin.Forms Navigation.PushModalAsync

Does Caliburn.Micro 3.0 (and Caliburn.Micro.Xamarin.Forms) implement functionality to mimic/support Navigation.PushModalAsync in Xamarin.Forms?
No. It's not build in, but its easy to enhance it. Usually, MvvM frameworks are navigating by ViewModels. Caliburn is following this pattern. So it needs some kind of navigation service. This navigationservice is responsible for creating the Views for the ViewModels and call the view framework (Xamarin.Froms in our case) specific navigation functions. NavigationPageAdapter is the thing we are searching for. Now let's enhance it.
public interface IModalNavigationService : INavigationService
{
Task NavigateModalToViewModelAsync<TViewModel>(object parameter = null, bool animated = true);
// TODO: add more functions for closing
}
public class ModalNavigationPageAdapter : NavigationPageAdapter, IModalNavigationService
{
private readonly NavigationPage _navigationPage;
public ModalNavigationPageAdapter(NavigationPage navigationPage) : base(navigationPage)
{
_navigationPage = navigationPage;
}
public async Task NavigateModalToViewModelAsync<TViewModel>(object parameter = null, bool animated = true)
{
var view = ViewLocator.LocateForModelType(typeof(TViewModel), null, null);
await PushModalAsync(view, parameter, animated);
}
private Task PushModalAsync(Element view, object parameter, bool animated)
{
var page = view as Page;
if (page == null)
throw new NotSupportedException(String.Format("{0} does not inherit from {1}.", view.GetType(), typeof(Page)));
var viewModel = ViewModelLocator.LocateForView(view);
if (viewModel != null)
{
TryInjectParameters(viewModel, parameter);
ViewModelBinder.Bind(viewModel, view, null);
}
page.Appearing += (s, e) => ActivateView(page);
page.Disappearing += (s, e) => DeactivateView(page);
return _navigationPage.Navigation.PushModalAsync(page, animated);
}
private static void DeactivateView(BindableObject view)
{
if (view == null)
return;
var deactivate = view.BindingContext as IDeactivate;
if (deactivate != null)
{
deactivate.Deactivate(false);
}
}
private static void ActivateView(BindableObject view)
{
if (view == null)
return;
var activator = view.BindingContext as IActivate;
if (activator != null)
{
activator.Activate();
}
}
}
We just declared the interface IModalNavigationService that extends INavigationService and implement it in our ModalNavigationPageAdapter. Unfortunately Caliburn made alot of functions private, so we have to copy them over to our inherited version.
In caliburn you can navigate via navigationservice.For<VM>().Navigate(). We want to follow this style, so we have to implement something like navigationservice.ModalFor<VM>().Navigate() which we do in an extension method.
public static class ModalNavigationExtensions
{
public static ModalNavigateHelper<TViewModel> ModalFor<TViewModel>(this IModalNavigationService navigationService)
{
return new ModalNavigateHelper<TViewModel>().AttachTo(navigationService);
}
}
This method returns a ModalNavigateHelperthat simplifies the usage of our navigation service (similar to Caliburn's NavigateHelper). It's nearly a copy, but for the IModalNavigationService.
public class ModalNavigateHelper<TViewModel>
{
readonly Dictionary<string, object> parameters = new Dictionary<string, object>();
IModalNavigationService navigationService;
public ModalNavigateHelper<TViewModel> WithParam<TValue>(Expression<Func<TViewModel, TValue>> property, TValue value)
{
if (value is ValueType || !ReferenceEquals(null, value))
{
parameters[property.GetMemberInfo().Name] = value;
}
return this;
}
public ModalNavigateHelper<TViewModel> AttachTo(IModalNavigationService navigationService)
{
this.navigationService = navigationService;
return this;
}
public void Navigate(bool animated = true)
{
if (navigationService == null)
{
throw new InvalidOperationException("Cannot navigate without attaching an INavigationService. Call AttachTo first.");
}
navigationService.NavigateModalToViewModelAsync<TViewModel>(parameters, animated);
}
}
Last but not least, we have to use our shiny new navigation service instead of the old one. The App class is registering the NavigationPageAdapter for the INavigationService as singleton in PrepareViewFirst. We have to change it as follows
public class App : FormsApplication
{
private readonly SimpleContainer container;
public App(SimpleContainer container)
{
this.container = container;
container
.PerRequest<LoginViewModel>()
.PerRequest<FeaturesViewModel>();
Initialize();
DisplayRootView<LoginView>();
}
protected override void PrepareViewFirst(NavigationPage navigationPage)
{
var navigationService = new ModalNavigationPageAdapter(navigationPage);
container.Instance<INavigationService>(navigationService);
container.Instance<IModalNavigationService>(navigationService);
}
}
We are registering our navigation service for INavigationService and IModalNavigationService.
As you can see in the comment, you have to implement close functions that call PopModalAsync by yourself.

Resources