DevTalk.net

A blog on programming and quantitative finance

RESTful Web Conversations 2

without comments

Having created a back-end using the new WCF Web API, I decided it was time to create a front-end and, not being a particular fan of JavaScript, went with WebSharper instead. Here’s a brief experience report.

Source code is, as always, available here: https://bitbucket.org/nesteruk/restfulwebconversation

JSONP, again…

Before doing anything, I mirrored the conversation data structure using the F# record type:

type ConversationItem = {
  Id : int
  PartyId : int
  Speech : string
  Responses : ConversationItem list
  }

So to try things out, I wrote a very simple function that simply did a GET of some data from the server:

[<JavaScript>]
let serverUrl = "http://localhost:51688/"
[<JavaScript>]
let Update(id:string) =
  JQuery.GetJSON(serverUrl + id,
    fun (result:ConversationItem, msg) ->
      JQuery.Of("#serverItem").Text(msg)) |> ignore

This raised the all too familiar error message:

XMLHttpRequest cannot load http://localhost:51688/. Origin null is not allowed by Access-Control-Allow-Origin.

For a moment I thought that maybe I’ve gone crazy and I’d have to implement JSONP support manually like I did for WCF REST (in a pre-.Net 4 scenario), so I tried to adjust the query string to explicitly invoke the callback:

[<JavaScript>]
let Update(id:string) =
  JQuery.GetJSON(serverUrl + id + "?callback=?",
    fun (result:ConversationItem, msg) ->
      JQuery.Of("#serverItem").Text(msg)) |> ignore

Now that I got this working, I hit another problem: the result I got from the service was XML! Now I know that JSON works fine with the new API, but when I opened Fiddler, what I saw is that the header had an Accept: */* instead! As it turns out, with JSONP you cannot even customize it, which means that my nice, resource-oriented server must be forced to return JSON by default.

While waiting for clarification on how to do this (typical fiddling in Web.config didn’t help one bit), I turned off Chrome web security and went with a JSON (as opposed to JSONP) approach. I suspect that the lack of JSONP support us due to the nature of the Web API, which isn’t ready yet.

(On a side note, I noticed that the entity names of my lists in XML had a wrong name. So using XML would have been impossible anyway without further fiddling the XML serializer.)

Consuming the Service

So to start with I create a bare-minimum HTML structure to house my UI:

[<JavaScript>]
let Conversation() =
  Div [
    P [Id "serverItem"] -< [Text "Server text here"]
    Div [Id "clientItems"]
    Button [Text "Hello"] |>! OnClick (fun x y -> Update(""))
  ]

And I subsequently try to project each piece of data onto the UI using the following piece of code:

[<JavaScript>]
let Update(id:string) =
  JQuery.GetJSON(serverUrl + id,
    fun (item:ConversationItem, msg) ->
      // what the server says
      JQuery.Of("#serverItem").Text(item.Speech).Ignore
      // what the client can respond with
      // hateoas but without atom:link
      if not (List.isEmpty item.Responses) then
        let listItems = 
          item.Responses |> List.map (fun r -> LI [ A [HRef "#"] -< [Text r.Speech]])
        let wholeList = OL [] -< listItems
        JQuery.Of("#clientItems").Empty().Append(wholeList.Dom).Ignore
    ) |> ignore

Now, we can turn to the next and most painful part of our site – the handling of links.

Links and HATEOAS

Okay, so what should those links contain? Common sense dictates that the URL they point to should contain the link to the conversation item that has just been pressed, e.g., http://mysite.com/123 where 123 is the ID of what I just said.

That’s all well and good but gets us nowhere, because we’re not interested in postbacks (it’s pure AJAX) and because even if we get the data at the URL, we’d still need to render it. Anyways, we can agree that we need the link, if only to do another AJAX call.

The moral of the story here is that you don’t get nice permalinks, you cannot add a bookmark to a conversation point (largely because the conversation has a context which we cannot preserve), but you can define AJAX-based link handling actions en masse. Essentially, we make the Update() function recursive and then wire up each link to call it:

let listItems = 
  item.Responses |> List.map 
    (fun r -> LI [ A [HRef "#"] -< [Text r.Speech]]
              |>! OnClick(fun x y -> Update(r.Id.ToString()))
    )

This works, but what about the progressive enabling/disabling of links? Our Empty() JavaScript call wipes all existing links but they must, presumably, be stored somewhere on the client in order to be useful.

If you remember my previous post, we agreed to make the list of enabled options a string such as 1,2,3. The server then repopulates the set of links each time we get a response, taking into account the links we already have. Now, how can we put together this string without creating any extra data structures? The answer is, of course, to use jQuery.

The agreement here is to assign each link its ID

fun r -> LI [ A [HRef "#"; Id (r.Id.ToString())] -< [Text r.Speech]]
...

and subsequently iterate over all of them, creating the query string. Now, due to F#‘s statelessness, the code below uses ref cells and is a bit ugly:

let enabledOptions = ref ""
 
JQuery.Of("#clientItems").Find("a").Each(
  fun (e:Dom.Element) -> let id = JQuery.Of(e).Attr("id")
                         enabledOptions := !enabledOptions + id + ",").Ignore

And then, of course, we pass enabledOptions as a parameter to the URL. Note that this could be considered bad by some REST purists used to clean URLs. However, this is something I can live with.

Conclusion

As you can see, I am not posting the full example online yet. There are a few reasons why I am not doing this:

  • I am still not sure what the situation with JSONP is right now. I hope that I’m just missing something in the new API, as opposed to having to implement this myself. Same goes for having JSON as the default output type for the service.

  • I am still looking for a way to define conversations more efficiently. The main problem right now is that the way I’m doing it with enable/disable list causes it to be a ’poor man’s HSM’ instead of a proper one. Perhaps the Stateless library could be of some use here.

  • Another problem I’m having is that, due to the fact that HTML is generated via JavaScript, I can’t see a place where I can initialize the content of my HTML after it has been returned. This is mainly due to my lack of knowledge of WebSharper, but I hope to figure it out before I post the next part.

Overall though, I’m happy to say that I didn’t write a single line of JavaScript to get it all working. To be continued!

Written by Dmitri Nesteruk

May 13th, 2011 at 2:55 pm

Posted in FSharp

Tagged with , , ,