Handle Button Click Event on Web View

In my last project, I was required to load a web view from internet. It was piece of cake, at least what it look like at first. But then it wasn’t that simple anymore when I need to add listener event in C# based from what user did in web view. Let say, the user click a button, it would do whatever it supposed to do in web, but I also had to do something accordingly in my xamarin app based on what button user just clicked. I wasn’t sure if that was even possible, but after wandering all day long in stackoverflow, I found solution for both Android and iOS.

Android Web View Renderer

We move to Android project first. In this project we need create three classes. First, of course the renderer of our web view. Second is web view client, this where we gonna inject our event to html, in this case we will inject it to a button. And then the third is web chrome client where we’ll put event listener to our injected event.

Starting with the renderer, all we need to do is set web view client and web chrome client to our own web view client and chrome client.

 

public class HybridWebViewRenderer : WebViewRenderer
{
    public HybridWebViewRenderer(Context context) : base(context)
    { }
    public HybridWebViewRenderer(){ }

    protected override void OnElementChanged(ElementChangedEventArgs e)
    {
        base.OnElementChanged(e);

        Control.Settings.JavaScriptEnabled = true;
        var webView = Control as Android.Webkit.WebView;

        this.Control.SetWebViewClient(new HybridWebViewClient());
        this.Control.SetWebChromeClient(new HybridWebChromeClient());

        this.Control.LoadUrl(Control.Url);
    }
}

 

Then in web view client, basically what we gonna do is, find the particular button we want and then inject the event. The event is, of course, button click event and we gonna make it show up an alert. Yes, alert! Not just any alert, but an alert with specific keyword that we can recognize it later in web chrome client when we adding event listener.  For this example, I will use ‘dosomething’ and ‘dosomethingelse’ as our keywords. Let see how it looks like.

public class HybridWebViewClient : WebViewClient
{

    public override async void OnPageFinished(WebView view, string url)
    {
        base.OnPageFinished(view, url);

        int i = 10;
        while (view.ContentHeight == 0 && i-- > 0)
            await Task.Delay(1000);
        // find the particular button
        string funcurl = "var btn1 = document.getElementsByClassName('btn-primary')[1]; if(btn1 != null){btn1.addEventListener('click', function() { alert('dosomething'); }); }" +
                         "var btn2 = document.getElementsByClassName('btn-primary')[2]; if(btn2 != null){btn2.addEventListener('click', function() { alert('dosomethingelse'); }); }";
        view.LoadUrl("javascript: r(function(){" + funcurl + ");
        break;

    }
}

In web chrome client, we’ll filter any alert from the html page to find out if any of our alert has beed fired. We’ll override OnJsAlert method, do some process we suppose to do, and then cancel the result and  return it with true value. By doing that it means we won’t show the alert to user because it’s unnecessary for them to see our alert.

public class HybridWebChromeClient : WebChromeClient
{
    public override bool OnJsAlert(WebView view, string url, string message, JsResult result)
    {
        if(message.Contains("doseomething"))
        {
            // do something here
            result.Cancel();
            return true;
        }
        else if(message.Contains("dosomethingelse"))
        {
            // do something else here
            result.Cancel();
            return true;
        }
       return base.OnJsAlert(view, url, message, result);
    }
}

iOS Web View Renderer

In iOS, the process is slightly simpler, because we only need two classes, first the web view renderer class, and the of course the web view delegate class. Just like the Android project, all we need to do in renderer is just setting the delegate class to our own delegate class and the let the delegate class do all the job. This is how the renderer looks like.

public class HybridWebViewRenderer : WebViewRenderer
{
    protected override void OnElementChanged(VisualElementChangedEventArgs e)
    {
        base.OnElementChanged(e);
        Delegate = new HybridUiWebViewDelegate(this);

        this.ScrollView.DecelerationRate = UIScrollView.DecelerationRateNormal;
        var webView = e.NewElement as HybridWebView;

    }
}

Unlike Android, in iOS we don’t have web chrome client to catch javascript alert event, so we need to do something else. All we have is just a delegate class and all its override methods. But among those methods, there’s one method that will be very useful in situation like this. That method called ShouldStartLoad. This method is fired when user start navigating to new url and we can decide programmatically what the app gonna do when it happens. So, what we gonna do is, make the button load certain url, and then we catch the event in ShouldStartLoad method, do what we suppose to there and then cancel the request so web view won’t actually load our url. The url, of course, is a fake url that contain our keyword that we can identify, just like what we did with alert in Android.

public class HybridUiWebViewDelegate : UIWebViewDelegate
{
    HybridWebViewRenderer hybridWebViewRenderer;
    public HybridUiWebViewDelegate(HybridWebViewRenderer _webViewRenderer = null)
    {
        hybridWebViewRenderer = _webViewRenderer ?? new HybridWebViewRenderer();
    }
public override async void LoadingFinished(UIWebView webView)
    {
        var wv = hybridWebViewRenderer.Element as HybridWebView;
        if (wv != null)
        {
           await Task.Delay(100);// wait here till content is rendered

           // find the particular button
           string funcurl = string.Empty;
           funcurl = "var btn1 =  document.getElementsByClassName('btn-primary')[1]; if(btn1 != null){btn1.addEventListener('click', function() { window.location = \"dosomething\"; }); }" +
                     "var btn2 =  document.getElementsByClassName('btn-primary')[2]; if(btn2 != null){reject.addEventListener('click', function() { window.location = \"dosomethingelse\"; }); }";

           webView.EvaluateJavascript("javascript: r(function(){" + funcurl + "});");

    }

    public override bool ShouldStartLoad(UIWebView webView, NSUrlRequest request, UIWebViewNavigationType navigationType)
    {
        if(request.Url.ToString().Contains("dosomething"))
        {
            // do something here
            return false;
        }
        else if(request.Url.ToString().Contains("dosomethingelse"))
        {
            // do something else here
            return false;
       }

      return true;
    }
}

Credit:

Set Placeholder Color of Xamarin’s Picker

In one of my previous post, we’ve discussed about how to adjust font size of Xamarin’s Picker. Font size in picker, especially in Android picker, is simply fixed, so we need a workaround if we want to adjust it. In iOS, font size of the picker is more flexible, you can set height request and width request, or simply put it on a grid, the size of whole picker will follow its container. But there’s another property of picker that also fixed. It’s the placeholder color. I know, most of the time we won’t need to change the placeholder color of the picker. However, for the sake of the app’s theme, sometime we gonna need to set color of the placeholder. So, In case you need it, you can use this method to set the placeholder color.

Custom Picker

To set the placeholder color, first thing we gonna need, of course, a bindable property for it. So we gonna update our CustomPicker from previous post and add Placeholder Color property in it

 

public class CustomPicker : Picker
{
    public static readonly BindableProperty FontSizeProperty =
        BindableProperty.Create(nameof(FontSize), typeof(Int32), typeof(CustomPicker), 24, BindingMode.TwoWay);

    public Int32 FontSize
    {
        set { SetValue(FontSizeProperty, value); }
        get { return (Int32)GetValue(FontSizeProperty); }
    }

    public static BindableProperty PlaceholderColorProperty =
        BindableProperty.Create(nameof(PlaceholderColor), typeof(string), typeof(CustomPicker), "#CCCCCC", BindingMode.TwoWay);

    public string PlaceholderColor
    {
        get { return (string)GetValue(PlaceholderColorProperty); }
        set { SetValue(PlaceholderColorProperty, value); }
    }
}

 

Custom Renderer Android

Next, we update the renderer in Android. In Android, representing the placeholder color, we have property called Hint Text Color. The only problem is Hint Text Color use Color class from Android.Graphic class unlike default Color of Xamarin from Xamarin.Form class. As a result, we need to parse the color. Fortunately, Android.Graphic.Color take hex code as its parameter so we can just simply parse it. The following code is how we do it.

public class CustomPickerRenderer : PickerRenderer
{
    protected override void OnElementChanged(ElementChangedEventArgs e)
    {
        base.OnElementChanged(e);
        this.Control.Background = Forms.Context.GetDrawable(Resource.Drawable.CustomPickerBackground);
        Control?.SetPadding(20, 20, 20, 20);
        if (e.OldElement != null || e.NewElement != null)
        {
            var customPicker = e.NewElement as CustomPicker;
            Control.TextSize *= (customPicker.FontSize * 0.01f);
            Control.SetHintTextColor(Android.Graphics.Color.ParseColor(customPicker.PlaceholderColor));
        }
    }
}

Custom Renderer iOS

Nah, it’s where things got tricky. To set placeholder color, or in iOS term, Foreground Color, we gonna need class named UIColor. And UIColor doesn’t take Hex Code as its parameter. It takes RGB color. Consequently, we need to parse Hex to RGB first. I made three little functions to take the color Red, Green and Blue from our Hex Code.

Finish with the color thing, next we gonna put that color into something, called NSAttributedString. So, what is NSAttributedString? First, Placeholder Attribute in iOS Picker is a NSAttributedString, so we need to make our own NSAttributedString if we want to modify our placeholder. Second, NSAttributedString has many attributes that you’re able to modify. In this example we just use the Foreground Color. but it has many more important attributes such as Background Color , Font, and Stroke Color.

So, this is how the custom renderer in iOS looks like.

public class CustomPickerRenderer: PickerRenderer
{
    protected override void OnElementChanged(ElementChangedEventArgs e)
    {
        base.OnElementChanged(e);
        if (e.OldElement != null || e.NewElement != null)
        {
            var customPicker = e.NewElement as CustomPicker;

            string placeholderColor = customPicker.PlaceholderColor;
            UIColor color = UIColor.FromRGB(GetRed(placeholderColor), GetGreen(placeholderColor), GetBlue(placeholderColor));

            var placeholderAttributes = new NSAttributedString(customPicker.Title,new UIStringAttributes()
            { ForegroundColor = color });

            Control.AttributedPlaceholder = placeholderAttributes;
        }
    }

    private float GetRed(string color)
    {
        Color c = Color.FromHex(color);
        return (float)c.R;
    }

    private float GetGreen(string color)
    {
        Color c = Color.FromHex(color);
        return (float)c.G;
    }

    private float GetBlue(string color)
    {
        Color c = Color.FromHex(color);
        return (float)c.B;
    }
}

Let’s try it out!

I made minor change to example I used in previous post. I add placeholder color and run it in Android and iOS emulator.


    
        
        
        
        
    

 

Sample Code is available in my Github repo.