четверг, 17 июня 2010 г.

Playing with WebSharper

I’ve decided to make post about WebSharper long time ago…and finally time has come :).

The idea behind the WebSharper is not new, compilers that translate some source language to JavaScript already exists (for example GWT). Main distinguished feature of WS is using F# as a source language: built-in metaprogramming capabilities, type inference, succinct and expressive syntax, seamless integration with .NET platform makes it a very good choice both server (F# itself)and client side (translated JavaScript). Besides JavaScript translation WS provides statically-typed wrappers for existing JavaScript libraries (like JQuery), HTML combinators for defining content of pages, formlets and many other interesting things.

As a sample we’ll make simple StickyNotes application. Web design is not my primary and favorite skill, so I'll omit cross-browser compatibility and bind all styles to Firefox. For a start server-side part will be trivial: storing all notes data in application state. Later (in forthcoming posts) it will be extended with specific user notes, registration routine (with formlets), persisting data in database etc…

Server part

module State = 

type Note =
{ X : int
Y : int
Content : string }

let private key = "StickyNotesState"
let private doSave (notes : Note list) =
HttpContext.Current.Application.Set(key, notes)
notes

let private doLoad () =
match HttpContext.Current.Application.Get(key) with
| :? list<Note> as v -> v
| _ -> doSave []

let private lockObj = obj()

let save notes = lock lockObj (fun () -> doSave notes)
let load () = lock lockObj doLoad

Type Note stores basic note information (coordinates and content).Remaining part of the module just store/load functionality with HttpApplicationState on backend. This code has no WS specific features, just pure F#.

Client/Server communucations

module Rpc = 
[<Rpc>]
let loadNotes () =
State.load ()

[<Rpc>]
let saveNotes notes =
State.save notes

Client will make server calls througn invoking methods annotated with Rpc attribute.

Client side.

[<Require(typeof<Styles.StickyNotes>)>]
module Notes =

// client-side storage for notes
[<JavaScript>]
let notes = System.Collections.Generic.Dictionary<_, _>()

// configuration data for JQuery.animate function
type AnimateConfiguration = { opacity : float }

[<JavaScript>]
let main () =

// moves specified element to the top in z-order
let maxZ = ref 0
let bringToTop (e : Element) =
incr maxZ
e.Css("z-index", string (!maxZ))


let body = Div []

// create 'Note' visual component and append it to body
// if state is defined then it contains previousy stored state
let noteId = ref 0
let createNote (state : option<Note>) =
let currentId = !noteId
incr noteId

let edit = Div [ Class "edit"; Html5.Attr.ContentEditable "true"]
let close = Div [Class "closebutton"]
let rec note =
Div [Class "note"] -< [
Div [Class "header"] |>! OnMouseDown (fun _ _ -> bringToTop note)
close
edit
]

close |> OnClick(fun _ _ ->
note.JQuery.Animate({opacity=0.3}, 300.0, "linear", (fun () ->
notes.Remove(currentId) |> ignore
note.Remove()
)) |> ignore
)

// make element draggable
JQueryUI.Draggable.New(note, JQueryUI.DraggableConfiguration(Handle = ".header")) |> ignore
notes.Add(currentId, (note, edit))

match state with
| Some(n) ->
edit.Append n.Content
note.Css("left", string n.X + "px")
note.Css("top", string n.Y + "px")

| _ ->
()

body.Append(note)

// saves current snapshot of notes in server storage
let saveNotes (el : Element) (_ : JQueryEvent) =
el.SetAttribute("disabled", "true")
el.Text <- "Saving..."

notes
|> Seq.map(fun kv ->
let n,e = kv.Value
let pos = n.JQuery.Position()
{ X = pos.Left; Y = pos.Top; Content = e.Html }
)
|> Seq.toList
|> Rpc.saveNotes

el.Text <- "Save"
el.RemoveAttribute("disabled")


// restore previous state
let notes = Rpc.loadNotes()
for n in notes do
createNote (Some n)

Table [
TR [
TD [ Width "30"] -< [Button [Text "Create"] |>! OnClick(fun _ _ -> createNote None) ]
TD [ Button [Text "Save" ] |>! OnClick saveNotes ]
]
TR [TD [ColSpan "2" ] -< [body] ]
]


[<JavaScriptType>]
type Body() =
inherit Web.Control()
[<JavaScript>]
override this.Body = Notes.main ()

Notes:

  1. Javascript attribute marks items that should be compiled into JavaScript
  2. AnimateConfiguration type is static wrapper for calling .animate function. WebSharper JavaScript translator converts F# record types into JavaScript objects with matching fields. We need to pass fixed number of parameters so solution with record will be shorted than common approach from section 7.
  3. Element.Css function sets style property to given object via calling .css()
  4. Page structure is defined with handy HTML combinators(Div, Table etc…)
  5. -< combinator appends one sequence to another. It is basically used to create element both with attributes and child elements.
  6. |>! combinator allows attaching event handlers to elements in a composable way. Its definition is simple:
    let (|>!) x f = f x; x 
  7. JQueryUI.Draggable is a typed wrapper to  JQuery draggable plugin. It accepts parameters in form of DraggableConfiguration object: type with fields having DefaultValueAttibute attached. This is common convention for passing objects with optional fields to JavaScript code.
  8. When F# to JavaScript translator meets type annotated with JavaScriptTypeAttribute, it generates not only data fields but also class representation.

Also you’ve noticed Require attribute atop of Notes module. This attribute is utilized by WebSharper resource control system that tracks all necessary dependencies(css or js files) and orders them properly – all these activities are based on declarative information provided by developer. First of al you need to define a resource, in our sample it will be external css file.

module Styles = 
type StickyNotes() =
interface IResource with
member this.Render(r, w) =
let u = r.GetWebResourceUrl(typeof<StickyNotes>, "StickyNotes.css")
Resource.RenderCss u w

Resource is type that has default constructor and implements interface IResource.

After than annotate all types that depends on this resource with Require attribute (you can also apply RequireAssembly to assembly). WebSharper will build directed graph and use for resource management. ScriptManager control that should be embedded in the head section emits all necessary page resources preserving correct order.

Entire VS2010 solution with this sample can be found here, it already contains WebSharper JQueryUI extension but you also need WebSharper to be installed, so you can build and run the application.

Demonstration:

1. I’ve opened Firefox and created two notes

initial

2. One note is closed (it’s a pity, but I wasn’t able to capture fancy semi-transparent note when it dissapears)

first_deleted

3. I saved the state and opened the same page in Chrome (on the right).

same_page_in_chrome

Stay tuned!

Комментариев нет:

Отправить комментарий

 
GeekySpeaky: Submit Your Site!