Monodroid Javascript Call-back

后端 未结 6 1860
时光取名叫无心
时光取名叫无心 2020-12-30 11:53

I\'m trying to use monodroid with webkit to create an app. I am having a problem with letting the html page call a javascript method, which would be an interface to a method

相关标签:
6条回答
  • 2020-12-30 12:25

    Let's take a step back. You want to invoke C# code from JavaScript. If you don't mind squinting just-so, it's quite straightforward.

    First, let's start with our Layout XML:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
            android:orientation="vertical"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent">
        <WebView
                android:id="@+id/web"
                android:layout_width="fill_parent" 
                android:layout_height="wrap_content" 
        />
    </LinearLayout>
    

    Now we can get to app itself:

    [Activity (Label = "Scratch.WebKit", MainLauncher = true)]
    public class Activity1 : Activity
    {
        const string html = @"
    <html>
    <body>
    <p>This is a paragraph.</p>
    <button type=""button"" onClick=""Foo.run()"">Click Me!</button>
    </body>
    </html>";
    
        protected override void OnCreate (Bundle bundle)
        {
            base.OnCreate (bundle);
    
            // Set our view from the "main" layout resource
            SetContentView (Resource.Layout.Main);
    
            WebView view = FindViewById<WebView>(Resource.Id.web);
            view.Settings.JavaScriptEnabled = true;
            view.SetWebChromeClient (new MyWebChromeClient ());
            view.LoadData (html, "text/html", null);
            view.AddJavascriptInterface(new Foo(this), "Foo");
        }
    }
    

    Activity1.html is the HTML content we're going to show. The only interesting thing is that we provide a /button/@onClick attribute which invokes the JavaScript fragment Foo.run(). Note the method name ("run") and that it starts with a lowercase 'r'; we will return to this later.

    There are three other things of note:

    1. We enable JavaScript with view.Settings.JavaScriptEnabled=true. Without this, we can't use JavaScript.
    2. We call view.SetWebChromeClient() with an instance of a MyWebChromeClient class (defined later). This is a bit of "cargo-cult programming": if we don't provide it, things don't work; I don't know why. If we instead do the seemingly equivalent view.SetWebChromeClient(new WebChromeClient()), we get an error at runtime:

      E/Web Console( 4865): Uncaught ReferenceError: Foo is not defined at data:text/html;null,%3Chtml%3E%3Cbody%3E%3Cp%3EThis%20is%20a%20paragraph.%3C/p%3E%3Cbutton%20type=%22button%22%20onClick=%22Foo.run()%22%3EClick%20Me!%3C/button%3E%3C/body%3E%3C/html%3E:1
      

      This makes no sense to me either.

    3. We call view.AddJavascriptInterface() to associate the JavaScript name "Foo" with an instance of the class Foo.

    Now we need the MyWebChromeClient class:

    class MyWebChromeClient : WebChromeClient {
    }
    

    Note that it doesn't actually do anything, so it's all the more interesting that just using a WebChromeClient instance causes things to fail. :-/

    Finally, we get to the "interesting" bit, the Foo class which was associated above with the "Foo" JavaScript variable:

    class Foo : Java.Lang.Object, Java.Lang.IRunnable {
    
        public Foo (Context context)
        {
            this.context = context;
        }
    
        Context context;        
    
        public void Run ()
        {
            Console.WriteLine ("Foo.Run invoked!");
            Toast.MakeText (context, "This is a Toast from C#!", ToastLength.Short)
            .Show();
        }
    }
    

    It just shows a short message when the Run() method is invoked.

    How this works

    During the Mono for Android build process, Android Callable Wrappers are created for every Java.Lang.Object subclass, which declares all overridden methods and all implemented Java interfaces. This includes the above Foo class, resulting in the Android Callable Wrapper:

    package scratch.webkit;
    
    public class Foo
        extends java.lang.Object
        implements java.lang.Runnable
    {
        @Override
        public void run ()
        {
            n_run ();
        }
    
        private native void n_run ();
    
        // details omitted for clarity
    }
    

    When view.AddJavascriptInterface(new Foo(this), "Foo") was invoked, this wasn't associating the JavaScript "Foo" variable with the C# type. This was associating the JavaScript "Foo" variable with an Android Callable Wrapper instance associated with the instance of the C# type. (Ah, indirections...)

    Now we get to the aforementioned "squinting." The C# Foo class implemented the Java.Lang.IRunnable interface, which is the C# binding for the java.lang.Runnable interface. The Android Callable Wrapper thus declares that it implements the java.lang.Runnable interface, and declares the Runnable.run method. Android, and thus JavaScript-within-Android, does not "see" your C# types. They instead see the Android Callable Wrappers. Consequently, the JavaScript code isn't calling Foo.Run() (capital 'R'), it's calling Foo.run() (lowercase 'r'), because the type that Android/JavaScript has access to declares a run() method, not a Run() method.

    When JavaScript invokes Foo.run(), then the Android Callable Wrapper scratch.webview.Foo.run() method is invoked which, through the joy that is JNI, results in the execution of the Foo.Run() C# method, which is really all you wanted to do in the first place.

    But I don't like run()!

    If you don't like having the JavaScript method named run(), or you want parameters, or any number of other things, your world gets much more complicated (at least until Mono for Android 4.2 and [Export] support). You would need to do one of two things:

    1. Find an existing bound interface or virtual class method that provides the name and signature that you want. Then override the method/implement the interface, and things look fairly similar to the example above.
    2. Roll your own Java class. Ask on the monodroid mailing list for more details. This answer is getting long as it is.
    0 讨论(0)
  • 2020-12-30 12:27

    @kogr's answer worked for me. You do not need to inherit from IRunnable. You do need to add a reference to Mono.Android.Export.dll and tag the exposed public class method with [Export] as he suggests.

    My exposed method returned void, not string, but other than that it was identical.

    0 讨论(0)
  • 2020-12-30 12:32

    Adding this answer to complement the two options under "But I don't like run()!" in the jonp's asnwer (because I don't like run!).

    The [Export] attribute, which is now available, requires a paid edition of Xamarin.
    If you are using the free edition, the following workaround might work for you.

    Inherit from Android.Webkit.WebChromeClient, override the OnJsAlert method, and have your web page "call methods" by using the alert() function, passing serialized data in the message.

    This should be no problem because we need WebChromeClient anyway, and you are supposed to have full control over the web page source code too.

    E.g.:

    private class AlertableWebChromeClient : Android.Webkit.WebChromeClient
    {
        private const string XAMARIN_DATA_ALERT_TAG = "XAMARIN_DATA\0";
    
        public override bool OnJsAlert(Android.Webkit.WebView view, string url, string message, Android.Webkit.JsResult result)
        {
            if (message.StartsWith(XAMARIN_DATA_ALERT_TAG))
            {
                //Parse 'message' for data - it can be XML, JSON or whatever
    
                result.Confirm();
                return true;
            }
            else
            {
                return base.OnJsAlert(view, url, message, result);
            }
        }
    }
    

    In the web page:

    if (window.xamarin_alert_callback != null) {
        alert("XAMARIN_DATA\0" + JSON.stringify(data_object));
    }
    

    The xamarin_alert_callback is used as a flag and can be set in various ways (e.g. WebView.LoadURL(javascript:...) or WebView.AddJavascriptInterface(something, "xamarin_alert_callback")) to let the page know it runs under the alert-enabled browser.

    0 讨论(0)
  • 2020-12-30 12:35

    I found that calling LoadData after AddJavascriptInterface was necessary. Also, with this change, assigning a WebChromeClient was not needed. For example, the modified version below runs fine:

    public class Activity1 : Activity
    {
        const string html = @"
        <html>
        <body>
        <p>This is a paragraph.</p>
        <button type=""button"" onClick=""Foo.run()"">Click Me!</button>
        </body>
        </html>";
    
        protected override void OnCreate (Bundle bundle)
        {
            base.OnCreate (bundle);
    
            SetContentView (Resource.Layout.Main);
    
            WebView view = FindViewById<WebView> (Resource.Id.web);
            view.Settings.JavaScriptEnabled = true;
    
            view.AddJavascriptInterface (new Foo (this), "Foo");
            view.LoadData (html, "text/html", null);
        }
    
        class Foo : Java.Lang.Object, Java.Lang.IRunnable
        {
            Context context;
    
            public Foo (Context context)
            {
                this.context = context;
            }
    
            public void Run ()
            {
                Toast.MakeText (context, "This is a Toast from C#!", ToastLength.Short).Show ();
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-30 12:41

    In addition to what jonp said, if you want to send strings back and forth to JavaScript and C# in Android / Xamarin, for instance JSON strings, and you can't use the [Export] attribute, you could try using

    Android.Net.UrlQuerySanitizer.IValueSanitizer
    

    It only has one method

    string Sanitize(string value);
    

    which is handy for passing strings (JSON) between JS and C#. Don't forget to use lower case sanitize in your JavaScript code.

    0 讨论(0)
  • 2020-12-30 12:43
    // C#
    // !!!
    using Java.Interop; // add link to Mono.Android.Export
    
    public class Activity1 : Activity
    {
        const string html = @"
        <html>
        <body>
        <p>This is a paragraph.</p>
        <button type=""button"" onClick=""Foo.SomeMethod('bla-bla')"">Click Me!</button>
        </body>
        </html>";
    
        class Foo : Java.Lang.Object // do not need Java.Lang.IRunnable 
        {
            Context context;
    
            public Foo (Context context)
            {
                this.context = context;
            }
    
            [Export] // !!! do not work without Export
            [JavascriptInterface] // This is also needed in API 17+
            public string SomeMethod(string param)
            {
                Toast.MakeText (context, "This is a Toast from C#!" + param, ToastLength.Short).Show ();
            }
        }
    
        protected override void OnCreate (Bundle bundle)
        {
            base.OnCreate (bundle);
    
            SetContentView (Resource.Layout.Main);
    
            WebView view = FindViewById<WebView> (Resource.Id.web);
            view.Settings.JavaScriptEnabled = true;
    
            view.AddJavascriptInterface (new Foo (this), "Foo");
            view.LoadData (html, "text/html", null);
        }
    }
    
    0 讨论(0)
提交回复
热议问题