February 2018

Volume 33 Number 2

[C#]

Writing Native Mobile Apps Using a Customizable Scripting Language

By Vassili Kaplan

In the February 2016 issue of MSDN Magazine, I showed how to create a custom scripting language based on the Split-And-Merge algorithm for parsing mathematical expressions in C# (msdn.com/magazine/mt632273). I called my language Customizable Scripting in C#, or CSCS. Recently, I published an E-book that provided more details about creating a custom language (bit.ly/2yijCod). Creating your own scripting language might not initially seem to be particularly useful, even though there are some interesting applications of it (for example, game cheating). I also found some applications in Unity programming.

But then I discovered an even more interesting application for a customizable scripting language—writing cross-platform applications for mobile devices. It turns out that it’s possible to use CSCS to write apps for Android and iOS (and Windows Phone can be easily added, as well). And the same code can be used for all platforms. I published an introduction on how to do that in the November-December 2017 issue of CODE Magazine (codemag.com/article/1711081).

In this article I’m going to take a deeper dive and show how to use CSCS to program for mobile devices. I‘ll also correct some inaccuracies in the CODE Magazine article. You’ll see that anything that can be done on the native platform can be done in CSCS. I’m also going to show how you can add missing features to CSCS on the fly.

To run the code shown in this article, you’ll need Visual Studio 2017 with Xamarin installed, either on Windows or on macOS. I personally use Visual Studio Community Edition 2017 on my MacBook. Note that a Mac is needed to deploy iOS apps to the Apple App Store.

A “Hello, World!” for Mobile Apps

Take a look at Figure 1, which shows some basic CSCS code for text-to-speech and voice recognition. Let’s examine the code line by line.

Figure 1 A “Hello, World!” in CSCS for 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);
  }
}

The AutoScale function lets you automatically adjust widget size based on the actual device screen size. For instance, with AutoScale, the width of a widget will be twice as large on a device with a width of 1280 pixels as on one with a width of 640 pixels. The actual signature of the AutoScale function is:

AutoScale(scale = 1.0);

If you don’t use the default scale = 1.0 parameter, the specified scale parameter will be applied to the difference. For example, if scale = 0.5 the difference in widget sizes when moving from 640 to 1280 pixels will be not twice but 1.5 times because the formula to calculate the new size is:

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

But if scale = 2, the widget will be 3 times larger according to the calculation. A special case of scale = 0 also satisfies the formula here: No scale adjustment will be performed—the widget will have exactly the same size regardless of the device size. This scale parameter can also be applied per widget—it can be specified as an optional parameter in the GetLocation function. I’ll show how to do this in a bit.

Next, I define a voice variable. Note that CSCS is a Python-like scripting language—the type of the variable is inferred from the context, so the voice variable will be represented as a C# string behind the scenes.

Then I define a button. A widget definition in CSCS always takes two statements: The first specifies the location of the widget and the second is the actual definition of the widget. Behind the scenes, a UIButton widget is used for iOS and a Button is used for Android.

The general syntax for creating a location on the screen is:

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

Here’s the meaning of the arguments:

  • ReferenceX: The name of another widget for horizontal placement. It can be the string “ROOT,” meaning the parent widget or the main screen.
  • PlacementX: A horizontal point relative to the widget indicated in ReferenceX. Possible values are listed at the end of these arguments.
  • ReferenceY: The name of another widget for vertical placement. It can be the string “ROOT,” meaning the parent widget or the main screen.
  • PlacementY: A vertical point relative to the widget indicated in ReferenceY. Possible values are listed at the end these arguments.
  • AdjustmenX: An additional horizontal movement of the widget in pixels. It can also be negative; the positive direction goes from left to right.
  • AdjustmenY: An additional vertical movement of the widget in pixels. It can also be negative; the positive direction goes from top to bottom.
  • ScaleOption: Indicates whether to apply a particular scaling option to the widget. If this option is false or not provided, the adjustment specified in the AutoScale function will be done. If the option is provided, the adjustment parameters and the size of the widget will be modified according to the Scale parameter.
  • Scale: The measure to be used for adjusting the size of the widget. The functionality is the same as in the AutoScale function. As a matter of fact, the same code will be executed.
  • Parent: The parent of the widget. If not specified, the widget will be added to the Main Layout on Android or to the Root View Controller on iOS (specifically to Window.RootViewController.View).

Possible values for the placement arguments are very similar to the Android RelativeLayout.LayoutParams class. They can be any of: “CENTER,” “LEFT,” “RIGHT,” “TOP,” “BOTTOM,” “ALIGN_LEFT,” “ALIGN_RIGHT,” “ALIGN_TOP,” “ALIGN_BOTTOM,” “ALIGN_PARENT_TOP,” “ALIGN_PARENT_BOTTOM.”

These parameters are used for both horizontal and vertical placement on iOS and on Android. No XML or XAML knowledge is needed. And there’s no iOS Storyboard to deal with.

Once the location is created, you place a widget in it. Here’s the general syntax for doing so:

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

AddButton is a particular case of such a function, where the initialization argument is the text shown on the button. Other examples of widget functions are AddLabel, AddView, AddCombobox and there are many more, as you’ll see.

The AddAction function assigns an action to a button when the user clicks on it. It generally has the following syntax:

AddAction(widgetName, callbackFunction);

A callback function in CSCS always has two parameters, a sender and a context argument, a concept borrowed from C#.

Inside of the talk_click function, first I call the ShowToast function, which calls a native Toast implementation on Android and a custom Toast-like implementation on iOS. The iOS implementation just constructs a small frame with a message and destroys it after a timeout.

Finally, I call to the voice recognition function:

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

The first parameter is the name of the callback function to call when the voice recognition is complete. The second parameter is the voice. It’s optional and, by default, it’s U.S. English. The voices are specified as ISO 639-1 code for the language name and ISO 3166-1 alpha-2 for the country code (for example. “en-US” for English, U.S., “es-MX” for Spanish, Mexico, “pt-BR”, for Portuguese, Brazil, and so on).

The signature of the voice recognition callback function is the following:

function voice_recog(errorStatus, recognized)

The errorStatus argument will be an empty string on success and a description of the error on failure. If the function is successful, the recognized word is passed as the second parameter. If not, an alert dialog will be shown to the user (implemented as a UIAlertController on iOS and as an AlertDialog.Builder on Android). If the voice recognition is successful, the text-to-speech function Speak will be called. It has the following signature:

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

The results of running the script in Figure 1 are shown in Figure 2. The figure on the left, representing an iPhone, shows successful voice recognition—when a pronounced word was recognized. The figure on the right, representing an Android, shows a failure, when there’s no microphone installed on the system (a common case when using a simulator).

An Example of Running the “Hello, World!” Script on iPhone (Left) and on Android (Right)
Figure 2 An Example of Running the “Hello, World!” Script on iPhone (Left) and on Android (Right)

General Structure of the Project

Where in the workflow will the CSCS code be executed? The answer is different for the iOS and Android projects. You’ll see it in what follows, but the full details are in the accompanying source code download at github.com/vassilych/mobile.

The common code, used by both platforms, is in the shared project part, scripting.Shared, which contains all the C# files needed for parsing the CSCS code. The code specific to each platform is located in the scripting.iOS and scripting.Droid projects. See the structure of a sample project in Figure 3.

General Structure of a Xamarin Project with CSCS Scripting
Figure 3 General Structure of a Xamarin Project with CSCS Scripting

The actual CSCS script is located in the msdnScript.cscs file under the Resources folder in the scripting.Shared project. Note that you can include other CSCS files by calling the following CSCS function:

ImportFile("anotherScriptFile.cscs");

For the Android project I set up a link to the msdnScript.cscs file from the scripting.Droid Assets folder, and for the iOS project I set up a link from the scripting.iOS Resources folder. You can also reference the script in a number of ways, for example keeping different versions of the script on different platforms.

The CommonFunctions.cs file contains functionality common to iOS and Android. In particular, it holds the method that executes the msdnScripting.cscs script that’s shown in Figure 4. Note that I distinguish between the iOS- and Android-specific code by using the preprocessor __IOS__ and __ANDROID__ directives. The platform-specific code is mostly located in the corresponding projects, scripting.iOS or scripting.Droid.

Figure 4 Executing the CSCS Script

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

Where do you call the RunScript function from? You can call it only after the global layout has been initialized, so you can add widgets to it.

It turns out that it’s trickier to do this on Android than on iOS: Just calling the RunScript function at the end of the MainActivity.OnCreate function fails because some variables haven’t been initialized yet. So you must put RunScript right before the main activity actually starts running. The Android Activity Lifestyle documentation at goo.gl/yF8dTZ provides a clue: It must go right after the Main­Activity.On­Resume method completes. Some global variables (for instance, the screen size, the orientation and so on) are not yet initialized even at the end of the OnResume method, so the trick is to register a global layout watcher at the end of the OnResume method that will be triggered as soon as the global layout is constructed:

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

Note that I use a special Boolean variable m_scriptRun to make sure that the script runs just once. The OnGlobalLayout method in the layout listener then executes the script:

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

For iOS the situation is somewhat easier, you can just run the script at the end of the AppDelegate.FinishedLaunching method.

Text-to-Speech

Let’s see how to add some functionality to CSCS, using text-to-speech as an example.

First, I need to create a class deriving from the ParserFunction class and override its protected virtual Evaluate method, as shown in Figure 5.

Figure 5 Speak Function Implementation

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

This class is just a wrapper over the actual text-to-speech implementation. For iOS, the text-to-speech implementation is shown in Figure 6 The Android implementation is similar, but it takes a bit more coding. You can see it in the accompanying source code download.

Figure 6 iOS Text-to-Speech Implementation (Fragment)

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

Once I have an implementation, I need to plug it in to the parser. This is done in the shared project in CommonFunctions.RegisterFunctions static method (also shown in Figure 3):

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

Voice Recognition

For voice recognition I need to use a callback function in order to tell the user what word was actually recognized (or to report an error, as in Figure 2).

I’m going to implement two functions for voice recognition—one to start voice recognition and another to cancel it. These two functions are registered with the parser just as I registered text-to-speech in the previous section:

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

The implementation of these two functions for iOS is shown in Figure 7. For Android the implementation is similar, but note that voice recognition was added to iOS only in version 10.0, so I must check the device version and, if necessary, inform the user that the device doesn’t support it in iOS versions prior to 10.0.

Figure 7 Voice Recognition Implementation

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

The actual voice recognition code is in the SST class. It’s a bit too long to show here and is also different for iOS and Android. I invite you to check it out in the accompanying source code.

I didn’t have a callback function in text-to-speech, but you can add one in a similar way to tell the user when the speech completes (or if there’s an error). The callback to the CSCS code is performed by invoking the UIVariable.GetAction method:

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

You can see how this function is used in Figure 7.

Example: A Currency Convertor

As an example of using different CSCS features for cross-platform app development, let’s create an app from scratch—a currency convertor.

To get up-to-date exchange rates in your app, you have to use an online service. I chose exchangerate-api.com. The site provides an easy-to-use Web service where the first 1,000 requests per month are free, which should be enough to start. After registration you get a unique key you must supply with each request.

My app has different views in portrait and landscape modes. Figure 8 shows portrait mode and Figure 9 shows landscape mode for both iPhone and Android.

Currency Convertor in Portrait Mode on iPhone (Left) and on Android (Right)
Figure 8 Currency Convertor in Portrait Mode on iPhone (Left) and on Android (Right)

Currency Convertor in Landscape Mode on iPhone (Top) and on Android (Bottom)
Figure 9 Currency Convertor in Landscape Mode on iPhone (Top) and on Android (Bottom)

Figure 10 contains the entire CSCS implementation of the currency convertor app.

Figure 10 CSCS Implementation of the Currency Convertor App

function on_about(sender, arg) {
  OpenUrl("https://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);

Functions on_about and on_refresh are both callbacks that happen when the user clicks on a button.

The on_about method is executed when the user clicks on the “Powered by” button in the Settings tab, which causes the OpenUrl function to open the exchangerate-api.com homepage in the default browser (this tab isn’t shown in Figure 8 and Figure 9). The on_refresh method is executed when the user clicks on the Convert button. You then get the selected currencies and the CSCS currency_request function is invoked, which does the actual rate conversion.

The currency_request function first checks if both currencies are the same—in this case I already know that the rate is 1 and there’s no need to call a Web service (I want to save my free limited uses of this service per month). Otherwise, the WebRequest function is called. This function is common to both iOS and for Android and its implementation is shown in Figure 11. Note that you don’t have to do the exception handling in C# code. If an exception is thrown (for example, if the service is unavailable), the exception will be propagated to the CSCS code, where it will be caught. Note also that the WebRequest function is implemented synchronously. You can also make it asynchronous by supplying the callback function to be called when the request is done (analogous to the voice recognition functionality I showed earlier).

Figure 11 C# Implementation of the WebRequestFunction Evaluate Method

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

Let’s continue analyzing the CSCS code in Figure 10. I was describing what happens in the currency_request function. The JSON response I get from exchangerate-api.com looks like the following:

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

The timestamp is the number of seconds passed since Jan. 1, 1970. The CSCS function Timestamp(format) converts this number of seconds to a specified date or time format.

StrBetween(data, strStart, strEnd) is a convenience function for extracting a substring from the data string between strStart1 and strStart2 strings.

Once I extract the rate, date and time, I set them to the corresponding labels using the SetText(widgetName, text) function.

In the init function I initialize the data, and I can add additional currencies for the conversion.

It’s easy to have different layouts for different orientations: register orientation change callbacks with the RegisterOrientationChange function. The on_portrait and on_landscape functions are called each time the device orientation changes. As you can see at the bottom of Figure 10, it’s set up by invoking:

RegisterOrientationChange("on_portrait", "on_landscape");

In general you add widgets to the screen at particular locations, using the logic explained in the “Hello, World!” example in the first section. You probably noticed the different phone backgrounds for landscape and portrait mode. This is done using the SetBackground(imageName) CSCS function.

AddOrSelectTab function has the following signature:

AddOrSelectTab(Tab_Name, Picture_when_active, Picture_when_inactive);

If the the tab doesn’t exist yet, it will be added, otherwise it will be selected and all consecutive widgets will be added to this tab. Figure 12 shows how the tabs look in both active and inactive modes.

Active and Inactive Tabs on iOS
Figure 12 Active and Inactive Tabs on iOS

Wrapping Up

In this article you saw that with CSCS you can program mobile apps using a scripting language. The script is converted to native code using the C# interpreter and the Xamarin Framework. The CSCS scripts can do anything that can be done in C# (and in Xamarin C# you can do anything that can be done in native app development).

I’ve already published an app written entirely in CSCS. Check out the iOS version at apple.co/2yixGxZ and the Android version at goo.gl/zADtNb.

CSCS scripting for mobile apps is far from complete. To add new functionality to CSCS, you create a new class that derives from the ParserFunction class and override its Evaluate method. Then you register that class with the parser, supplying its CSCS name:

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

Using CSCS you can place all the widgets programmatically, and the same code will be used for both Android and iOS. And you don’t need to use any XAML for that, as you would with Xamarin.Forms.

You can also combine CSCS with the existing C# code—it’s easy to call C# code from CSCS, as I explained at codemag.com/article/1711081. In that article you can also check the list of functions implemented in CSCS. But for the latest, most up-to-date CSCS functions and features, visit github.com/vassilych/mobile.

Unfortunately, there’s no room to discuss some other cool things you can do in CSCS, such as in-app purchasing and billing, in-app advertisements, scheduling one-shot and repetitive events, and more, but you can check them out in the accompanying source code download.

Vassili Kaplan is a former Microsoft Lync developer. He is passionate about programming in C#, C++, Python and now in CSCS. He currently lives in Zurich, Switzerland, and works as a freelancer for various banks. You can reach him at iLanguage.ch

Thanks to the following Microsoft technical expert for reviewing this article: James McCaffrey
Dr. James McCaffrey works for Microsoft Research in Redmond, Wash. He has worked on several Microsoft products, including Internet Explorer and Bing. Dr. McCaffrey can be reached at jamccaff@microsoft.com.


Discuss this article in the MSDN Magazine forum