【问题标题】:Monodroid Javascript Call-backMonodroid Javascript 回调
【发布时间】:2012-03-16 22:13:14
【问题描述】:

我正在尝试将 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方法。

【问题讨论】:

    标签: javascript xamarin.android


    【解决方案1】:

    让我们退后一步。您想从 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>
    

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

    [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()。这有点像“货物崇拜编程”:如果我们不提供它,事情就不起作用;我不知道为什么。如果我们改为执行看似等效的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 实例会导致事情失败。 :-/

    最后,我们来到了“有趣”的部分,即上面与 "Foo" JavaScript 变量相关联的 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,它声明了所有重写的方法和所有实现的 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 有权访问的类型声明了run() 方法,不是Run() 方法。

    当 JavaScript 调用 Foo.run() 时,Android Callable Wrapper scratch.webview.Foo.run() 方法被调用,通过 JNI 的乐趣,导致 Foo.Run() C# 方法的执行,这正是你想要做的首先。

    但我不喜欢 run()!

    如果您不喜欢名为 run() 的 JavaScript 方法,或者您需要参数或其他任何东西,那么您的世界会变得更加复杂(至少在 Mono for Android 4.2 和 [Export] 支持之前) .您需要做以下两件事之一:

    1. 找到提供所需名称和签名的现有绑定接口或虚拟类方法。然后重写方法/实现接口,看起来和上面的例子很相似。
    2. 滚动您自己的 Java 类。在monodroid mailing list 上询问更多详情。这个答案越来越长了。

    【讨论】:

    • 感谢您的回答!很详细
    • IRunnable 是必需的还是仅在原始问题使用时才包含在内? api 17 中@JavascriptInterface 的新要求对解决方案有何影响?
    • @jonp 老兄有没有人告诉过你你是真正的摇滚明星!
    • +1。目前,[Export] 属性在 Xamarin 的付费版本中可用,免费版本没有。
    • 我添加了an answer 来补充这两个选项。
    【解决方案2】:
    // 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);
        }
    }
    

    【讨论】:

    • SomeMethod 里面不应该有 return 语句吗?
    • 好答案。请注意,如果您想针对 Android API 17+,您需要使用 [JavascriptInterface] 和 [Export] 注释您的 SomeMethod(string) 方法。您还可以选择通过将 [Export] 注释更改为 [Export ("SomeOtherMethod")] 来指定要为导出方法提供的名称。
    • 感谢@AdilHussain,你说得对,我实现了 kogr 的解决方案(上图),直到我添加了 [JavascriptInterface] 属性,它才起作用。我已经编辑了解决方案来显示这一点。 PS 我在我的测试 Android 设备上运行 API 19
    【解决方案3】:

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

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

    【讨论】:

      【解决方案4】:

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

      Android.Net.UrlQuerySanitizer.IValueSanitizer
      

      只有一种方法

      string Sanitize(string value);
      

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

      【讨论】:

        【解决方案5】:

        我发现在 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 并在网页上显示。可行吗?
        【解决方案6】:

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

        现在可用的 [Export] 属性需要 Xamarin 的付费版本。
        如果您使用的是免费版,以下解决方法可能对您有用。

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

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

        例如:

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多