Xamarin.macOS Navigated event not fire for CustomWebView - macos

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.

Related

Implementing Custom Webview in Xamarin Forms

I'm new with Xamarin.Forms and I have implemented a custom web view renderer in Droid project.
The issue is when implementing the renderer in iOS project, it's like the Webview is initialized without loading the CSS and Javascript. Because it only display HTML page without any functionality.
After some research, I know we have to implement WKWebviewRenderer and we have to load the LoadFileUrl method in iOS but I still can't catch the url in the renderer.
Anyone have an idea please about how to implement the following Android Renderer code into iOS Renderer??
Custom renderer in Droid project:
[assembly: ExportRenderer(typeof(Xamarin.Forms.WebView), typeof(MyProject.Droid.WebViewRenderer))]
namespace MyProject.Droid
{
public class WebViewRenderer : Xamarin.Forms.Platform.Android.WebViewRenderer
{
private bool isMyCustomWebview = false;
public IWebViewController ElementController => Element;
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
if (e.NewElement.GetType() == typeof(MyCustomWebview))
{
Control.SetWebViewClient(new Callback(this));
isMyCustomWebview = true;
}
else
{
Control.SetWebViewClient(new Callback(Plugin.CurrentActivity.CrossCurrentActivity.Current.Activity));
isMyCustomWebview = false;
}
}
public class Callback : WebViewClient
{
Activity _context;
public Callback(Activity _context)
{
this._context = _context;
}
WebViewRenderer _renderer;
public Callback(WebViewRenderer renderer)
{
_renderer = renderer ?? throw new ArgumentNullException("Renderer");
}
public override void OnPageStarted(Android.Webkit.WebView view, string url, Android.Graphics.Bitmap favicon)
{
base.OnPageStarted(view, url, favicon);
if (_renderer != null && _renderer.isMyCustomWebview)
{
DependencyService.Get<ILoadingIndicator>().Show();
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)
{
base.OnPageFinished(view, url);
if (_renderer != null && _renderer.isMyCustomWebview)
{
DependencyService.Get<ILoadingIndicator>().Dismiss();
var source = new UrlWebViewSource { Url = url };
var args = new WebNavigatedEventArgs(WebNavigationEvent.NewPage, source, url, WebNavigationResult.Success);
_renderer.ElementController.SendNavigated(args);
}
}
}
}
}
UPDATE: Custom renderer in iOS project:
[assembly: ExportRenderer(typeof(Xamarin.Forms.WebView), typeof(MyProject.iOS.WebViewRenderer))]
namespace MyProject.iOS
{
// Xamarin.Forms.Platform.iOS.WebViewRenderer
public class WebViewRenderer : ViewRenderer<WebView, WKWebView>
{
WKWebView wkWebView;
protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
{
base.OnElementChanged(e);
if (Control != null) return;
var config = new WKWebViewConfiguration();
wkWebView = new WKWebView(Frame, config) { NavigationDelegate = new MyNavigationDelegate() };
SetNativeControl(wkWebView);
}
}
public class MyNavigationDelegate : WKNavigationDelegate
{
public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
//get url here
var url = webView.Url;
// webView.LoadFileUrl(url, url);
}
}
}
UPDATE:
Here's how I'm setting the source and base url for the webview in the portable project:
var result = await client.PostAsync("/embedded/pay", content);
if (result.IsSuccessStatusCode)
{
var resp = await result.Content.ReadAsStringAsync();
var html = new HtmlWebViewSource
{
Html = resp,
BaseUrl = paymentGatewayUrl
};
//Adding Cookie
CookieContainer cookies = new CookieContainer();
var domain = new Uri(html.BaseUrl).Host;
var cookie = new Cookie
{
Secure = true,
Expired = false,
HttpOnly = false,
Name = "cookie",
Expires = DateTime.Now.AddDays(10),
Domain = domain,
Path = "/"
};
cookies.Add(new Uri(html.BaseUrl), cookie);
webView.Source = html;
}
You could modify the code of Custom Renderer like following .
[assembly: ExportRenderer(typeof(WebView), typeof(MyWebViewRenderer))]
namespace xxx.iOS
{
public class MyWebViewRenderer : ViewRenderer<WebView, WKWebView>
{
WKWebView wkWebView;
protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
{
base.OnElementChanged(e);
if (Control != null) return;
var config = new WKWebViewConfiguration();
wkWebView = new WKWebView(Frame, config) { NavigationDelegate = new MyNavigationDelegate() };
SetNativeControl(wkWebView);
}
}
public class MyNavigationDelegate : WKNavigationDelegate
{
public override void DidFinishNavigation(WKWebView webView, WKNavigation navigation)
{
//get url here
var url = webView.Url;
//webView.LoadFileUrl();
}
}
}

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

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;
}
}

XamarinForms: how use a Custom Navigation Page

I order to have a cutom navigation on a specific view on my app,
I create a Custom Navigation ios:
public class CustomNavigationPageRenderer : NavigationRenderer
{
public override void ViewDidLoad()
{
base.ViewDidLoad();
UINavigationBar.Appearance.SetBackgroundImage(new UIImage(), UIBarMetrics.Default);
UINavigationBar.Appearance.ShadowImage = new UIImage();
UINavigationBar.Appearance.BackgroundColor = UIColor.Clear;
UINavigationBar.Appearance.TintColor = UIColor.White;
UINavigationBar.Appearance.BarTintColor = UIColor.Clear;
UINavigationBar.Appearance.Translucent = true;
}
}
and a common interface class:
public partial class CustomPage : NavigationPage
{
public CustomPage(): base()
{
InitializeComponent();
}
public CustomPage(Page root) : base(root)
{
InitializeComponent();
}
public bool IgnoreLayoutChange { get; set; } = false;
protected override void OnSizeAllocated(double width, double height)
{
if (!IgnoreLayoutChange)
base.OnSizeAllocated(width, height);
}
}
Now, In my specific view, how have I use it?
I need to set on false the original navigation? (NavigationPage.SetHasNavigationBar(this, false);​)
public MySpecificViewNeedCustoNAv()
{
CustomPage myNavigationPage = new CustomPage();
...
need to set on false the original navigation?
No , you can refer the following code.
For example
Creating the Root Page in App.cs
public App ()
{
MainPage = new CustomPage (new xxxpage());
}
Pushing Pages to the Navigation Stack in your page
async void ButtonClicked (object sender, EventArgs e)
{
await Navigation.PushAsync (new xxxpage());
}

How can I share the value of a field between back-end C# and a renderer?

My C# looks like this:
public App()
{
InitializeComponent();
MainPage = new Japanese.MainPage();
}
public partial class MainPage : TabbedPage
{
public MainPage()
{
InitializeComponent();
var phrasesPage = new NavigationPage(new PhrasesPage())
{
Title = "Play",
Icon = "ionicons-2-0-1-ios-play-outline-25.png"
};
public partial class PhrasesPage : ContentPage
{
public PhrasesFrame phrasesFrame;
public PhrasesPage()
{
InitializeComponent();
NavigationPage.SetHasNavigationBar(this, false);
App.phrasesPage = this;
}
protected override void OnAppearing()
{
base.OnAppearing();
App.dataChange = true;
phrasesFrame = new PhrasesFrame(this);
phrasesStackLayout.Children.Add(phrasesFrame);
}
public partial class PhrasesFrame : Frame
{
private async Task ShowCard()
{
if (pauseCard == false)
..
and I have an iOS renderer for a tab page
public class TabbedPageRenderer : TabbedRenderer
{
private MainPage _page;
private void OnTabBarReselected(object sender, UITabBarSelectionEventArgs e)
{
...
pauseCard = false;
...
My problem is there is no connection between the two and I would like to know how I can make it so that pauseCard could be set in one place and read in another.
Here is a simple custom Entry example using a bindable bool property that gets changed from the renderer every time the text changes in the entry.
Entry subclass w/ a bindable property called OnOff (bool)
public class CustomPropertyEntry : Entry
{
public static readonly BindableProperty OnOffProperty = BindableProperty.Create(
propertyName: "OnOff",
returnType: typeof(bool),
declaringType: typeof(CustomPropertyEntry),
defaultValue: false);
public bool OnOff
{
get { return (bool)GetValue(OnOffProperty); }
set { SetValue(OnOffProperty, value); }
}
}
iOS Renderer
Note: I keep a reference to the instance of the CustomPropertyEntry passed into OnElementChanged so later I can set its custom property when needed.
public class CustomPropertyEntryRenderer : ViewRenderer<CustomPropertyEntry, UITextField>
{
UITextField textField;
CustomPropertyEntry entry;
protected override void OnElementChanged(ElementChangedEventArgs<CustomPropertyEntry> e)
{
base.OnElementChanged(e);
if (Control == null)
{
textField = new UITextField();
SetNativeControl(textField);
}
if (e.OldElement != null)
{
textField.RemoveTarget(EditChangedHandler, UIControlEvent.EditingChanged);
entry = null;
}
if (e.NewElement != null)
{
textField.AddTarget(EditChangedHandler, UIControlEvent.EditingChanged);
entry = e.NewElement;
}
}
void EditChangedHandler(object sender, EventArgs e)
{
entry.OnOff = !entry.OnOff;
}
}
XAML Example:
<local:CustomPropertyEntry x:Name="customEntry" Text="" />
<Switch BindingContext="{x:Reference customEntry}" IsToggled="{Binding OnOff}" />

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