Ejecutar Socket.io en Xamarin.Android


Es muy frecuente querer ejecutar esta grandiosa tecnología en la mayoría de recursos que usamos, por lo que esta vez les enseñaré a implementar Socket.io sin necesidad de usar terceros y sacarse las canas entre versiones y dependencias en servidor, etc etc… como me pasó a mi.

La ejecución es muy sencilla, tenemos un WebView donde se ejecuta código en Razor, HTML y un poco de JS donde se hará un hilo de comunicación de doble vía entre el WebView y el Code-Behind, a esta técnica la llamé BlackOps, ya que aquí se harán todas estas tareas oscuras y transparentes al usuario final, la cual darán una sensación de que corre de manera nativa.

  • En Xamarin Studio crear un nuevo proyecto tipo “Android WebView Application”, en este caso lo llamaré “EtonMessy_And”. 
  • En la carpeta Assets crear otra carpeta llamada “js” y crear un archivo llamado “blackOpsHelper”, descargar el cliente de Socket.io desde https://cdn.socket.io/socket.io-1.0.4.js y JQuery desde http://code.jquery.com/jquery-1.11.0.min.js 
  • En el archivo RazorView.cshtml agregar las referencias necesarias a los script.
  • Como buena práctica es encapsular toda la lógica en un archivo *.js que podría llamarse BlackOpsHelper.js, sin embargo, por razones del diseño del control WebView en Xamarin no es posible ejecutar llamados a Internet (probablemente se corrija más adelante), por el momento..  añadir el código base que he diseñado en una etiqueta “script”.

var NodeServerIP = 'http://191.236.166.41:3000/';

var NodeHelper = (function () {
    var URLSocket = 'Error';
    var socket = io.connect(NodeServerIP);

    this.init = function () {
        URLSocket = NodeServerIP;
    }

    socket.on('Welcome', function (data) {
        console.log(data.Message);
    });

    socket.on('connect', function () {
        console.log('The server is online.');

    });
    socket.on('error', function (data) {
        console.log('Error del servidor: ' + data);
        createLog('error');
    });
    socket.on('disconnect', function () {
        console.log('Disconnected to server.');
        createLog('disconnect');
    });
    socket.on('reconnect', function (data) {
        console.log('Reconnected is ok with ' + data + ' attempts.');
    });
    socket.on('reconnect_attempt', function () {
        attempts++;
        console.log('Trying to connect...' + attempts);
    });
    socket.on('reconnecting', function (data) {
        console.log('Reconnecting with server...' + data);
    });
    socket.on('reconnect_error', function (data) {
        console.log('Error to reconnect: ' + data);
    });
    socket.on('reconnect_failed', function () {
        console.log('Reconnect failed.');
    });

    this.template = function () {
        return (true);
    };

    return (this);
}).call({});
  • Como no es necesario tener implementada una maqueta totalmente compleja, proceder a eliminar el contenido de la etiqueta “body” y a la declaración de Model.
  • Actualmente no es posible ver una depuración del control WebView en Xamarin.Android así que esta vez toca implementar nuestra propia consola de logs para registrar cada movimiento y así poder detectar que está pasando con el código en JavaScript de la vista, para ello solo declaré el siguiente marcado en la etiqueta “body”
    <form name="myform">
        <h1>BlackOps WebView</h1>
        <textarea name="outputtext" id="Log" style="width:100%;height:100%;"></textarea>
    </form>
  • En WinRT se tiene una función llamada external.notify para llevar a cabo llamados desde JS hacia C#, le he hecho un Override a esa función ya que en el estándar no existe para que haga lo que yo necesito, que en este caso es hacer posible la comunicación entre JS->C#, también añadí los llamados para crear los logs, por lo tanto el script creado anteriormente queda de la siguiente manera.

 

        var NodeServerIP = 'http://23.96.194.158:3000/';

        var BlackOps = (function () {
            var URLSocket = 'Error';
            var socket = io.connect(NodeServerIP);

            this.init = function () {
                URLSocket = NodeServerIP;

                /// Create an event in document called Log and send a text like parameter
                $(document).trigger("Log", ['init BlackOps']);
            }

            socket.on('Welcome', function (data) {
                $(document).trigger("ComplexLog", [data]);
            });

            socket.on('ReceiveTest', function(data){
            	$(document).trigger("ComplexLog", [data]);
            })

            socket.on('connect', function () {
                $(document).trigger("Log", ['The server is online.']);

            });
            socket.on('error', function (data) {
                $(document).trigger("Log", ['Error del servidor']);
            });
            socket.on('disconnect', function () {
                $(document).trigger("Log", ['Disconnected to server.']);
            });
            socket.on('reconnect', function (data) {
                $(document).trigger("Log", ['Reconnected is ok']);
            });
            socket.on('reconnect_attempt', function () {
                $(document).trigger("Log", ['Trying to connect...']);
            });
            socket.on('reconnecting', function (data) {
                $(document).trigger("Log", ['Reconnecting with server...']);
            });
            socket.on('reconnect_error', function (data) {
                $(document).trigger("Log", ['Error to reconnect']);
            });
            socket.on('reconnect_failed', function () {
                $(document).trigger("Log", ['Reconnect failed']);
            });

            socket.on('Message', function (data) {
                $(document).trigger("MessageReceived", [data]);
            });

            /// When this function is called try to send
            /// a message to server, the Server will have an event
            /// called with the same name
            this.SendMessage = function(message){
            	$(document).trigger("Log", ['Prepared to send Message...']);

            	socket.emit('ToServer', {Message: message});
            };

            /// Execute a C# code from Javascript
            this.ScriptNotify = function (command, args) {
                location.href = "hybrid:" + command + "?" + args;
            }

            return (this);
        }).call({});

        /// A litle hack for WinRT Dev's:
        /// Declared for handle request into native app
        external.notify = function (command, args) {

            BlackOps.ScriptNotify(command, args);

            console.log(command + ": " + args);

        };
  • Sin embargo todavía hace falta declarar los listener de los eventos invocados y lo más importante inicializar todo el código anterior, para ello es necesario crear otra etiqueta “script” ya que son funcionalidades totalmente separadas. Cabe aclarar que estos dos scripts están diseñados para ser encapsulados en archivos distintos y tener otra arquitectura más decente, sin embargo esto será declarado en el archivo RazorView.cshtml, solo por recordarlo.
        $(document).trigger("Log", ['Script loaded']);

        $(document).ready(function () {

            $(document).trigger("Log", ['DOM loaded']);
            BlackOps.init();
        })

        // This javascript method is called from C#
        function SendMessage2Server(text) {
            $(document).trigger("Log", ['Message from C#: ' + text]);

            BlackOps.SendMessage(text);
        }

        $(document).on("MessageReceived", function (event, data) {
            external.notify("Notify2App", "Data=" + data);
        });

        $(document).on("ComplexLog", function (event, data){
        	var logs = $("#Log").val();
            $("#Log").val(logs + "\n- " + data.Message);
        });

        $(document).on("Log", function (event, data) {
            var logs = $("#Log").val();
            $("#Log").val(logs + "\n- " + data);
        });
    • Con la funcionalidad hecha en el “FrontEnd” donde capturaremos y manipularemos todos los eventos de Socket.io es necesario darle el control necesario a Xamarin.Android; para ello se necesitarán dos cosas, la primera es modificar la vista Main.axml y modificar completamente el Code Behind de esta. En la vista solo declarar un botón y arreglar el tamaño del WebView, este botón es para enviar un mensaje al servidor mediante BlackOps
<?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"
    android:minWidth="25px"
    android:minHeight="25px">
    <WebView
        android:layout_width="fill_parent"
        android:layout_height="218.3dp"
        android:id="@+id/webView"
        android:layout_marginBottom="0.0dp"
        android:clipToPadding="true"
        android:clipChildren="true" />
    <Button
        android:text="Send Data to server"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@+id/sendDataButton" />
</LinearLayout>
  • Por último adjunto el código del Code Behind, está explicado el código más relevante
using System;
using Android.App;
using Android.Content;
using Android.Runtime;
using Android.Views;
using Android.Webkit;
using Android.Widget;
using Android.OS;
//using EtonMessy_And.Views;
using EtonMessy_And.Models;

namespace EtonMessy_And
{
    [Activity(Label = "EtonMessy", MainLauncher = true)]
    public class MainActivity : Activity
    {
		HybridWebViewClient hybridWebClient;
		WebView webView;

        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);

            // Set our view from the "main" layout resource
            SetContentView(Resource.Layout.Main);

			webView = FindViewById<WebView>(Resource.Id.webView);
			Button sendDataButton = FindViewById<Button> (Resource.Id.sendDataButton);

			hybridWebClient = new HybridWebViewClient (this);

			// Used for handle Click Event
			sendDataButton.Click += SendDataButton_Click;

			// For use Socket.io in WebView
            webView.Settings.JavaScriptEnabled = true;

            // Use subclassed WebViewClient to intercept hybrid native calls
			webView.SetWebViewClient(hybridWebClient);

            // Render the view from the type generated from RazorView.cshtml
            var template = new RazorView();
            var page = template.GenerateString();

            // Load the rendered HTML into the view with a base URL
            // that points to the root of the bundled Assets folder
            webView.LoadDataWithBaseURL("file:///android_asset/", page, "text/html", "UTF-8", null);

            //webView.LoadUrl("http://www.google.com");
        }

		void SendDataButton_Click (object sender, EventArgs e)
		{
			if (webView != null)
			{
				// Notify the JS and send Message
				hybridWebClient.SendMessage2Server (webView, "Hello I am an Android!.");
			}
		}

        private class HybridWebViewClient : WebViewClient
        {
            Activity context;
            public HybridWebViewClient(Activity context)
            {
                this.context = context;
            }
            public override bool ShouldOverrideUrlLoading(WebView webView, string url)
            {

                // If the URL is not our own custom scheme, just let the webView load the URL as usual
                var scheme = "hybrid:";

                if (!url.StartsWith(scheme))
                    return false;

                // This handler will treat everything between the protocol and "?"
                // as the method name.  The querystring has all of the parameters.
                var resources = url.Substring(scheme.Length).Split('?');
                var method = resources[0];
                var parameters = System.Web.HttpUtility.ParseQueryString(resources[1]);

                switch (method)
                {
                    case "Send2Server":
                        SendMessage2Server(webView, url);
                        break;
                    case "Notify2App":
                        Toast.MakeText(context, "Handle the link in c#\n" + url, ToastLength.Short).Show();
                        break;
                    default:
                        break;
                }

                return true;
            }

			/// <summary>
			/// Method to invoke a function in JS
			/// </summary>
			/// <param name="webView">Current Webview.</param>
			/// <param name="args">Message to send.</param>
            public void SendMessage2Server(WebView webView, string args)
            {
				string jsCode = string.Format("javascript: SendMessage2Server('{0}');", args);

                webView.LoadUrl(jsCode);
            }
        }
    }
}

Por último, nos falta dejar volar nuestra imaginación, ya que con este método no estamos dependiendo de ningún tercero, si no de nuestra habilidad para desarrollar, entiendo que es un método de cierta manera arcaico y en algunas ocasiones ineficiente pero hasta el momento la mejor manera de como pude solucionar los retos que tenía.

También hay que aclarar que el código del servidor tuvo que ser modificado para recibir los mensajes enviados.

Un comentario en “Ejecutar Socket.io en Xamarin.Android

  1. hola muy bueno tu post pero puedes dejar el ejemplo como una solucion descargable para poder hacer pruebas. Saludos

Los comentarios están cerrados.