Tracking down WebView URL’s Changes in Xamarin Forms

Hi guys, in this post I just want to share a simple tip that maybe you guys gonna need it. So, in my latest project, I created an app that had a webview on it. Among those web pages that I need to show, there was a certain page that I couldn’t display to the user because it didn’t work properly. In order to keep the business process run well, I had to navigate back to native page whenever user reach that page. In short, I need put a tracker on my webview. I look up to the internet but couldn’t find anything that suit my need so I came up with simple solution.

Custom WebView

The Original Xamarin Forms’s Webview of course didn’t support this tracking feature, even tough you bind a certain variable to source property of web view, it won’t change when you navigate to to other pages. So, we gonna need new property, I call it CurrentUrl.

public class CustomWebView : WebView
{
    public static BindableProperty CurrentUrlProperty =
        BindableProperty.Create(nameof(CurrentUrl), typeof(string), typeof(CustomWebView), null, BindingMode.TwoWay);

    public string CurrentUrl
    {
        get { return (string)GetValue(CurrentUrlProperty); }
        set { SetValue(CurrentUrlProperty, value); }
    }
}

I gonna use this new property to store the current url the web view is showing. No, let see how to store that current url.

WebView Delegate

Because my project requirement only for iOS, for now I just gonna show you how to do it in iOS. But I guess it won’t be much different in Android side.

So, I store the current URL on method ShouldStartLoad in UIWebViewDelegate. That method is run whenever you’re loading  a new page. This is also the method where you put header to web page, if it’s needed.  This is how the renderer and delegate look like.

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

public class CustomUiWebViewDelegate : UIWebViewDelegate
{
CustomWebViewRenderer customWebViewRenderer;
    public CustomUiWebViewDelegate(CustomWebViewRenderer _webViewRenderer = null)
    {
        customWebViewRenderer = _webViewRenderer ?? new CustomWebViewRenderer();
    }

    public override bool ShouldStartLoad(UIWebView webView, NSUrlRequest request, UIWebViewNavigationType navigationType)
    {
       var wv = customWebViewRenderer.Element as CustomWebView;
       wv.CurrentUrl = request.Url.ToString();
       return true;
    }
}

View Model

The last thing you need to do is binding that CurrentUrl property to certain variable in view model. So, whenever it changes, you can trigger something to be done. In the example below, I bind it to a variable with the same name, CurrentUrl.

public class WebViewViewModel : BaseViewModel
{
    private string currentUrl;
    public string CurrentUrl
    {
        get => currentUrl;
        set
        {
            SetProperty(ref currentUrl, value);
DoSomething(value);
        }
    }

    public WebViewViewModel()
    { }

    private void DoSomething(string url)
    {
        // actually do something here<span id="mce_SELREST_start" style="overflow:hidden;line-height:0;">&#65279;</span>
    }
}

Adding Authorization Header in Web View

So, this is my second post about web view. Still from my last project, I had to deal with some problems regarding web view. One of them was adding authorization header in web view. When I look up on net, there were so many answers, but most of them didn’t work for me. So, I thought I should share which method that works for me.

Android Web View Renderer

In Android, I simply put the Authorization header on web view renderer. In some forums, there’re some different opinions about where we should put this header, like put it in web chrome client, but those methods didn’t work in my case. So, this is how I added the authorization header in my project.

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

    protected override void OnElementChanged(ElementChangedEventArgs e)
    {
        base.OnElementChanged(e);
        var webView = Control as Android.Webkit.WebView;
        var Token = Settings.AccessToken;

        Dictionary headers = new Dictionary()
        {
            {"Authorization", "Bearer " + Token }
        };
        this.Control.LoadUrl(Control.Url, headers);
    }

}

I put it on OnElementChanged method, as a result, whenever the user navigate to other page in web view, causing the url changed, this method will be fired. So, the header will be added whenever user make new request in web view.

iOS Web View Delegate

Different from it’s Android counter part, we can’t add the Authorization header on web view renderer in iOS, we have to put it on web view delegate. In iOS web view delegate, there’s a method called ShouldStartLoad

Just like its name, this method is fired whenever the web view load new page. So, it’s actually pretty similar with the Android, but the way this method works is little bit different. In Nutshell, what we gonna do in this method are :

  1. Checking if the request has Authorization Header
  2.  If not, Copy the request and then add the header to the copied request.
  3. Cancel the original request without Authorization Header
  4. If the request already has the header, just let it through

So, this how it looks like in real code.

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

}

public class HybridUiWebViewDelegate : UIWebViewDelegate
{
    public override bool ShouldStartLoad(UIWebView webView, NSUrlRequest request, UIWebViewNavigationType navigationType)
    {
        if (!request.Headers.ContainsKey(new NSString("Authorization")))
        {
            var copy = request.MutableCopy() as NSMutableUrlRequest;
            var token = Settings.AccessToken;

            NSMutableDictionary dic = new NSMutableDictionary();
            dic.Add(new NSString("Authorization"), new NSString("Bearer " + token));
            copy.Headers = dic;

           string currentUrl = request.Url.ToString();

           if (currentUrl.ToLower() != StaticVariables.CurrentUrl.ToLower())
           {
               StaticVariables.CurrentUrl = currentUrl;
               StaticVariables.NavigationStack.Add(currentUrl);
           }

           webView.LoadRequest(copy);

           return false;
      }
      return true;
    }
}

Hope this method also works for you.

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: