Febrero de 2018

Volumen 33, número 2

C#: escritura de aplicaciones móviles nativas mediante un lenguaje de scripting personalizable

Por Vassili Kaplan

En la edición de febrero de 2016 de MSDN Magazine, mostré como crear un lenguaje de scripting personalizado basado en el algoritmo de división y combinación para analizar expresiones matemáticas en C# (msdn.com/magazine/mt632273). Denominé a mi lenguaje Scripting personalizable en C# o CSCS. Recientemente, publiqué un libro electrónico que incluía más detalles sobre la creación de un lenguaje personalizado (bit.ly/2yijCod). Es posible que la creación de su propio lenguaje de scripting no parezca en principio especialmente útil, aunque algunas de sus aplicaciones pueden ser interesantes (por ejemplo, trampas en el juego). También encontré algunas aplicaciones en la programación de Unity.

Pero luego descubrí una aplicación aún más interesante para un lenguaje de scripting personalizable: la escritura de aplicaciones multiplataforma para dispositivos móviles. Resulta que es posible usar CSCS para escribir aplicaciones para iOS y Android (Windows Phone también se puede agregar fácilmente). El mismo código se puede usar para todas las plataformas. Publiqué una introducción sobre cómo hacerlo en la edición de noviembre-diciembre de 2017 de CODE Magazine (codemag.com/article/1711081).

En este artículo profundizaré más y mostraré cómo usar CSCS para la programación para dispositivos móviles. También corregiré algunas imprecisiones del artículo de CODE Magazine. Observará que todo lo que se puede hacer en la plataforma nativa se puede hacer en CSCS. También mostraré cómo agregar características que faltan a CSCS sobre la marcha.

Para ejecutar el código que se muestra en este artículo, necesitará Visual Studio 2017 con Xamarin instalado, ya sea en Windows o en macOS. Yo uso Visual Studio Community Edition 2017 en mi MacBook. Tenga en cuenta que se necesita un Mac para implementar aplicaciones de iOS en App Store de Apple.

Hola mundo para Mobile Apps

Eche un vistazo a la Figura 1, que muestra algo de código de CSCS básico para el reconocimiento de voz y de texto a voz. Vamos a examinar el código línea a línea.

Figura 1 Hola mundo en CSCS para Mobile Apps

AutoScale();
voice = "en-US";
locButtonTalk = GetLocation("ROOT", "CENTER", "ROOT", "BOTTOM", 0, 0);
AddButton(locButtonTalk, "buttonTalk", "Click me", 200, 80);
AddAction(buttonTalk,  "talk_click");
function talk_click(sender, arg) {
  ShowToast("Please say your name...");
  VoiceRecognition("voice_recog", voice);
}
function voice_recog(errorStatus, recognized) {
  if (errorStatus != "") {
    AlertDialog("CSCS", "Error: " + errorStatus);
  } else {
    ShowToast("Word recognized: " + recognized);
    Speak("Hello, " + recognized, voice);
  }
}

La función AutoScale permite ajustar automáticamente el tamaño del widget en función del tamaño de pantalla real del dispositivo. Por ejemplo, con AutoScale, el ancho de un widget será el doble tanto en un dispositivo con un ancho de 1280 píxeles como en uno con un ancho de 640 píxeles. La firma real de la función AutoScale es:

AutoScale(scale = 1.0);

Si no usa el parámetro de escala = 1,0 predeterminado, el parámetro de escala predeterminado se aplicará a la diferencia. Por ejemplo, si la escala = 0,5, la diferencia en los tamaños de widget al pasar de 640 a 1280 píxeles no será del doble sino de 1,5 veces, porque la fórmula para calcular el nuevo tamaño es la siguiente:

newSize = size + size * scale * (1280 / 640 - 1) = size * (1 + scale) = size * 1.5.

No obstante, si la escala = 2, el widget será el triple de grande de acuerdo con el cálculo. Un caso especial de escala = 0 también satisface la fórmula siguiente: No se realizará ningún ajuste de escala (el widget tendrá exactamente el mismo tamaño, independientemente del tamaño del dispositivo). Este parámetro de escala también se puede aplicar por widget, es decir, se puede especificar como un parámetro opcional en la función GetLocation. Explicaré cómo hacerlo dentro de un momento.

A continuación, defino una variable voice. Observe que CSCS es un lenguaje de scripting similar a Python: el tipo de variable se deduce del contexto, por lo que la variable voice se representará como una cadena de C# en segundo plano.

A continuación, defino un botón. Una definición de widget en CSCS siempre toma dos instrucciones: la primera especifica la ubicación del widget y la segunda es su definición real. En segundo plano, se usa un widget UIButton para iOS y un widget Button para Android.

La sintaxis general para crear una ubicación en la pantalla es la siguiente:

GetLocation(ReferenceX, PlacementX, ReferenceY, PlacementY,
            AdjustmentX = 0, AdjustmentY = 0,
            ScaleOption = false, Scale = 0.0, Parent = null);

El siguiente es el significado de los argumentos:

  • ReferenceX: nombre de otro widget de colocación horizontal. Puede ser la cadena "ROOT", que hace referencia al widget primario o a la pantalla principal.
  • PlacementX: punto horizontal relativo al widget que se indica en ReferenceX. Los valores posibles se indican al final de estos argumentos.
  • ReferenceY: nombre de otro widget de colocación vertical. Puede ser la cadena "ROOT", que hace referencia al widget primario o a la pantalla principal.
  • PlacementY: punto vertical relativo al widget que se indica en ReferenceY. Los valores posibles se indican al final de estos argumentos.
  • AdjustmentX: movimiento horizontal adicional del widget en píxeles. También puede ser negativo; la dirección positiva va de izquierda a derecha.
  • AdjustmentY: movimiento vertical adicional del widget en píxeles. También puede ser negativo; la dirección positiva va de arriba abajo.
  • ScaleOption: indica si se debe aplicar una opción de escalado particular al widget. Si esta opción es false o no se proporciona, se realizará el ajuste especificado en la función AutoScale. Si la opción se proporciona, los parámetros de ajuste y el tamaño del widget se modificarán de acuerdo con el parámetro Scale.
  • Escala: medida que se usará para ajustar el tamaño del widget. La funcionalidad es la misma que en la función AutoScale. En realidad, se ejecutará el mismo código.
  • Parent: elemento primario del widget. Si no se especifica, el widget se agregará al diseño principal en Android o al controlador de la vista raíz en iOS (específicamente a Window.RootViewController.View).

Los valores posibles para los argumentos de colocación son muy similares a los de la clase RelativeLayout.LayoutParams de Android. Pueden ser los siguientes: "CENTER", "LEFT", "RIGHT", "TOP", "BOTTOM", "ALIGN_LEFT", "ALIGN_RIGHT", "ALIGN_TOP", "ALIGN_BOTTOM", "ALIGN_PARENT_TOP", "ALIGN_PARENT_BOTTOM".

Estos parámetros se usan tanto para la colocación horizontal como vertical en iOS y en Android. No se requieren conocimientos de XML ni XAML. No existe ningún guion gráfico de iOS con el que trabajar.

Una vez creada la ubicación, se coloca el widget en ella. La siguiente es la sintaxis general para hacerlo:

AddWidget(location, widgetName, initParameter, width, height);

AddButton es un caso particular de una función de este tipo, donde el argumento de inicialización es el texto que se muestra en el botón. Otros ejemplos de funciones de widget son AddLabel, AddView y AddCombobox, además de muchas otras, como podrá comprobar.

La función AddAction asigna una acción a un botón cuando el usuario hace clic en este. Por lo general, presenta la sintaxis siguiente:

AddAction(widgetName, callbackFunction);

Una función de devolución de llamada en CSCS siempre tiene dos parámetros, sender y context, un concepto tomado prestado de C#.

Dentro de la función talk_click, primero llamo a la función ShowToast, que llama a una implementación de notificaciones del sistema nativa en Android y a una implementación similar a la de notificaciones del sistema personalizada en iOS. La implementación de iOS solo construye un pequeño marco con un mensaje y lo destruye tras un tiempo de espera.

Por último, llamo a la función de reconocimiento de voz:

VoiceRecognition("voice_recog", voice = "en-US");

El primer parámetro es el nombre de la función de devolución de llamada a la que se llamará cuando se complete el reconocimiento de voz. El segundo parámetro es la voz. Es opcional y, de manera predeterminada, es inglés (EE. UU.). Las voces se especifican como código ISO 639-1 para el nombre del lenguaje e ISO 3166-1 alpha-2 para el código de país (por ejemplo, "en-US" para inglés de EE. UU., "es-MX" para español de México, "pt-BR" para portugués de Brasil, etc.).

La firma de la función de devolución de llamada de reconocimiento de voz es la siguiente:

function voice_recog(errorStatus, recognized)

El argumento errorStatus será una cadena vacía en caso de acierto y una descripción del error en caso de error. Si la función es correcta, la palabra reconocida se pasa como el segundo parámetro. De lo contrario, se mostrará un cuadro de diálogo de alerta al usuario (implementado como UIAlertController en iOS y como AlertDialog.Builder en Android). Si el reconocimiento de voz es correcto, se llamará a la función de texto a voz Leer. Presenta la firma siguiente:

Speak(wordToPronounce, voice = "en-US");

Los resultados de la ejecución del script de la Figura 1 se muestran en la Figura 2. La figura de la izquierda, que representa un iPhone, muestra un reconocimiento de voz correcto (cuando se reconoce una palabra pronunciada). La figura de la derecha, que representa un Android, muestra un error en que no hay ningún micrófono instalado en el sistema (un caso habitual cuando se usa un simulador).

Ejemplo de ejecución de Hola mundo Script en iPhone (izquierda) y en Android (derecha)
Figura 2 Ejemplo de ejecución de Hola mundo Script en iPhone (izquierda) y en Android (derecha)

Estructura general del proyecto

¿En qué parte del flujo de trabajo se ejecutará el código de CSCS? La respuesta es diferente para los proyectos de iOS y Android. Lo podrá comprobar a continuación, pero los detalles completos se encuentran en la descarga de código fuente complementaria en github.com/vassilych/mobile.

El código común que usan ambas plataformas se encuentra en la parte del proyecto compartida, scripting.Shared, que contiene todos los archivos de C# necesarios para analizar el código de CSCS. El código específico de cada plataforma se encuentra en los proyectos scripting.iOS y scripting.Droid. Puede ver la estructura de un proyecto de ejemplo en la Figura 3.

Estructura general de un proyecto de Xamarin con scripting de CSCS
Figura 3 Estructura general de un proyecto de Xamarin con scripting de CSCS

El script de CSCS real se encuentra en el archivo msdnScript.cscs en la carpeta Recursos del proyecto scripting.Shared. Observe que puede incluir otros archivos de CSCS mediante una llamada a la función de CSCS siguiente:

ImportFile("anotherScriptFile.cscs");

Para el proyecto de Android, configuré un vínculo al archivo msdnScript.cscs desde la carpeta Recursos de scripting.Droid, mientras que para el proyecto de iOS configuré un vínculo desde la carpeta Recursos de scripting.iOS. También puede hacer referencia al script de distintas maneras, por ejemplo, puede mantener distintas versiones del script en plataformas diferentes.

El archivo CommonFunctions.cs contiene funciones comunes para iOS y Android. En especial, contiene el método que ejecuta el script msdnScripting.cscs que se muestra en la Figura 4. Observe que distingo entre el código específico de iOS y Android mediante las directivas __IOS__ y __ANDROID__ del preprocesador. El código específico de la plataforma se encuentra principalmente en los proyectos correspondientes, scripting.iOS o scripting.Droid.

Figura 4 Ejecución del script de CSCS

public static void RunScript()
{
  RegisterFunctions();
  string fileName = "msdnScript.cscs";
  string script = "";
#if __ANDROID__
  Android.Content.Res.AssetManager assets = MainActivity.TheView.Assets;
  using (StreamReader sr = new StreamReader(assets.Open(fileName))) {
    script = sr.ReadToEnd();
  }
#endif
#if __IOS__
  string[] lines = System.IO.File.ReadAllLines(fileName);
  script = string.Join("\n", lines);
#endif
  Variable result = null;
  try {
    result = Interpreter.Instance.Process(script);
  } catch (Exception exc) {
    Console.WriteLine("Exception: " + exc.Message);
    Console.WriteLine(exc.StackTrace);
    ParserFunction.InvalidateStacksAfterLevel(0);
    throw;
  }
}

¿Desde dónde se llama a la función RunScript? Puede llamarla solo después de la inicialización de un diseño global, de modo que pueda agregarle widgets.

Resulta que es más difícil hacerlo en Android que en iOS: La llamada a la función RunScript al final de la función MainActivity.OnCreate genera un error porque algunas variables aún no están inicializadas. Por tanto, debe colocar RunScript justo antes de que la actividad principal empiece a ejecutarse realmente. La documentación de The Activity Lifecicle (Ciclo de vida de las actividades) de Android goo.gl/yF8dTZ ofrece una pista: Debe ir justo después del final del método Main­Activity.On­Resume. Algunas variables globales (por ejemplo, el tamaño de pantalla, la orientación, etc.) siguen sin inicializarse aunque se haya completado el método OnResume, por lo que el truco consiste en registrar un monitor de diseño global al final del método OnResume que se desencadenará cuando el diseño global esté construido:

protected override void OnResume()
{
  base.OnResume();
  if (!m_scriptRun) {
    ViewTreeObserver vto = TheLayout.ViewTreeObserver;
    vto.AddOnGlobalLayoutListener(new LayoutListener());
    m_scriptRun = true;
  }
}

Observe que uso una variable booleana especial m_scriptRun para asegurarme de que el script solo se ejecuta una vez. A continuación, el método OnGlobalLayout del agente de escucha de diseño ejecuta el script:

public class LayoutListener : 
  Java.Lang.Object, ViewTreeObserver.IOnGlobalLayoutListener
{
  public void OnGlobalLayout()
  {
    var vto = MainActivity.TheLayout.ViewTreeObserver;
    vto.RemoveOnGlobalLayoutListener(this);
    CommonFunctions.RunScript();
  }
}

Para iOS, la situación es algo más fácil, ya que puede ejecutar el script al final del método AppDelegate.FinishedLaunching.

Texto a voz

Veamos cómo agregar algunas funciones a CSCS, con la implementación de texto a voz como ejemplo.

En primer lugar, tengo que crear una clase que se derive de la clase ParserFunction y reemplace su método Evaluate virtual protegido, como se muestra en la Figura 5.

Figura 5 Implementación de la función Leer

public class SpeakFunction : ParserFunction
{
  protected override Variable Evaluate(ParsingScript script)
  {
    bool isList = false;
    List<Variable> args = Utils.GetArgs(script,
         Constants.START_ARG, Constants.END_ARG, out isList);
    Utils.CheckArgs(args.Count, 1, m_name);
    TTS.Init();
    string phrase = args[0].AsString();
    TTS.Voice     = Utils.GetSafeString(args, 1, TTS.Voice);
    TTS.Speak(phrase);
    return Variable.EmptyInstance;
  }
}

Esta clase es simplemente un encapsulador sobre la implementación de texto a voz real. Para iOS, la implementación de texto a voz se muestra en la Figura 6. La implementación para Android es similar, pero requiere un poco más de codificación. Puede comprobarlo en la descarga de código fuente complementaria.

Figura 6 Implementación de texto a voz para iOS (fragmento)

using AVFoundation;
namespace scripting.iOS
{
  public class TTS
  {
    static AVSpeechSynthesizer g_synthesizer = new AVSpeechSynthesizer();
    static public float  SpeechRate { set; get; }      = 0.5f;
    static public float  Volume { set; get; }          = 0.7f;
    static public float  PitchMultiplier { set; get; } = 1.0f;
    static public string Voice { set; get; }           = "en-US";
    static bool m_initDone;
    public static void Init()
    {
      if (m_initDone) {
        return;
      }
      m_initDone = true;
      // Set the audio session category, then it will speak
      // even if the mute switch is on.
      AVAudioSession.SharedInstance().Init();
      AVAudioSession.SharedInstance().SetCategory(AVAudioSessionCategory.Playback,
         AVAudioSessionCategoryOptions.DefaultToSpeaker);
    }
    public static void Speak(string text)
    {
      if (g_synthesizer.Speaking) {
        g_synthesizer.StopSpeaking(AVSpeechBoundary.Immediate);
      }
      var speechUtterance = new AVSpeechUtterance(text) {
        Rate = SpeechRate * AVSpeechUtterance.MaximumSpeechRate,
        Voice = AVSpeechSynthesisVoice.FromLanguage(Voice),
        Volume = Volume,
        PitchMultiplier = PitchMultiplier
      };
      g_synthesizer.SpeakUtterance(speechUtterance);
    }
  }
}

Una vez que tengo una implementación, debo conectarla al analizador. Esto se lleva a cabo en el proyecto compartido en el método estático CommonFunctions.RegisterFunctions (también se muestra en la Figura 3):

ParserFunction.RegisterFunction("Speak", new SpeakFunction());

Reconocimiento de voz

Para el reconocimiento de voz, debo usar una función de devolución de llamada con la finalidad de indicar al usuario qué palabra se reconoció realmente (o para notificar un error, como se muestra en la Figura 2).

Voy a implementar dos funciones de reconocimiento de voz, una para iniciarlo y otra para cancelarlo. Estas dos funciones se registran con el analizador del mismo modo que registré la implementación de texto a voz en la sección anterior:

ParserFunction.RegisterFunction("VoiceRecognition", new VoiceFunction());
ParserFunction.RegisterFunction("StopVoiceRecognition", new StopVoiceFunction());

La implementación de estas dos funciones para iOS se muestra en la Figura 7. Para Android, la implementación es similar, pero observe que el reconocimiento de voz se agregó a iOS solo en la versión 10.0, por lo que debo comprobar la versión del dispositivo y, si es necesario, notificar al usuario que el dispositivo no lo admite en las versiones de iOS anteriores a 10.0.

Figura 7 Implementación del reconocimiento de voz

public class VoiceFunction : ParserFunction
{
  static STT m_speech = null;
  public static  STT LastRecording { get { return m_speech; }}
  protected override Variable Evaluate(ParsingScript script)
  {
    bool isList = false;
    List<Variable> args = Utils.GetArgs(script,
                          Constants.START_ARG, Constants.END_ARG, out isList);
    Utils.CheckArgs(args.Count, 1, m_name);
    string strAction = args[0].AsString();
    STT.Voice = Utils.GetSafeString(args, 1, STT.Voice).Replace('_', '-');
    bool speechEnabled = UIDevice.CurrentDevice.CheckSystemVersion(10, 0);
    if (!speechEnabled) {
      UIVariable.GetAction(strAction, "\"" +
       string.Format("Speech recognition requires iOS 10.0 or higher.
       You have iOS {0}",
                     UIDevice.CurrentDevice.SystemVersion) + "\"", "");
      return Variable.EmptyInstance;
    }
    if (!STT.Init()) {
      // The user didn't authorize accessing the microphone.
      return Variable.EmptyInstance;
    }
    UIViewController controller = AppDelegate.GetCurrentController();
    m_speech = new STT(controller);
    m_speech.SpeechError += (errorStr) => {
      Console.WriteLine(errorStr);
      controller.InvokeOnMainThread(() => {
        UIVariable.GetAction(strAction, "\"" + errorStr + "\"", "");
      });
    };
    m_speech.SpeechOK += (recognized) => {
      Console.WriteLine("Recognized: " + recognized);
      controller.InvokeOnMainThread(() => {
        UIVariable.GetAction(strAction, "", "\"" + recognized + "\"");
      });
    };
    m_speech.StartRecording(STT.Voice);
    return Variable.EmptyInstance;
  }
}
public class StopVoiceFunction : ParserFunction
{
  protected override Variable Evaluate(ParsingScript script)
  {
    VoiceFunction.LastRecording?.StopRecording();
    script.MoveForwardIf(Constants.END_ARG);
    return Variable.EmptyInstance;
  }
}

El código de reconocimiento de voz real se encuentra en la clase SST. Es demasiado largo para mostrarlo aquí, además de ser diferente para iOS y Android. Le invito a consultarlo en el código fuente complementario.

No tenía ninguna función de devolución de llamada en la implementación de texto a voz, pero puede agregar una de manera similar para informar al usuario cuando se complete la pronunciación (o si se produce un error). La devolución de llamada al código de CSCS se realiza invocando el método UIVariable.GetAction:

public static Variable GetAction(string funcName, string senderName, string eventArg)
{
  if (senderName == "") {
    senderName = "\"\"";
  }
  if (eventArg == "") {
    eventArg = "\"\"";
  }
  string body = string.Format("{0}({1},{2});", funcName, senderName, eventArg);
  ParsingScript tempScript = new ParsingScript(body);
  Variable result = tempScript.ExecuteTo();
  return result;
}

Puede ver cómo se usa esta función en la Figura 7.

Ejemplo: Conversor de moneda

Como ejemplo de uso de distintas características de CSCS para el desarrollo de aplicaciones multiplataforma, vamos a crear una aplicación desde cero, un conversor de moneda.

Para obtener tipos de cambio actualizados en su aplicación, debe usar un servicio en línea. En mi caso, elegí exchangerate-api.com. El sitio proporciona un servicio web fácil de usar, donde las 1000 primeras solicitudes mensuales son gratuitas, lo que debería ser suficiente para empezar. Tras el registro, obtiene una clave única que debe proporcionar con cada solicitud.

Mi aplicación tiene distintas vistas en modo vertical y horizontal. En la Figura 8 se muestra el modo vertical y en la Figura 9 se muestra el modo horizontal para iPhone y Android.

Conversor de moneda en modo vertical en iPhone (izquierda) y en Android (derecha)
Figura 8 Conversor de moneda en modo vertical en iPhone (izquierda) y en Android (derecha)

Conversor de moneda en modo horizontal en iPhone (arriba) y en Android (abajo)
Figura 9 Conversor de moneda en modo horizontal en iPhone (arriba) y en Android (abajo)

La Figura 10 contiene la implementación de CSCS completa de la aplicación de conversión de moneda.

Figura 10 Implementación de CSCS completa de la aplicación de conversión de moneda

function on_about(sender, arg) {
  OpenUrl("http://www.exchangerate-api.com");
}
function on_refresh(sender, arg) {
  currency1 = GetText(cbCurrency1);
  currency2 = GetText(cbCurrency2);
  currency_request(currency1, currency2);
}
function currency_request(currency1, currency2) {
  if (currency1 == currency2) {
    time = Now("HH:mm:ss");
    date = Now("yyyy/MM/dd");
    rate = 1;
  } else {
    url = apiUrl + currency1 + "/" + currency2;
    try {
      data = WebRequest(url);
    } catch(exception) {
      WriteConsole(exception.Stack);
      ShowToast("Couldn't get rates. " + exception);
      SetText(labelRateValue, "Error");
      return;
    }
    try {
      timestamp = StrBetween(data, "\"timestamp\":", ",");
      time      = Timestamp(timestamp, "HH:mm:ss");
      date      = Timestamp(timestamp, "yyyy/MM/dd");
      rate      = StrBetween(data, "\"rate\":", "}");
    } catch(exception) {
      ShowToast("Couldn't get rates. " + exception);
      SetText(labelRateValue, "Error");
      return;
    }
  }
  SetText(labelRateValue, rate);
  SetText(labelDateValue, date);
  SetText(labelTimeValue, time);
}
function init() {
  currencies = {"EUR", "USD", "GBP", "CHF", "JPY", "CNY", "MXN", "RUB", "BRL", "SAR"};
  flags      = {"eu_EU", "en_US", "en_GB", "de_CH", "ja_JP", "zh_CN",
                "es_MX", "ru_RU", "pt_BR", "ar_SA"};
  AddWidgetData(cbCurrency1, currencies);
  AddWidgetImages(cbCurrency1, flags);
  SetSize(cbCurrency1, 80, 40);
  SetText(cbCurrency1, "USD");
  AddWidgetData(cbCurrency2, currencies);
  AddWidgetImages(cbCurrency2, flags);
  SetSize(cbCurrency2, 80, 40);
  SetText(cbCurrency2, "MXN");
  SetImage(buttonRefresh,     "coins");
  AddAction(buttonRefresh,    "on_refresh");
  SetFontColor(buttonRefresh, "white");
  SetFontSize(buttonRefresh,  20);
  AddAction(aboutButton,      "on_about");
}
function on_portrait(sender, arg) {
  AddOrSelectTab("Rates", "rates_active.png", "rates_inactive.png");
  SetBackground("us_bg.png");
  locCurrency1 = GetLocation("ROOT", "LEFT", "ROOT", "TOP", 10, 80);
  AddCombobox(locCurrency1, "cbCurrency1", "", 280, 100);
  locCurrency2 = GetLocation("ROOT", "RIGHT", cbCurrency1, "CENTER", -10);
  AddCombobox(locCurrency2, "cbCurrency2", "", 280, 100);
  locRateLabel = GetLocation("ROOT", "CENTER", cbCurrency2, "BOTTOM", -80, 60);
  AddLabel(locRateLabel, "labelRate", "Rate:", 200, 80);
  locRateValue = GetLocation("ROOT", "CENTER", labelRate, "CENTER", 100);
  AddLabel(locRateValue, "labelRateValue", "", 240, 80);
  locDateLabel = GetLocation("ROOT", "CENTER", labelRate, "BOTTOM", -80);
  AddLabel(locDateLabel, "labelDate", "Date:", 200, 80);
  locDateValue = GetLocation("ROOT", "CENTER", labelDate, "CENTER", 100);
  AddLabel(locDateValue, "labelDateValue", "", 240, 80);
  locTimeLabel = GetLocation("ROOT", "CENTER", labelDate, "BOTTOM", -80);
  AddLabel(locTimeLabel, "labelTime", "Time:", 200, 80);
  locTimeValue = GetLocation("ROOT", "CENTER", labelTime, "CENTER", 100);
  AddLabel(locTimeValue, "labelTimeValue", "", 240, 80);
  locRefresh = GetLocation("ROOT", "CENTER", "ROOT", "BOTTOM", 0, -4);
  AddButton(locRefresh, "buttonRefresh", "Convert", 200, 100);
  AddOrSelectTab("Settings", "settings_active.png", "settings_inactive.png");
  locAbout = GetLocation("ROOT", "CENTER", "ROOT", "BOTTOM", -4);
  AddButton(locAbout, "aboutButton", "Powered by exchangerate-api.com", 360, 100);
}
function on_landscape(sender, arg) {
  AddOrSelectTab("Rates", "rates_active.png", "rates_inactive.png");
  SetBackground("us_w_bg.png");
  locCurrency1 = GetLocation("ROOT", "LEFT", "ROOT", "CENTER", 50);
  AddCombobox(locCurrency1, "cbCurrency1", "", 200, 120);
  locCurrency2 = GetLocation(cbCurrency1, "RIGHT", "ROOT", "CENTER", 40);
  AddCombobox(locCurrency2, "cbCurrency2", "", 200, 120);
  locDateLabel = GetLocation(cbCurrency2, "RIGHT", "ROOT", "CENTER", 60);
  AddLabel(locDateLabel, "labelDate", "Date:", 180, 80);
  locDateValue = GetLocation(labelDate, "RIGHT", labelDate, "CENTER", 10);
  AddLabel(locDateValue, "labelDateValue", "", 220, 80);
  locRateLabel = GetLocation(cbCurrency2, "RIGHT", labelDate, "TOP", 60);
  AddLabel(locRateLabel, "labelRate", "Rate:", 180, 80);
  locRateValue = GetLocation(labelRate, "RIGHT", labelRate, "CENTER", 10);
  AddLabel(locRateValue, "labelRateValue", "", 220, 80);
  locTimeLabel = GetLocation(cbCurrency2, "RIGHT", labelDate, "BOTTOM", 60);
  AddLabel(locTimeLabel, "labelTime", "Time:", 180, 80);
  locTimeValue = GetLocation(labelTime, "RIGHT", labelTime, "CENTER", 10);
  AddLabel(locTimeValue, "labelTimeValue", "", 220, 80);
  locRefresh = GetLocation("ROOT", "CENTER", "ROOT", "BOTTOM", 0, -4);
  AddButton(locRefresh, "buttonRefresh", "Convert", 180, 90);
  AddOrSelectTab("Settings", "settings_active.png", "settings_inactive.png");
  locAbout = GetLocation("ROOT", "CENTER", "ROOT", "BOTTOM", -4);
  AddButton(locAbout, "aboutButton", "Powered by exchangerate-api.com", 360, 100);
}
AutoScale();
apiUrl = "https://v3.exchangerate-api.com/pair/c2cd68c6d7b852231b6d69ee/";
RegisterOrientationChange("on_portrait", "on_landscape");
init();
if (Orientation == "Portrait") {
  on_portrait("", "");
} else {
  on_landscape("", "");
}
SelectTab(0);

Las funciones on_about y on_refresh son devoluciones de llamada que suceden cuando el usuario hace clic en un botón.

El método on_about se ejecuta cuando el usuario hace clic en el botón "Con tecnología de" de la pestaña Configuración, que hace que la función OpenUrl abra la página principal de exchangerate-api.com en el explorador predeterminado (esta pestaña no se muestra en la Figura 8 ni en la Figura 9). El método on_refresh se ejecuta cuando el usuario hace clic en el botón Convertir. A continuación, obtiene las monedas seleccionadas y se invoca la función currency_request de CSCS, que realiza la conversión del tipo real.

La función currency_request comprueba primero si ambas monedas coinciden, en cuyo caso ya sé que el tipo es 1 y que no es necesario llamar a un servicio web (quiero reservar mis usos limitados gratuitos mensuales del servicio). De lo contrario, se llama a la función WebRequest. Esta función es común para iOS y Android, y su implementación se muestra en la Figura 11. Observe que no tiene que llevar a cabo el control de excepciones en el código de C#. Si se inicia una excepción (por ejemplo, si el servicio no está disponible), la excepción se propagará al código de CSCS, donde se detectará. Observe también que la función WebRequest se implementa de forma sincrónica. Para que se implemente de manera asincrónica, puede suministrar la función de devolución de llamada a la que se debe llamar cuando se lleve a cabo la solicitud (de manera similar a la funcionalidad de reconocimiento de voz que expliqué anteriormente).

Figura 11 Implementación de C# del método Evaluate de WebRequestFunction

public class WebRequestFunction : ParserFunction
{
  protected override Variable Evaluate(ParsingScript script)
  {
    bool isList = false;
    List<Variable> args = Utils.GetArgs(script,
                          Constants.START_ARG, Constants.END_ARG, out isList);
    Utils.CheckArgs(args.Count, 1, m_name);
    string uri = args[0].AsString();
    string responseFromServer = "";
    WebRequest request = WebRequest.Create(uri);
    using (WebResponse response = request.GetResponse()) {
      Console.WriteLine("{0} status: {1}", uri,
                        ((HttpWebResponse)response).StatusDescription);
      using (StreamReader sr = new StreamReader(response.GetResponseStream())) {
        responseFromServer = sr.ReadToEnd();
      }
    }
    return new Variable(responseFromServer);
  }
}

Vamos a seguir analizando el código de CSCS de la Figura 10. Estaba describiendo qué sucede en la función currency_request. La respuesta JSON que obtengo de exchangerate-api.com tiene un aspecto parecido al siguiente:

{"result":"success","timestamp":1511464063,"from":"USD","to":"CHF",­"rate":­0.99045395}

La marca de tiempo es el número de segundos transcurridos desde el 1 de enero de 1970. La función de CSCS Timestamp(format) convierte este número de segundos a un formato de fecha u hora especificado.

StrBetween(data, strStart, strEnd) es una función de conveniencia para extraer una subcadena de la cadena de datos entre las cadenas strStart1 y strStart2.

Después de extraer el tipo, la fecha y la hora, los establezco en las etiquetas correspondientes mediante la función SetText(widgetName, text).

En la función init, inicializo los datos y puedo agregar monedas adicionales para la conversión.

Es fácil tener distintos diseños para orientaciones diferentes: registre devoluciones de llamada de cambio de orientación con la función RegisterOrientationChange. Se llama a las funciones on_portrait y on_landscape cada vez que cambia la orientación del dispositivo. Como puede ver al final de la Figura 10, se ha configurado invocando lo siguiente:

RegisterOrientationChange("on_portrait", "on_landscape");

En general, agrega widgets a la pantalla en ubicaciones concretas mediante la lógica explicada en el ejemplo de Hola mundo de la sección anterior. Probablemente, habrá observado los distintos fondos del teléfono para los modos horizontal y vertical. Esto se realiza mediante la función SetBackground(imageName) de CSCS.

La función AddOrSelectTab tiene la firma siguiente:

AddOrSelectTab(Tab_Name, Picture_when_active, Picture_when_inactive);

Si la pestaña no existe todavía, se agregará. De lo contrario, se seleccionará y se le agregarán todos los widgets consecutivos. En la Figura 12 se muestra el aspecto de las pestañas en los modos activo e inactivo.

Pestañas activas e inactivas en iOS
Figura 12 Pestañas activas e inactivas en iOS

Resumen

En este artículo, pudo comprobar que CSCS permite programar aplicaciones móviles mediante un lenguaje de scripting. El script se convirtió en código nativo mediante el intérprete de C# y el marco de Xamarin. Los scripts de CSCS pueden hacer todo aquello que se puede realizar en C# (y en Xamarin C# puede hacer todo lo que se puede llevar a cabo en el desarrollo de aplicaciones nativas).

Ya publiqué una aplicación escrita íntegramente en CSCS. Compruebe la versión de iOS en apple.co/2yixGxZ y la versión de Android en goo.gl/zADtNb.

Al scripting de CSCS para aplicaciones móviles le queda mucho para completarse. Para agregar nuevas funciones a CSCS, debe crear una nueva clase que se derive de la clase ParserFunction y reemplazar su método Evaluate. A continuación, debe registrar esa clase con el analizador y proporcionar su nombre de CSCS:

ParserFunction.RegisterFunction("CSCS_Name", new MyNewCustomFunction())

CSCS le permite colocar todos los widgets mediante programación. El mismo código se usará para iOS y Android. En este caso, no es necesario usar ningún lenguaje XAML, como lo haría con Xamarin.Forms.

También puede combinar CSCS con el código de C# existente. Es fácil llamar al código de C# desde CSCS, como ya expliqué en codemag.com/article/1711081. En ese artículo, también puede consultar la lista de funciones implementadas en CSCS. No obstante, para las funciones y características más recientes y actualizadas de CSCS, visite github.com/vassilych/mobile.

Lamentablemente, no hay espacio para abordar otras cuestiones interesantes que se pueden hacer en CSCS, como compras y facturación desde la aplicación, anuncios desde la aplicación, programación de eventos únicos y repetitivos, etc., pero puede consultarlas en la descarga de código fuente complementaria.

Vassili Kaplan es un antiguo desarrollador de Microsoft Lync. Es un apasionado de la programación en C#, C++ y Python, y ahora también en CSCS. Actualmente vive en Zurich, Suiza, y trabaja como autónomo para varios bancos. Puede encontrarlo en iLanguage.ch.

Gracias al siguiente experto técnico de Microsoft por revisar este artículo: James McCaffrey
El Dr. James McCaffrey trabaja para Microsoft Research en Redmond, Washington. Ha colaborado en el desarrollo de varios productos de Microsoft como, por ejemplo, Internet Explorer y Bing. Puede Puede ponerse en contacto con McCaffrey en jamccaff@microsoft.com.


Discuta sobre este artículo en el foro de MSDN Magazine