Building a F# app using Xamarin

I'm a sucker for new things, especially new things mobile. So when I saw that Xamarin now supports development in F#, a language I know nothing about (except it "does math"); I knew I had to try.

I have been working on a tool to allow me to easily track the status of my sites from my own phone, as well as make sure that my 'shared' azure hosted website stays responsive for visitors at all time. I originally built the backend mobile service to ping the sites I have on Microsoft Azure and keep them from being garbage collected by the Azure instance. Then realized it would be fun to build a little tool to have on my phone. So, I thought a less secure version would be a perfect chance to learn a little bit about F# and a little bit more about Xamarin. And so I wrote Ping Me F#, based on a tool that is currently under development. This app still needs improvement. As of this writing:

  1. I cannot delete websites registered with the service from the app.

  2. Everything is running synchronously. This c# developer sometimes struggles with async in c# let alone F#

  3. There is not a way to refresh the feed.

In another post I will discuss the backend, it is based on Azure Mobile Services and uses Node.js for the service methods, and I used twilio and their node module to alert users via text of the status of the website. This is just a demo app, there is no security around the phone number registered with the website. In the production version I do have security around a user's websites and phone numbers. This is just for demonstration purposes.

To build Ping Me F# I followed Rachel Reese's wonderful demonstration app on the ever popular Tasky and Xamarin's very own Shirt Store F# app.

The most obvious first step

  • Create a new F# project for iOS or Android. In this instance, I chose iOS.

image of new project

  • I don't know how other people build their F# apps. I started with items I had identified as being shareable so that I don't have to rewrite the code for Android.

    • This included the website model, services and I shamelessly used xamarin's helpers from their f# store app.

    The service was fun to build. I will show three key methods built to allow communication with the node backend.

    Create Request (the foundation of the requests in the application).

    let CreateRequest request =
        let request = WebRequest.Create ("https://pingme.azure-mobile.net/tables/urls/"+request) :?> HttpWebRequest
        request.Method <- "GET"
        request.ContentType <- "application/json"
        request.Accept <- "application/json"
        request
    

    Add Website, this uses the request created from Create Request, changes the http verb to POST and adds a JSON body. (Which is flawlessly interpreted by the backend :fistbump:)

    member this.AddWebsite (url:string, phone:string) = async {
            return! System.Threading.Tasks.Task.Run(fun _->
                let website = Website.CreateWebsite(url, phone)
                let json = CurrentWebsite.GetJson website
                let content = encoding.GetBytes json
                let request = CreateRequest ("")
                request.Method <- "POST" 
                request.ContentLength <- int64 content.Length 
                use s = request.GetRequestStream()
                s.Write(content, 0, content.Length)
                ReadResponseText request) |> Async.AwaitTask }
    

    Finally, add a Read Request method to the application service. This is another one that I used from Xamarin. What works, works. It is used to interogate and ensure that any information from the service is correctly translated to the application.

      let ReadResponseText (req : HttpWebRequest) =
        use response = req.GetResponse ()
        use s = response.GetResponseStream()
        use r = new StreamReader (s, System.Text.Encoding.UTF8)
        r.ReadToEnd()
    

image of shared stuff

Now that we have an app backend, let's do some F# UI. Yep, UI written in F#.

Here is where I thought the terseness of F# was a sight to behold. This is the main view in its entirety.

type PingMeViewController () as this =
inherit UIViewController ()

let table = new UITableView()

let date = WebService.Shared.GetWebsitesAsync() |> Async.RunSynchronously

do this.Title <- "Ping Me F#"

override this.ViewDidLoad () =
    base.ViewDidLoad ()
    let addNewTask = 
        EventHandler(fun sender eventargs -> 
            this.NavigationController.PushViewController (new AddWebsiteViewController(), true))

    this.NavigationItem.SetRightBarButtonItem (new UIBarButtonItem(UIBarButtonSystemItem.Add, addNewTask), false)
    table.Frame <- this.View.Bounds
    this.View.Add table 

override this.ViewWillAppear animated =
    base.ViewWillAppear animated
    table.Source <- new PingMeDataSource(date , this.NavigationController)
    table.ReloadData()

Note that the control above uses Xamarin-familiar controls like UITableView and UIViewController.

Then, wire in the controller in the AppDelegate.fs.

  override val Window = null with get,set

// This method is invoked when the application is ready to run.
override this.FinishedLaunching (app, options) =
    this.Window <- new UIWindow (UIScreen.MainScreen.Bounds)
    this.Window.RootViewController <- new UINavigationController(new PingMeViewController ())
    this.Window.MakeKeyAndVisible ()
    true

You will probably see a few errors right now, most likely around the AddWebsiteViewController control. So let's go ahead and add that controller. If you implement Add control in the same file, please ensure that you put the control above the PingMeViewController.

type AddWebsiteViewController(website:Website, isNew:bool) =
inherit UIViewController ()
new() = new AddWebsiteViewController (Website.CreateWebsite(), true)
override this.ViewDidLoad () =
    base.ViewDidLoad ()

    let addView = new UIView(this.View.Bounds, BackgroundColor = UIColor.White)

    let description = new UITextField(RectangleF(20.f, 64.f, 280.f, 50.f),
                                      Text = website.Url,
                                      Placeholder = "website url")
    addView.Add description

    let phoneNumber = new  UITextField(RectangleF(20.f, 114.f, 280.f, 50.f),
                                      Text = website.Phone,
                                      KeyboardType = UIKeyboardType.NumberPad,
                                      Placeholder = "phone with areacode")

    addView.Add phoneNumber

    let statusLabel = new UILabel(RectangleF(20.f, 164.f, 100.f, 30.f), Text = "Site Status") 
    addView.Add statusLabel

    let statusDescription = new UILabel(RectangleF(160.f, 164.f, 100.f, 30.f), Text = website.Status)
    addView.Add statusDescription

    let addedLabel = new UILabel(RectangleF(20.f, 214.f, 280.f, 50.f))
    addView.Add addedLabel

    let addUpdateButton = UIButton.FromType(UIButtonType.RoundedRect, Frame = RectangleF(20.f, 214.f, 280.f, 50.f))

    addUpdateButton.TouchUpInside.AddHandler
        (fun _ _ -> 
            match isNew with 
            | true -> 
                WebService.Shared.AddWebsite(description.Text, phoneNumber.Text) |> Async.RunSynchronously
                addedLabel.Text <- "Added!"
            | false -> 
                WebService.Shared.UpdateWebsite(description.Text, phoneNumber.Text, website.id) |> Async.RunSynchronously
                addedLabel.Text <- "Updated!"
            )

    addUpdateButton.SetTitle("Save", UIControlState.Normal)
    addView.Add addUpdateButton

    description.TouchDown.AddHandler (fun _ _ -> addedLabel.Text <- "")

    this.View.Add addView

We will delve into this later this week.

Last but not least, add the data source.

I put this in the same file, because I was picking up on some of the quirks of F#. It may be obvious to many, but everything can be in one file as long as the items/classes/controls do not depend on items lower in the script.

type PingMeDataSource(websitesource: Website [], navigation: UINavigationController) = 
inherit UITableViewSource()
let sites = ResizeArray(websitesource)
let cellIdentifier = "WebsiteCell"

override x.RowsInSection(view, section) = sites.Count
override x.GetCell(view, indexPath) = 
    let t = sites.[indexPath.Item]
    let cell =
        match view.DequeueReusableCell cellIdentifier with 
        | null -> new UITableViewCell(UITableViewCellStyle.Value1, cellIdentifier)
        | cell -> cell
    cell.TextLabel.Text <- t.Url
    cell.DetailTextLabel.Text <- t.Status 
    cell
override x.RowSelected (tableView, indexPath) = 
    tableView.DeselectRow (indexPath, false)
    navigation.PushViewController (new AddWebsiteViewController(sites.[indexPath.Item], false), true)
override x.CanEditRow (view, indexPath) = true
override x.CommitEditingStyle(view, editingStyle, indexPath) = 
    match editingStyle with 
    | UITableViewCellEditingStyle.Delete ->
        view.DeleteRows([|indexPath|], UITableViewRowAnimation.Fade)
    | _ -> Console.WriteLine "CommitEditingStyle:None called"

We now have a working app that texts if the website is not responding with an OK (200) status code.

image of app in emulator

image of texts from winpho

That's it. The code is available here and you can always email me (trsneed@gmail.com) or reach out to me on twitter (@timrsneed) with any questions or comments. Stay tuned later this week for a deep dive into several of these components, as well as the Node.js backend.

comments powered by Disqus