Monodroid Javascript回拨

时间:2012-03-16 22:13:15

标签: javascript xamarin.android

我正在尝试使用monodroid和webkit来创建一个应用程序。我有一个问题,让html页面调用一个javascript方法,这将是我的应用程序中的方法的接口。有关如何在java中执行此操作的http://developer.android.com/guide/webapps/webview.html有一个教程,但相同的代码不适用于C#。

call monodroid method from javascript example的这个交换链接了一些关于使用JNI解决monodroid和javascript接口方法问题的线程,但是我无法让它工作。

现在,我正在尝试使用一些代码指令,但没有成功:

// Java
class RunnableInvoker {
Runnable r;
public RunnableInvoker (Runnable r) {
this.r = r;
}
// must match the javascript name:
public void doSomething() {
r.run ();
}
}

From C#, you'd create a class that implements Java.Lang.IRunnable:

// C#
class SomeAction : Java.Lang.Object, Java.Lang.IRunnable {
Action a;
public void SomeAction(Action a) {this.a = a;}
public void Run () {a();}
}

Then to wire things up:

// The C# action to invoke
var action = new SomeAction(() => {/* ... */});

// Create the JavaScript bridge object:
IntPtr RunnableInvoker_Class = JNIEnv.FindClass("RunnableInvoker");
IntPtr RunnableInvoker_ctor = JNIEnv.GetMethodID (RunnableInvoker_Class, "<init>", "(Ljava/lang/Runnable;)V");
IntPtr instance = JNIEnv.NewObject(RunnableInvoker_Class, RunnableInvoker_ctor, new JValue (action));

// Hook up WebView to JS object
web_view.AddJavascriptInterface (new Java.Lang.Object(instance, JniHandleOwnership.TransferLocalRef), "Android");

这段代码应该可以让人们在应用程序内部的html页面上点击一个按钮,调用java,然后调用C#。这没有用。

我想知道是否有人知道问题是什么,或者是另一个想法,所以我可以使用monodroid让webkit中加载一个html按钮来调用ac#方法,或者能够让我的c#代码调用一个javascript方法。

6 个答案:

答案 0 :(得分:21)

让我们退后一步。您想从JavaScript调用C#代码。如果你不介意眯眼,那就很简单了。

首先,让我们从布局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>

现在我们可以进入app本身:

[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是我们要展示的HTML内容。唯一有趣的是我们提供了一个/button/@onClick属性来调用JavaScript片段Foo.run()。注意方法名称(“run”),它以小写的“r”开头;我们稍后会回到这里。

还有其他三件事需要注意:

  1. 我们使用view.Settings.JavaScriptEnabled=true启用JavaScript。没有这个,我们就不能使用JavaScript。
  2. 我们使用view.SetWebChromeClient()类的实例(稍后定义)调用MyWebChromeClient。这是一个“货物狂热编程”:如果我们不提供它,事情就不起作用;我不知道为什么。如果我们改为看似等效的view.SetWebChromeClient(new WebChromeClient()),我们会在运行时收到错误:

    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
    

    这对我来说也没有意义。

  3. 我们致电view.AddJavascriptInterface()将JavaScript名称"Foo"与班级Foo的实例相关联。
  4. 现在我们需要MyWebChromeClient类:

    class MyWebChromeClient : WebChromeClient {
    }
    

    请注意,它实际上并没有做任何事情,所以使用WebChromeClient实例导致事情失败更有趣。 : - /

    最后,我们进入“有趣”位,Foo类与上面的"Foo" JavaScript变量相关联:

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

    在调用Run()方法时,它只显示一条短消息。

    这是如何工作的

    在Mono for Android构建过程中,为每个Java.Lang.Object子类创建Android Callable Wrappers,它声明所有重写的方法和所有已实现的Java接口。这包括上面的Foo类,导致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
    }
    

    调用view.AddJavascriptInterface(new Foo(this), "Foo")时,这并未将JavaScript "Foo"变量与C#类型相关联。这是将JavaScript "Foo"变量与与C#类型的实例关联的Android Callable Wrapper实例相关联。 (啊,间接......)

    现在我们进入前面提到的“眯眼”。 C#Foo类实现了Java.Lang.IRunnable接口,这是java.lang.Runnable接口的C#绑定。因此,Android Callable Wrapper声明它实现了java.lang.Runnable接口,并声明了Runnable.run方法。 Android,以及Android内部的JavaScript,不会“看到”您的C#类型。他们改为看到Android Callable Wrappers。因此,JavaScript代码不会调用Foo.Run()(大写'R'),而是调用Foo.run()(小写'r'),因为Android / JavaScript有权访问的类型声明{{1 }}方法, run()方法。

    当JavaScript调用Run()时,将调用Android Callable Wrapper Foo.run()方法,该方法通过JNI的快乐导致执行scratch.webview.Foo.run() C#方法,即真的只是你想做的一切。

    但我不喜欢run()!

    如果你不喜欢名为Foo.Run()的JavaScript方法,或者你想要参数或其他任何东西,你的世界会变得更加复杂(至少在Mono for Android 4.2和{{1支持)。你需要做两件事之一:

    1. 查找提供所需名称和签名的现有绑定接口或虚拟类方法。然后覆盖方法/实现接口,事情看起来与上面的例子非常相似。
    2. 滚动您自己的Java类。请致电monodroid mailing list了解更多详情。这个答案很长很长。

答案 1 :(得分:10)

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

答案 2 :(得分:2)

@ kogr的回答对我有用。您不需要从IRunnable继承。您需要添加对Mono.Android.Export.dll的引用,并按照他的建议使用[Export]标记公开的公共类方法。

我公开的方法返回 void ,而不是 string ,但除此之外它是相同的。

答案 3 :(得分:2)

除了what jonp said之外,如果你想在Android / Xamarin中来回发送JavaScript和C#的字符串,例如JSON字符串,并且你不能使用[Export]属性,那么你可以尝试使用

Android.Net.UrlQuerySanitizer.IValueSanitizer

它只有一种方法

string Sanitize(string value);

这对于在JS和C#之间传递字符串(JSON)很方便。不要忘记在JavaScript代码中使用小写清理。

答案 4 :(得分:1)

我发现在AddJavascriptInterface之后调用LoadData是必要的。此外,通过此更改,不需要分配WebChromeClient。例如,下面的修改版本运行良好:

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

答案 5 :(得分:1)

添加此答案以补充“但我不喜欢run()”下的两个选项!在jonp's asnwer(因为我不喜欢跑!)。

[Export]属性现已推出,需要付费版Xamarin 如果您使用的是免费版,则以下解决方法可能适合您。

继承Android.Webkit.WebChromeClient,覆盖OnJsAlert方法,并使用alert()函数让您的网页“调用方法”,并在邮件中传递序列化数据。

这应该没问题,因为我们无论如何都需要WebChromeClient,你也应该完全控制网页源代码。

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

在网页中:

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

xamarin_alert_callback用作标志,可以通过各种方式设置(例如WebView.LoadURL(javascript:...)WebView.AddJavascriptInterface(something, "xamarin_alert_callback")),让页面知道它在启用警报的浏览器下运行。< / p>