Monodroid JavaScript 回调

8
我正在尝试使用monodroid和webkit创建一个应用程序。我遇到了一个问题,即让HTML页面调用JavaScript方法,该方法将是我应用程序中的方法接口。在http://developer.android.com/guide/webapps/webview.html上有一个教程,介绍了如何在Java中完成此操作,但是同样的代码在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按钮调用C#方法,或者让我的C#代码调用Javascript方法。

6个回答

23

让我们退后一步。您想从JavaScript调用C#代码。如果您不介意努力看清,这非常简单。

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

现在我们可以进入应用本身:

[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. 我们使用一个MyWebChromeClient类的实例来调用view.SetWebChromeClient()(稍后定义)。这有点像“ cargo-cult programming”,如果我们不提供它,事情就无法正常工作;我不知道为什么。如果我们改用看似等效的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的实例关联。

现在我们需要MyWebChromeClient类:

class MyWebChromeClient : WebChromeClient {
}
注意,它实际上并没有做任何事情,所以更有趣的是,只使用一个WebChromeClient实例就会导致事情失败。 :-/
最后,我们来到了“有趣”的部分,即与JavaScript变量"Foo"相关联的Foo类:
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,这包括了上面的Foo类,声明了所有重写的方法和实现的Java接口,从而生成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 能够访问的类型声明了一个run()方法,而不是一个Run()方法。
当 JavaScript 调用 Foo.run() 时,会调用 Android Callable Wrapper scratch.webview.Foo.run() 方法,通过 JNI 的乐趣,最终执行 Foo.Run() C# 方法,这实际上是你最初想做的事情。
但我不喜欢 run()!
如果您不喜欢将 JavaScript 方法命名为run(),或者希望添加参数等其他任何内容,您的世界会变得更加复杂(直到 Mono for Android 4.2 和[Export]支持)。您需要执行以下两个步骤之一:
1.找到一个已经绑定的接口或虚拟类方法,它提供了您想要的名称和签名。然后重写该方法/实现该接口,情况看起来与上面的示例相似。
2.自己编写 Java 类。在monodroid邮件列表上询问更多详细信息。这个答案已经够长了。

IRunnable是必需的还是只是因为原始问题使用了它而包含在内? @api 17中@JavascriptInterface的新要求如何影响解决方案? - hultqvist
@jonp 兄弟,有人告诉过你吗,你是真正的摇滚巨星! - Aiden Strydom
截至目前,[Export] 属性仅在付费版本的 Xamarin 中可用,免费版本不包含此功能。 - GSerg
我已经添加了一个答案来补充这两个选项。 - GSerg

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

1
SomeMethod里面不应该有一个返回语句吗? - Radu Simionescu
2
好的答案。请注意,如果您想针对Android API 17+进行目标设置,则需要在SomeMethod(string)方法上注释[JavascriptInterface]以及[Export]。并且您可以通过将[Export]注释更改为[Export("SomeOtherMethod")]来指定要导出的方法的名称。 - Adil Hussain
谢谢@AdilHussain,你说得对。我实现了kogr的解决方案(上面),但在添加[JavascriptInterface]属性之前无法正常运行。我已编辑了解决方案以显示此内容。PS:我在我的测试Android设备上运行API 19。 - Has AlTaiar

2

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

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


2
除了 jonp所说的之外,如果您想在Android / Xamarin中将字符串来回发送到JavaScript和C#,例如JSON字符串,并且无法使用[Export]属性,则可以尝试使用。
Android.Net.UrlQuerySanitizer.IValueSanitizer

它只有一个方法。
string Sanitize(string value);

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

2

为了补充 jonp的答案中"But I don't like run!"下的两个选项,我在这里提供另一种解决方法(因为我不喜欢run())。

[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")),以便让页面知道它是在启用警报的浏览器下运行。


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

有没有办法通过回调从C#将数据传递回javascript。我可以像你提到的那样从Javascript调用C#函数。但是,在处理本地代码后,我想将一些数据传递回Javascript并在网页上显示它。这可行吗? - User382

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接