Xamarin Forms custom control: implement a method in the renderer - c#

I'm trying to create an extended version of the standard XF Map object:
public class RRMap: Map
{
public void DoSomethingOnMap() {
/* ... */
}
}
I also created an Android renderer (iOS will come later):
[assembly: ExportRenderer(typeof(RRMap), typeof(RRMapRendererAndroid))]
namespace MyApp.Droid.Renderers
{
public class RRMapRendererAndroid : MapRenderer
{
public RRMapRendererAndroid(Context context) : base(context) { }
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Map> e)
{
base.OnElementChanged(e);
if (e.NewElement != null)
{
Control.GetMapAsync(this);
}
}
protected override MarkerOptions CreateMarker(Pin pin)
{
var marker = new MarkerOptions();
marker.SetPosition(new LatLng(pin.Position.Latitude, pin.Position.Longitude));
marker.SetTitle(pin.Label);
marker.SetSnippet(pin.Address);
marker.SetIcon(BitmapDescriptorFactory.DefaultMarker(210));
return marker;
}
}
}
Everything is working fine so far: the map is rendered and pins are created with a custom color.
Unfortunately, I'm stuck on the implementation of DoSomethingOnMap method: it should be a method in the shared code, but it should be implemented in different ways, depending on the platform.
In other circumstances, I would create an interface using DependencyService for implementation, but in this particular case I can't figure out how to proceed.

The first solution is you can use a messaging-center, this can communicate between shared project and iOS/Android project.
Publish a message in the doSomethingOnMap method and anywhere you subscribed to the message will be triggered.
The second is create an event in your shared project and subscribe to that event in the renderer, I wrote both two solutions below:
In your shared project:
public class CustomMap : Map
{
public List<CustomPin> CustomPins { get; set; }
public event EventHandler CallToNativeMethod;
public void doSomething()
{
if (CallToNativeMethod != null)
CallToNativeMethod(this, new EventArgs());
}
public void doSomething(CustomMap myMap) {
MessagingCenter.Send<CustomMap>(this, "Hi");
}
}
In the renderer:
protected override void OnElementChanged(ElementChangedEventArgs<View> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
}
if (e.NewElement != null)
{
MessagingCenter.Subscribe<CustomMap>(this, "Hi", (sender) =>
{
// Do something whenever the "Hi" message is received
Console.WriteLine("hi");
});
((CustomMap)e.NewElement).CallToNativeMethod += (sender, arg) =>
{
Console.WriteLine("native method");
};
}
}
At anywhere you want to call this method:
private void Button_Clicked(object sender, System.EventArgs e)
{
customMap.doSomething();
customMap.doSomething(customMap);
}

Related

How to intercept "a redirect URL" from webview in Xamarin Forms

I'm displaying an OAtuh2 HTML page in WebView that returns me, after clicking on a validation button that is on this page, a redirect URL that I would like to intercept to use the information from it.
in XML file
<ContentPage.Content>
<WebView x:Name="browser"></WebView>
</ContentPage.Content>
in CS file
browser.Source = "https://myUrl";
My low knowledge in Xamarin doesn't allow me to know how to do it
Thanks for your help
You can do as ToolmakerSteve mentioned using WebNavigatingEventArgs.
And for details you can implement this on each specific platform with custom renderer .
iOS
[assembly: ExportRenderer(typeof(WebView), typeof(MyRenderer))]
namespace FormsApp.iOS
{
class MyRenderer : WkWebViewRenderer
{
protected override void OnElementChanged(VisualElementChangedEventArgs e)
{
base.OnElementChanged(e);
this.NavigationDelegate = new MyDelegate();
}
}
public class MyDelegate : WKNavigationDelegate
{
public override void DecidePolicy(WKWebView webView, WKNavigationAction navigationAction, Action<WKNavigationActionPolicy> decisionHandler)
{
if(navigationAction.NavigationType == WKNavigationType.Other)
{
if(navigationAction.Request.Url != null)
{
//do something
}
decisionHandler(WKNavigationActionPolicy.Cancel);
return;
}
decisionHandler(WKNavigationActionPolicy.Allow);
}
}
}
Android
[assembly: ExportRenderer(typeof(Xamarin.Forms.WebView), typeof(MyRenderer))]
namespace FormsApp.Droid
{
class MyRenderer : WebViewRenderer
{
public MyRenderer(Context context):base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
if (Control != null)
{
Control.SetWebViewClient(new MyClient());
}
}
}
public class MyClient : WebViewClient
{
public override bool ShouldOverrideUrlLoading(Android.Webkit.WebView view, IWebResourceRequest request)
{
//do something
return true;
}
}
}
Refer to
https://stackoverflow.com/a/45604360/8187800
https://stackoverflow.com/a/4066497/8187800

How to prevent Xamarin forms, a custom menu and WebView renderer with EvaluateJavascript from freezing?

I have created a custom menu item which appears in the default menu which pops up when selecting text on my custom WebView.
On clicking on the menu item it calls EvaluateJavascript to get the selected WebView text, and then passes the text to another page.
However after performing this action once or twice, some text on certain areas of the screen start to become unresponsive to clicks eg. text on the parts of the WebView become unselectable, clicks on that part of the screen on other pages becomes unresponsive and even the soft keyboard becomes unclickable in some spots. If this continues for a while sometimes my app will then suddenly freeze the entire operating system and I have to soft reset my phone. It appears that there maybe some serious memory leakage going on.
I create my custom menu item in the MainActivity class:
public override void OnActionModeStarted(ActionMode mode)
{
if (Root.IsCurrentPageType<DictPage>() && DictP.IsWebViewFocused())
{
IMenu menu = mode.Menu;
menu.Add("To Notes");
menu.GetItem(0).SetOnMenuItemClickListener(new MyMenuItemOnMenuItemClickListener(this, mode));
}
base.OnActionModeStarted(mode);
}
It is then handled in the Listener class...
public class MyMenuItemOnMenuItemClickListener : Java.Lang.Object, IMenuItemOnMenuItemClickListener
{
private MainActivity mContext;
ActionMode _mode;
public MyMenuItemOnMenuItemClickListener(MainActivity activity, ActionMode mode)
{
this.mContext = activity;
_mode = mode;
}
public bool OnMenuItemClick(IMenuItem item)
{
WEB.CopyToMainNotes();
Device.BeginInvokeOnMainThread(() =>
{
//close menu if clicked
_mode?.Finish();
});
return true;
}
}
...which calls CopyToMainNotes on my derived WebView class and its associated Renderer and EventHandler classes:
public class WebViewEx : Xamarin.Forms.WebView
{
public static WebViewEx WEB;
//Namespace
//YourClass
public event WebViewExEventHandler CallNativeMethodEvent;
public void CallNativeMethod(WebViewExEventType type)
{
WebViewExEventArgs e = new WebViewExEventArgs();
e.EventType = type;
CallNativeMethodEvent?.Invoke(this, e);
}
public WebViewEx()
{
WEB = this;
}
public void CopyToMainNotes()
{
Device.BeginInvokeOnMainThread(() =>
{
CallNativeMethod(WebViewExEventType.copyToMainNotes);
});
}
}
public delegate void WebViewExEventHandler(object sender, WebViewExEventArgs e);
public class WebViewExEventArgs : EventArgs
{
public enum WebViewExEventType { copyToMainNotes };
public WebViewExEventType EventType = WebViewExEventType.copyToMainNotes;
public WebViewExEventArgs() : base()
{
}
}
public class WebViewExRenderer : WebViewRenderer
{
public WebViewExRenderer(Android.Content.Context context) : base(context)
{
}
protected override void OnElementChanged(ElementChangedEventArgs<Xamarin.Forms.WebView> e)
{
base.OnElementChanged(e);
if (Control != null)
{
WebViewEx ex = e.NewElement as WebViewEx;
ex.CallNativeMethodEvent += WebViewEx_CallNativeMethodEvent;
}
}
internal class JavascriptCallback : Java.Lang.Object, IValueCallback
{
public JavascriptCallback(Action<string> callback)
{
_callback = callback;
}
private Action<string> _callback;
public void OnReceiveValue(Java.Lang.Object value)
{
_callback?.Invoke(Convert.ToString(value));
}
}
private void WebViewEx_CallNativeMethodEvent(object sender, WebViewExEventArgs e)
{
switch (e.EventType)
{
case WebViewExEventType.copyToMainNotes:
{
CopyToMainNotes();
break;
}
}
}
public void CopyToMainNotes()
{
string script = "(function(){ return window.getSelection().toString()})()";
var response = string.Empty;
Control?.EvaluateJavascript(script, new JavascriptCallback((r) =>
{
response = r;
Device.BeginInvokeOnMainThread(() =>
{
DPage.CopyThisTextToAnotherPage(response.ToString().Trim('\"'));
});
}));
}
}
The CopyToMainNotes method above is where the EvaluateJavascript takes place and the selected text finally gets sent to another page.
Any ideas where I might be going wrong here? Thanks in advance!

How to pass values from custom PageRenderer to Shared Xaml Page code behind in Xamarin

I have a Xamarin page in which I had to use Android native page renderer in order to support platform specific API.
BasePage.xaml passes control to MyPage.xaml with Navigation.PushAsync()
XAML page : MyPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Views.MyPage" Title="My Page">
<ContentPage.Content>
</ContentPage.Content>
</ContentPage>
Android Custom page renderer for the above is something like below.
[assembly: ExportRenderer(typeof(MyPage), typeof(MyPageRenderer))]
namespace MyApp.Droid.Renderers
{
public class MyPageRenderer : PageRenderer
{
private Context _localContext;
private global::Android.Views.View view;
private Activity activity;
public event EventHandler ItemAdded;
public MyPageRenderer(Context context) : base(context)
{
_localContext = context;
}
protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
{
base.OnElementChanged(e);
if (e.OldElement != null || Element == null)
{
return;
}
try
{
SetupUserInterface();
SetupEventHandlers();
AddView(view);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(#"ERROR: ", ex.Message);
}
}
private void SetupUserInterface()
{
activity = this.Context as Activity;
view = activity.LayoutInflater.Inflate(Resource.Layout.axml_layout, this, false);
}
protected override void OnLayout(bool changed, int l, int t, int r, int b)
{
base.OnLayout(changed, l, t, r, b);
var msw = MeasureSpec.MakeMeasureSpec(r - l, MeasureSpecMode.Exactly);
var msh = MeasureSpec.MakeMeasureSpec(b - t, MeasureSpecMode.Exactly);
view.Measure(msw, msh);
view.Layout(0, 0, r - l, b - t);
}
private void SetupEventHandlers()
{
//blah blah
}
private void ButtonTapped(object sender, EventArgs e)
{
//do something
//Here Navigate back to page which triggered this with outcome parameter or some event
ItemAdded(this, EventArgs.Empty);
}
}
}
My intention is send back control to MyPage.xaml.cs or BasePage.xaml.cs from MyPageRenderer with outcome of ButtonTapped.I am using event ItemAdded and handle it in code behind of that page. I can not access ItemAdded event which is in android specific renderer only from shared project.
I have to update ViewModel of BasePage so that I update the content of the items there when MyPage has been popped after adding new item by back button.
Problem:
I can access MyPage and BasePage but can not access renderer method and variables from Shared project because Android project depends on shared not vice versa.
I have to do something like below which is working for non-native render page
BasePage:
var myPage = new MyPage();
myPage.ItemAdded += OnItemAdded;
await Navigation.PushAsync(myPage);
MyPage:
public event EventHandler ItemAdded;
.
.
void SomeMethod(){
ItemAdded(this, EventArgs.Empty);
}
Question: How do we pass control from NativeRenderer back to Xamarin Forms shared code?
I know we can pass control to MainActivity class but I want to pass control to BasePage.xaml.cs which I did not get from documentation. If anyone has worked on PageRenderer please suggest.
in "MyPage" Class
public class MyPage : ContentPage
{
public void RaiseSomeButtonClicked() => OnSomeButtonClickeded();
private void OnSomeButtonClicked()
{
//by using aggregators you can publish any event and subscribe it in you BasePage.xaml.cs
((App)App.Current).Container.Resolve<IEventAggregator>()
.GetEvent<SomeButtonClickedEvent>().Publish(new SomeButtonClickedEvent());
}
}
in "MyPageRenderer" Class :
public class MyPageRenderer : PageRenderer
{
MyPage myPage;
//...
protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
{
myPage = (MyPage)e.NewElement;
//...
}
private void ButtonTapped(object sender, EventArgs e)
{
//do something
myPage.RaiseSomeButtonClicked();
}
}
in "BasePage.xaml.cs", subscribe this event.
public partial class BasePage : ContentPage
{
private readonly SubscriptionToken _SomeButtonClickedEventSubscription;
public BasePage()
{
InitializeComponent();
_SomeButtonClickedEventSubscription = eventAggregator.Value.GetEvent<SomeButtonClickedEvent>().SubscribeAsync(async e =>
{
//anything you want to do when button clicked!
}, threadOption: ThreadOption.UIThread, keepSubscriberReferenceAlive: true);
}
}
You should define Your event class in this way:
public class SomeButtonClickedEvent : PubSubEvent<SomeButtonClickedEvent>
{
//you can define parameters here, if the event needs to pass a parameter.
}
With reference to zohre moradi's answer I could achieve this as below.
This does not use IEventAggregator -Subscribe/Publish of events methods. If event is only required at one page IEventAggregator can be avoided.
MyPage.xaml.cs
public event EventHandler ItemAdded;
public void RaiseItemAdded()
{
ItemAdded(this, EventArgs.Empty);
}
//have to close return call back after item addition in MyPage
public async void CallPopAsync()
{
await Navigation.PopAsync();
}
MyPageRenderer.cs
MyPage mypage;
protected override void OnElementChanged(ElementChangedEventArgs<Page> e)
{
base.OnElementChanged(e);
mypage= (MyPage)e.NewElement;
if (e.OldElement != null || Element == null)
{
return;
}
try
{
SetupUserInterface();
SetupEventHandlers();
AddView(view);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(#"ERROR: ", ex.Message);
}
}
private void ButtonTapped(object sender, EventArgs e)
{
//do something
myPage.RaiseItemAdded();
//notify
Toast.MakeText(this.Context, "Item created", ToastLength.Long).Show();
myPage.CallPopAsync();
}
And in BasePage.xaml.cs
//in some method
var myPage = new MyPage();
myPage.ItemAdded += OnItemAdded;
await Navigation.PushAsync(myPage);
private void OnItemAdded(object sender, EventArgs e)
{
//call method to update binding object of viewmodel
}

Xamarin Custom renderer update value

I've custom renderer in Xamarin and I wonder how to dynamically update its value.
Here is my control in the main class:
public class MainControl : View
{
public double A
{
get;
set;
}
}
Here is my custom renderer, defined in Android:
[assembly: Xamarin.Forms.ExportRenderer(typeof(MainApplication.MainControl), typeof(MainApplication.Droid.CustomRenderer))]
namespace MainApplication.Droid
{
public class CustomRenderer : ViewRenderer<MainControl,
MainApplication.Droid.ControlAndroid>
{
private ControlAndroid control;
public CustomRenderer(Context context) : base(context)
{
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
}
protected override void OnElementChanged(ElementChangedEventArgs<MainControl> e)
{
base.OnElementChanged(e);
if (Control == null)
{
control = new ControlAndroid(Context);
SetNativeControl(control);
}
}
}
}
The method OnElementChanged does only update when creating the object. OnElementPropertyChanged are not trigged.
I expected that something should be trigged when changing the value of the property A from the main class.
I found the answer by my own. I figured out that I needed a bindable property (connected to my regular property "A") in order to get a call on OnElementPropertyChanged.

Xamarin.Forms.Maps 2.3.4 custom MapRenderer disables everything

My problem occurs after I updated Xamarin.Forms and Xamarin.Forms.Maps to the new version (2.3.4).
After that I also updated all google play services in Android project (and a lot of libraries that I hate).
The main problem is that I have a custom MapRenderer for custom pins, in iOS and UWP works fine, but in Android version this custom MapRenderer brokes all the Map. Any property change or method call seems to be ignored.
For example I have a button to toggle the map type (Hybrid or Street) and that action never changes it. I also noticed (according this tutorial: https://developer.xamarin.com/guides/xamarin-forms/application-fundamentals/custom-renderer/map/customized-pin/) that the property "VisibleRegion" never changes so the following code never executes:
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName.Equals("VisibleRegion") && !_isDrawn)
{
// Do something with your custom map
}
}
Inside that if i used to populate my custom pins (like the tutorial above) and now my Map is always empty.
Now i populate my map inside the IOnMapReadyCallback and it works fine, but the I still have the bindings problem.
If I ignore the custom MapRendered (removing the assembly line) all the bindings starts working fine but my map now have the old pins and all customization is gone (obviously).
In the PCL I have things like MyMap.MoveToRegion(...) and MyMap.MapType = _currentType; but those instructions only works if a don't use a custom MapRenderer.
My custom MapRenderer is almost the same as the tutorial above.
The custom Map is created with C# and not with XAML, it doesn't have any XAML binding but any property change or method call like the MoveToRegion or MapType is totally ignored if i'm using the MapRenderer.
Any help?
Thanks
I already found the solution.
Looking at the source code, MapRenderer already implements IOnMapReadyCallback and if you remove the implementation in the custom MapRendered, everything starts working again (but with no customization).
MapRenderer saves the google map instance in the property NativeMap (also exists the property Map that is the Xamarin forms map instance) so we don't need to implement IOnMapReadyCallback any more. I think we need to be careful in the use of NativeMap because at the begining it could be null.
In the method I mentioned before now i do this:
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName.Equals("VisibleRegion") && !_isDrawn)
{
PopulateMap();
OnGoogleMapReady();
}
}
and the code I had in OnMapReady now goes inside OnGoogleMapReady():
private void OnGoogleMapReady()
{
if (_mapReady) return;
NativeMap.InfoWindowClick += OnInfoWindowClick;
NativeMap.SetInfoWindowAdapter(this);
_mapReady = true;
}
I also added this in OnElementChanged to remove any registered delegate in NativeMap
if (e.OldElement != null)
{
NativeMap.InfoWindowClick -= OnInfoWindowClick;
}
At the moment exists a Pull Request that implements OnMapReady as virtual method, so we can override it in our implementation and now be sure when NativeMap is not null, but for that we need to wait for a next release.
You can read more here -> https://forums.xamarin.com/discussion/92565/android-ionmapreadycallback-forms-2-3-4
I got the same issue and I solved it thanks to this answer on a Xamarin Forum.
This is my map renderer (Android part) to replace the marker's image of a pin :
[assembly: ExportRenderer(typeof(CustomMap), typeof(CustomMapRenderer))]
namespace MyNamespace.Droid
{
public class CustomMapRenderer : MapRenderer, GoogleMap.IInfoWindowAdapter, IOnMapReadyCallback
{
GoogleMap map;
List<CustomPin> customPins;
bool isDrawn;
protected override void OnElementChanged(Xamarin.Forms.Platform.Android.ElementChangedEventArgs<Map> e)
{
base.OnElementChanged(e);
if (e.OldElement != null)
{
map.InfoWindowClick -= OnInfoWindowClick;
}
if (e.NewElement != null)
{
var formsMap = (CustomMap)e.NewElement;
customPins = formsMap.CustomPins;
Control.GetMapAsync(this);
}
}
protected override void OnElementPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName.Equals ("VisibleRegion") && !isDrawn) {
map.Clear ();
foreach (var pin in customPins) {
var marker = new MarkerOptions();
marker.SetPosition (new LatLng(pin.Pin.Position.Latitude, pin.Pin.Position.Longitude));
marker.SetTitle (pin.Pin.Label);
marker.SetSnippet (pin.Pin.Address);
marker.SetIcon (BitmapDescriptorFactory.FromResource (Resource.Drawable.fake_ic_pin));
map.AddMarker (marker);
}
isDrawn = true;
}
}
protected override void OnLayout(bool changed, int l, int t, int r, int b)
{
base.OnLayout(changed, l, t, r, b);
if (changed)
{
isDrawn = false;
}
}
void OnInfoWindowClick(object sender, GoogleMap.InfoWindowClickEventArgs e)
{
var customPin = GetCustomPin(e.Marker);
if (customPin == null)
{
throw new Exception("Custom pin not found");
}
if (!string.IsNullOrWhiteSpace(customPin.Url))
{
var url = Android.Net.Uri.Parse(customPin.Url);
var intent = new Intent(Intent.ActionView, url);
intent.AddFlags(ActivityFlags.NewTask);
Android.App.Application.Context.StartActivity(intent);
}
}
void IOnMapReadyCallback.OnMapReady(GoogleMap googleMap)
{
InvokeOnMapReadyBaseClassHack(googleMap);
map = googleMap;
map.SetInfoWindowAdapter(this);
map.InfoWindowClick += OnInfoWindowClick;
}
public Android.Views.View GetInfoContents(Marker marker)
{
return null;
}
public Android.Views.View GetInfoWindow(Marker marker)
{
return null;
}
CustomPin GetCustomPin(Marker annotation)
{
var position = new Position(annotation.Position.Latitude, annotation.Position.Longitude);
foreach (var pin in customPins)
{
if (pin.Pin.Position == position)
{
return pin;
}
}
return null;
}
void InvokeOnMapReadyBaseClassHack(GoogleMap googleMap)
{
System.Reflection.MethodInfo onMapReadyMethodInfo = null;
Type baseType = typeof(MapRenderer);
foreach (var currentMethod in baseType.GetMethods(System.Reflection.BindingFlags.NonPublic |
System.Reflection.BindingFlags.Instance |
System.Reflection.BindingFlags.DeclaredOnly))
{
if (currentMethod.IsFinal && currentMethod.IsPrivate)
{
if (string.Equals(currentMethod.Name, "OnMapReady", StringComparison.Ordinal))
{
onMapReadyMethodInfo = currentMethod;
break;
}
if (currentMethod.Name.EndsWith(".OnMapReady", StringComparison.Ordinal))
{
onMapReadyMethodInfo = currentMethod;
break;
}
}
}
if (onMapReadyMethodInfo != null)
{
onMapReadyMethodInfo.Invoke(this, new[] { googleMap });
}
}
}
}

Categories

Resources