Fevereiro de 2018

Volume 33 – Número 2

C# - Como escrever aplicativos móveis nativos usando uma linguagem de script personalizável

Por Vassili Kaplan

Na edição de fevereiro de 2016 da MSDN Magazine, mostrei como criar uma linguagem de script personalizada baseada no algoritmo de divisão e mesclagem para análise de expressões matemáticas em C# (msdn.com/magazine/mt632273). Chamei minha linguagem de script personalizável em C# (Customizable Scripting in C#) ou CSCS. Recentemente, publiquei um E-book com mais detalhes sobre a criação de uma linguagem personalizada (bit.ly/2yijCod). Criar sua própria linguagem de script inicialmente pode não parecer muito útil, apesar de existirem algumas aplicações interessantes para ela (por exemplo, roubar no jogo). Também achei algumas aplicações na programação Unity.

Mas, então, descobri uma aplicação ainda mais interessantes para a linguagem de script personalizável: escrever aplicativos de plataforma cruzada para dispositivos móveis. É possível usar a CSCS para escrever aplicativos para Android e iOS (e o Windows Phone pode ser facilmente adicionado também). E o mesmo código pode ser usado para todas as plataformas. Publiquei uma introdução sobre como fazer isso na edição de novembro/dezembro de 2017 da CODE Magazine (codemag.com/article/1711081).

Neste artigo, vou fazer uma análise mais profunda e mostrar como usar a CSCS para a programação de dispositivos móveis. Também vou corrigir algumas imprecisões do artigo da CODE Magazine. Você verá que tudo que por ser feito na plataforma nativa também pode ser feito na CSCS. Além disso, vou mostrar como você pode adicionar rapidamente recursos ausentes à CSCS.

Para executar o código mostrado neste artigo, você precisará do Visual Studio 2017 com o Xamarin instalado, tanto no Windows quanto no macOS. Uso o Visual Studio Community Edition 2017 no meu MacBook. Observe que um Mac é necessário para implantar aplicativos iOS na Apple App Store.

Um “Olá, mundo!” para aplicativos móveis

Observe a Figura 1, que mostra alguns códigos básicos da CSCS para conversão de texto em fala e reconhecimento de voz. Vamos analisar o código linha por linha.

A Figura 1 - Um “Olá, mundo!” na linguagem CSCS para aplicativos móveis

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

A função AutoScale permite que você ajuste automaticamente o tamanho do widget com base no tamanho da tela do dispositivo atual. Por exemplo, com o AutoScale, a largura de um widget será duas vezes maior em um dispositivo com largura de 1.280 pixels do que em um com largura de 640 pixels. A assinatura real da função AutoScale é:

AutoScale(scale = 1.0);

Se você não usar o parâmetro de escala padrão = 1.0, o parâmetro de escala especificado será aplicado à diferença. Por exemplo, se a escala for = 0,5, a diferença em tamanhos de widget quando se muda de 640 para 1.280 pixels não será duas vezes maior, mas 1,5 vez, pois a fórmula para calcular o novo tamanho é:

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

Mas se a escala for = 2, o widget será três vezes maior de acordo com o cálculo. Um caso especial de escala = 0 também satisfaz a fórmula aqui: Nenhum ajuste de escala será feito. O widget terá exatamente o mesmo tamanho, independentemente do tamanho do dispositivo. Esse parâmetro de escala também pode ser aplicado por widget. Ele pode ser especificado como um parâmetro opcional na função GetLocation. Vou mostrar como se faz isso daqui a pouco.

Em seguida, defino uma variável de voz. Observe que a CSCS é uma linguagem de script parecida como a Python, ou seja, o tipo de variável é deduzido com base no contexto, portanto, a variável de voz será representada como uma cadeia de caracteres em C# nos bastidores.

Em seguida, defino um botão. Uma definição de widget na CSCS sempre exige duas instruções: A primeira especifica o local do widget, e a segunda é a definição real do widget. Nos bastidores, o widget UIButton é usado para iOS, e um Botão é usado para Android.

A sintaxe geral para criar um local na tela é:

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

Veja a seguir o significado dos argumentos:

  • ReferenceX: O nome de outro widget para o posicionamento horizontal. Pode ser a cadeia de caracteres “ROOT”, que significa o widget pai ou a tela principal.
  • PlacementX: Um ponto horizontal relativo ao widget indicado em ReferenceX. Os valores possíveis estão listados no final desses argumentos.
  • ReferenceY: O nome de outro widget para o posicionamento vertical. Pode ser a cadeia de caracteres “ROOT”, que significa o widget pai ou a tela principal.
  • PlacementY: Um ponto vertical relativo ao widget indicado em ReferenceY. Os valores possíveis estão listados no final desses argumentos.
  • AdjustmenX: Um movimento horizontal adicional do widget em pixels. Também pode ser negativo; a direção positiva vai da esquerda para a direita.
  • AdjustmenY: Um movimento vertical adicional do widget em pixels. Também pode ser negativo; a direção positiva vai de cima para baixo.
  • ScaleOption: Indica se aplica-se uma opção de escala específica ao widget. Se esta opção for falsa ou não fornecida, será feito o ajuste especificado na função AutoScale. Se a opção for fornecida, os parâmetros de ajuste e o tamanho do widget serão modificados de acordo com o parâmetro Scale.
  • Dimensionamento: A medida a ser usada para ajustar o tamanho do widget. A funcionalidade é a mesma da função AutoScale. Na verdade, o mesmo código será executado.
  • Pai: O pai do widget. Se não for especificado, o widget será adicionado ao Layout Principal no Android ou ao Controlador de Exibição de Raiz no iOS (especificamente para Window.RootViewController.View).

Os valores possíveis para os argumentos de posicionamento são muito semelhantes à classe RelativeLayout.LayoutParams do Android. Eles podem ser qualquer um destes: “CENTER,” “LEFT,” “RIGHT,” “TOP,” “BOTTOM,” “ALIGN_LEFT,” “ALIGN_RIGHT,” “ALIGN_TOP,” “ALIGN_BOTTOM,” “ALIGN_PARENT_TOP,” “ALIGN_PARENT_BOTTOM.”

Esses parâmetros são usados para os posicionamentos horizontal e vertical no iOS e no Android. Nenhum conhecimento de XML ou XAML é necessário. E não há nenhum Storyboard do iOS a ser gerido.

Depois que a localização for criada, você coloca o widget nela. Veja a seguir a sintaxe geral para fazer isso:

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

AddButton é um caso específico de tal função, na qual o argumento de inicialização é o texto mostrado no botão. Outros exemplos de funções de widget são AddLabel, AddView, AddCombobox. Você verá que existem muitos outros.

A função AddAction atribui uma ação a um botão quando o usuário clica nele. Em geral, ela tem a seguinte sintaxe:

AddAction(widgetName, callbackFunction);

Uma função de retorno de chamada na CSCS sempre tem dois parâmetros, um remetente e um argumento de contexto, um conceito emprestado da C#.

Dentro da função talk_click, primeiro eu chamo a função ShowToast, que chama uma implementação Toast nativa no Android e uma implementação parecida com a Toast no iOS. A implementação do iOS só constrói um pequeno quadro com uma mensagem e a destrói depois de um tempo limite.

Por fim, chamo a função de reconhecimento de voz:

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

O primeiro parâmetro é o nome da função de retorno de chamada a ser feita quando o reconhecimento de voz estiver concluído. O segundo parâmetro é a voz. É opcional e, por padrão, está em inglês americano. As vozes são especificadas como código ISO 639-1 para o nome do idioma, e ISO 3166-1 alpha-2 para o código do país (por exemplo, “en-US” para inglês americano, “es-MX” para espanhol mexicano, “pt-BR”, para português do Brasil etc).

A assinatura da função de retorno de chamada de reconhecimento de voz é a seguinte:

function voice_recog(errorStatus, recognized)

O argumento errorStatus será uma cadeia de caracteres vazia, em caso de sucesso, e uma descrição do erro, em caso de falha. Se a função for bem-sucedida, a palavra reconhecida será passada como o segundo parâmetro. Caso contrário, uma caixa de diálogo de alerta será mostrada ao usuário (implementado como um UIAlertController no iOS e como um AlertDialog.Builder no Android). Se o reconhecimento de voz for bem-sucedido, a função Speak de conversão de texto em fala será chamada. Ele tem a seguinte assinatura:

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

Os resultados da execução do script na Figura 1 são mostrados na Figura 2. A figura à esquerda, representando um iPhone, mostra o reconhecimento de voz bem-sucedido, quando uma palavra pronunciada foi reconhecida. A figura à direita, representando um Android, mostra uma falha, quando não há nenhum microfone instalado no sistema (um caso comum quando se usa um simulador).

Um exemplo da execução de “Olá, mundo!” Script no iPhone (esquerda) e no Android (direita)
Figura 2 - Um exemplo de execução de “Olá, mundo!” Script no iPhone (esquerda) e no Android (direita)

Estrutura geral do projeto

Onde o código da CSCS será executado no fluxo de trabalho? A resposta é diferente para projetos do iOS e do Android. Você verá a seguir, mas os detalhes completos estão no download do código-fonte fornecido em github.com/vassilych/mobile.

O código comum, usado em ambas as plataformas, está na parte compartilhada do projeto, scripting.Shared, que contém todos os arquivos da C# necessários para analisar o código CSCS. O código específico de cada plataforma está localizado nos projetos scripting.iOS e scripting.Droid. Consulte a estrutura de um projeto de amostra na Figura 3.

Estrutura geral de um projeto Xamarin com script em CSCS
Figura 3 - Estrutura geral de um projeto Xamarin com script em CSCS

O verdadeiro script em CSCS está localizado no arquivo msdnScript.cscs na pasta Resources do projeto scripting.Shared. Observe que você pode incluir outros arquivos da CSCS chamando a seguinte função da CSCS:

ImportFile("anotherScriptFile.cscs");

Para o projeto Android, configuro um vínculo ao arquivo msdnScript.cscs na pasta do projeto scripting.Droid. Para o projeto iOS, configuro um vínculo da pasta Resources do projeto scripting.iOS. Você também pode referenciar o script de diversas formas, por exemplo, mantendo diferentes versões do script em diferentes plataformas.

O arquivo CommonFunctions.cs contém funcionalidades comuns ao iOS e aos Android. Especificamente, ele contém o método que executa o script msdnScripting.cscs mostrado na Figura 4. Observe que fiz a distinção entre o código específico do iOS e do Android usando as diretivas do pré-processador __IOS__ and __ANDROID__. A maioria do código específico da plataforma está localizado nos projetos correspondentes scripting.iOS ou scripting.Droid.

Figura 4 - Execução do script da 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;
  }
}

De onde você chama a função RunScript? Você pode chamá-la somente depois da inicialização do layout global, para que possa adicionar widgets a ela.

É mais complicado fazer isso no Android do que no iOS: Chamar a função RunScript no final da função MainActivity.OnCreate falha porque algumas variáveis ainda não foram inicializadas. Você tem que colocar RunScript imediatamente antes de a atividade principal começar sua execução. A documentação Android Activity Lifestyle em goo.gl/yF8dTZ dá uma pista: Ela tem que ficar logo depois de o método Main­Activity.On­Resume ser concluído. Algumas variáveis globais (por exemplo, o tamanho da tela, a orientação etc) ainda não foram inicializadas mesmo no final do método OnResume; portanto, o truque é registrar um inspetor de layout global no final do método OnResume, que será acionado assim que o layout global for construído:

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

Observe que eu uso uma variável booliana m_scriptRun para ter certeza de que o script é executado somente uma vez. O método OnGlobalLayout no ouvinte de layout executa o script:

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

Para o iOS, a situação é um pouco mais fácil. Você pode apenas executar o script no final do método AppDelegate.FinishedLaunching.

Conversão de texto em fala

Vamos ver como adicionar algumas funcionalidades à CSCS usando a conversão de texto em fala como exemplo.

Primeiramente, preciso criar uma classe derivada da classe ParserFunction e substituir seu método virtual protegido chamado Evaluate, como mostrado na Figura 5.

Figura 5 - Implementação da função de fala

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 classe é apenas um wrapper em relação à implementação real da conversão de texto em fala. Para o iOS, a implementação da conversão de texto em fala é mostrada na Figura 6. A implementação do Android é semelhante, mas requer um pouco mais de codificação. Você pode ver isso se fizer o download do código-fonte fornecido.

Figura 6 - Implementação da conversão de texto em fala do 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);
    }
  }
}

Assim que eu tiver uma implementação, preciso conectá-lo ao analisador. Isso é feito no projeto compartilhado no método estático CommonFunctions.RegisterFunctions (também mostrado na Figura 3):

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

Reconhecimento de voz

Para reconhecimento de voz, preciso usar uma função de retorno de chamada para informar ao usuário qual palavra foi, de fato, reconhecida (ou reportar um erro, como na Figura 2).

Vou implementar duas funções para reconhecimento de voz: uma para começar o reconhecimento de voz e outra para cancelá-la. Essas duas funções foram registradas com o analisador assim que registrei a conversão de texto em fala na seção anterior:

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

A implementação dessas duas funções para iOS é mostrada na Figura 7. Para o Android, a implementação é semelhante, mas observe que o reconhecimento de voz foi adicionado ao iOS somente na versão 10.0; portanto, tenho que verificar a versão do dispositivo e, se necessário, informar o usuário que o dispositivo não dá suporte a ele nas versões do iOS anteriores à 10.0.

Figura 7 - Implementação do reconhecimento 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;
  }
}

O verdadeiro código do reconhecimento de voz está na classe SST. É muito longo para mostrar aqui e também é diferente para iOS e Android. Convido você a conferi-lo no código-fonte fornecido.

Eu não tinha uma função de retorno de chamada na conversão de texto em fala, mas você pode adicionar uma de forma semelhante para mostrar ao usuário quando a fala for concluída (ou se houver um erro). A chamada de retorno para o código da CSCS é realizada chamando-se o 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;
}

Você pode ver como essa função é usada na Figura 7.

Exemplo: Um conversor de moeda

Como um exemplo do uso de diferentes recursos da CSCS para o desenvolvimento de aplicativos de plataforma cruzada, vamos criar um aplicativo do zero: um conversor de moeda.

Para obter taxas de câmbio atualizadas no seu aplicativo, você tem que usar um serviço online. Escolho o exchangerate-api.com. O site fornece um serviço Web fácil de usar em que as primeiras mil solicitações por mês são gratuitas, o que deve ser suficiente para começar. Após o registro, você recebe uma chave exclusiva que deve fornecer com cada solicitação.

Meu aplicativo tem diferentes visualizações em modos retrato e paisagem. A Figura 8 mostra o modo retrato, e a Figura 9 mostra o modo paisagem para iPhone e Android.

O Conversor de Moeda no Modo Retrato no iPhone (à esquerda) e no Android (à direita)
Figura 8 - O Conversor de Moeda no Modo Retrato no iPhone (à esquerda) e no Android (à direita)

Conversor de Moeda no Modo Paisagem no iPhone (acima) e no Android (abaixo)
Figura 9 - Conversor de Moeda no Modo Paisagem no iPhone (na parte superior) e no Android (na parte inferior)

A Figura 10 contém toda a implementação da CSCS do aplicativo conversor de moeda.

Figura 10 - Implementação da CSCS do aplicativo Conversor de Moeda

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

As funções on_about e on_refresh são retornos de chamada que ocorrem quando o usuário clica em um botão.

O método on_about é executado quando o usuário clica no botão “Da plataforma” na guia Configurações, o que faz com que a função OpenUrl abra a página inicial exchangerate-api.com no navegador padrão (essa guia não é mostrada na Figura 8 nem na Figura 9). O método on_refresh é executado quando o usuário clica no botão Converter. Em seguida, você obtém as moedas selecionadas, e a função currency_request da CSCS é chamada e faz, de fato, a conversão da taxa.

A função currency_request primeiro verifica se ambas as moedas são iguais. Nesse caso, já sei que a taxa é 1 e que não é preciso chamar o serviço Web (quero poupar meus usos gratuitos desse serviço por mês). Caso contrário, a função WebRequest é chamada. Essa função é comum para o iOS e o Android, e sua implementação é mostrada na Figura 11. Observe que você não tem que fazer o tratamento da exceção no código C#. Se uma exceção for lançada (por exemplo, se o serviço estiver indisponível), a exceção será propagada para o código da CSCS, onde será capturado. Observe também que a função WebRequest é implementada de forma síncrona. Você também pode torná-la assíncrona fornecendo a função de retorno de chamada a ser chamada quando a solicitação for feita (análoga à funcionalidade de reconhecimento de voz que mostrei anteriormente).

Figura 11 - Implementação em C# do Método de Avaliação 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 continuar analisando o código da CSCS na Figura 10. Eu estava descrevendo o que acontece na função currency_request. A resposta JSON que recebo de exchangerate-api.com parece com a seguinte:

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

O carimbo de data/hora é o número de segundos passados desde 1º de janeiro de 1970. A função da CSCS Timestamp(format) converte esse número de segundos em um formato de data ou hora específico.

StrBetween(data, strStart, strEnd) é uma função de conveniência para extrair uma subcadeia de caracteres da cadeia de dados entre as cadeias strStart1 e strStart2.

Assim que extraio a taxa, a data e a hora, faço sua definição para os rótulos correspondentes usando a função SetText(widgetName, text).

Na função init, inicializo os dados e posso adicionar outras moedas para conversão.

É fácil ter diferentes layouts para diferentes orientações: registre as chamadas de retorno de mudança de orientação com a função RegisterOrientationChange. As funções on_portrait e on_landscape são chamadas cada vez que a orientação do dispositivo muda. Como você pode ver na parte inferior da Figura 10, isso é configurado chamando-se:

RegisterOrientationChange("on_portrait", "on_landscape");

Em geral, você adiciona widgets à tela em determinados locais, usando a lógica explicada no exemplo “Olá, mundo!”, na primeira seção. Você provavelmente percebeu os diferentes fundos de telefone para os modos paisagem e retrato. Isso é feito usando-se a função da CSCS SetBackground(imageName).

A função AddOrSelectTab tem a seguinte assinatura:

AddOrSelectTab(Tab_Name, Picture_when_active, Picture_when_inactive);

Se a guia ainda não existir, ela será adicionada. Caso contrário, ela será selecionada e todos os widgets consecutivos serão adicionados a ela. A Figura 12 mostra a aparência das guias nos modos ativos e inativo.

Guias Ativas e Inativas no iOS
Figura 12 - Guias Ativas e Inativas no iOS

Conclusão

Neste artigo, você viu que pode programar com a CSCS aplicativos móveis usando uma linguagem de script. O script é convertido em código nativo usando o interpretador C# e o Xamarin Framework. Os scripts da CSCS podem fazer qualquer coisa que pode ser feita na C# (e, na Xamarin C#, você pode fazer qualquer coisa que pode ser feita no desenvolvimento de aplicativos nativos).

Já publiquei um aplicativo totalmente escrito em CSCS. Confira a versão do iOS em apple.co/2yixGxZ e a versão do Android em goo.gl/zADtNb.

O script da CSCS para aplicativos móveis está longe de estar completo. Para adicionar novas funcionalidades à CSCS, você cria uma nova classe derivada da classe ParserFunction e substitui seu método Evaluate. Em seguida, registra essa classe com o analisador, fornecendo seu nome da CSCS:

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

Usando a CSCS, você pode inserir todos os widgets de forma programática, e o mesmo código será usado tanto para o Android quanto para o iOS. E você não precisa usar nenhuma linguagem XAML para isso, como faria com a Xamarin.Forms.

Você também pode combinar a CSCS com o código C# existente. É fácil chamar o código C# da CSCS, conforme expliquei em codemag.com/article/1711081. Neste artigo, você também pode verificar a lista de funções implementadas na CSCS. Mas, para obter as funções e os recursos mais atuais da CSCS, visite github.com/vassilych/mobile.

Infelizmente, não temos mais espaço para discutir outras coisas legais que você pode fazer na CSCS, como compra e faturamento no aplicativo, propagandas no aplicativo, programação de evento único ou repetitivo e muito mais, mas você pode conferi-los no código-fonte fornecido com o download.

Vassili Kaplané ex-desenvolvedor do Microsoft Lync. Ele é apaixonado pela programação em C#, C++, Python e agora em CSCS. Atualmente, ele mora em Zurique, Suíça, e trabalha como autônomo para vários bancos Você pode encontrá-lo em iLanguage.ch

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: James McCaffrey
Entre em contato com o Dr. James McCaffrey trabalha para a Microsoft Research em Redmond, Washington. Ele trabalhou em vários produtos da Microsoft, incluindo Internet Explorer e Bing. Entre em contato com o Dr. McCaffrey pode ser encontrado em jamccaff@microsoft.com.


Discuta esse artigo no fórum do MSDN Magazine