Friday, January 28, 2011

WebSharper Sitelets Cont’d: Developing a Blog Service

The previous article might have left you with an erroneous impression that WebSharper sitelets only support a static set of URLs. This is not the case. In fact, the user-defined Action type can be an arbitrary data structure that maps to a URL. This comes particularly handy when designing a blogging website, since the set of blogs and their URLs is not statically bounded. Something like this would do:

type Id = int
type Action =
| ShowRecentBlogs
| CreateBlog
| ReadBlog of Id
| UpdateBlog of Id
| DeleteBlog of Id
view raw BlogAction.fs hosted with ❤ by GitHub

Now you would need to write a router, which is roughly a bijection between Action values and URLs. A boring way to do so might look like this:

let Router : Router<Action> =
let route = function
| GET (_, SPLIT_BY '/' []) ->
Some ShowRecentBlogs
| GET (_, SPLIT_BY '/' ["create"]) ->
Some CreateBlog
| GET (_, SPLIT_BY '/' ["read"; INT id]) ->
Some (ReadBlog id)
| GET (_, SPLIT_BY '/' ["update"; INT id]) ->
Some (UpdateBlog id)
| GET (_, SPLIT_BY '/' ["delete"; INT id]) ->
Some (DeleteBlog id)
| _ ->
None
let link x =
let uri =
match x with
| ShowRecentBlogs -> "/"
| CreateBlog -> "/create"
| ReadBlog x -> sprintf "/read/%i" x
| UpdateBlog x -> sprintf "/update/%i" x
| DeleteBlog x -> sprintf "/delete/%i" x
Some (Uri(uri, UriKind.Relative))
Router.New route link
view raw BlogRouter.fs hosted with ❤ by GitHub

Writing routers by hand you get full control – you can inspect an Http.Request value when routing, and generate any URL when linking. However writing the above code is not much fun. If you do not particularly care for the exact URLs, there is a much shorter way to get there:

let Router : Router<Action> = Router.Infer()
view raw AutoRouter.fs hosted with ❤ by GitHub

The Infer function inspects the Action type by reflection to derive a router automatically. It is smart enough to handle records, unions, most scalar types, lists, and options. In our case it derives the following mapping:

ShowRecentBlogs <=> /ShowRecentBlogs
CreateBlog <=> /CreateBlog
ReadBlog $x <=> /ReadBlog/$x
UpdateBlog $x <=> /UpdateBlog/$x
DeleteBlog $x <=> /DeleteBlog/$x
A smart thing to do would be to alter it just a little bit, for example by making ShowRecentBlogs available at the root. This can be done by combining a simple table router with the inferred router:

let Router : Router<Action> =
Router.Table [ShowRecentBlogs, "/"]
<|> Router.Infer()

This is much better: 80% of the benefit with 20% of the work.

Now that URL routing is out of the way, you can finally get down to the blog service itself. I present the most interesting thing first, the controller:

let Controller =
let handle = function
| ShowRecentBlogs ->
let blogs = Model.WithBlogs <| fun db -> db.GetRecentBlogs()
View.ShowRecentBlogs blogs
| CreateBlog ->
View.CreateBlog ()
| ReadBlog id ->
let blog = Model.WithBlogs <| fun db -> db.ReadBlog id
match blog with
| Some blog ->
View.ReadBlog blog
| None ->
Content.NotFound
| UpdateBlog id ->
let blog = Model.WithBlogs <| fun db -> db.ReadBlog id
match blog with
| None -> Content.Redirect Action.CreateBlog
| Some blog -> View.UpdateBlog blog
| DeleteBlog id ->
let blog = Model.WithBlogs <| fun db ->
let blog = db.ReadBlog id
if blog.IsSome then
db.DeleteBlog id
|> ignore
blog
match blog with
| Some blog ->
View.BlogDeleted blog
| None ->
Content.ServerError
{ Handle = handle }

The controller simply handles actions (Action values) and returns responses (Content values), no surprises here. Together with a router, the controller forms a sitelet, a self-contained website ready to go by itself or be embedded in a bigger application:

let Main : Sitelet<Action> =
{
Controller = Controller
Router = Router
}
view raw BlogsSitelet.fs hosted with ❤ by GitHub

For the model in this example, you can use the simplest thing that can possibly work in a development environment, namely a static mutable variable (see the listing at the end of the article). Now, views are quite straightforward as well. The only thing of interest is the use of WebSharper Formlets (client-side form combinators) to build the UI for editing and creating the blogs. I build a simple control that renders the form and talks to the model directly over RPC, redirecting to the homepage when done:

module Client =
open IntelliFactory.WebSharper
open IntelliFactory.WebSharper.Formlet
[<Rpc>]
let CreateBlog blog =
Model.WithBlogs <| fun db ->
db.CreateBlog { blog with Date = DateTime.UtcNow }
[<Rpc>]
let UpdateBlog blog =
Model.WithBlogs <| fun db ->
db.UpdateBlog { blog with Date = DateTime.UtcNow }
[<Inline "document.location = $location">]
let Redirect (location: string) = X<unit>
type BlogControl(homeLocation: string, blog: option<Blog>) =
inherit Web.Control()
new () = new BlogControl("?", None)
[<JavaScript>]
override this.Body =
let (id, title, summary, text) =
match blog with
| None -> (0, "", "", "")
| Some b -> (b.Id, b.Title, b.Summary, b.Text)
let titleForm =
Controls.Input title
|> Enhance.WithTextLabel "Title"
|> Validator.IsNotEmpty "Required."
let summaryForm =
Controls.TextArea summary
|> Enhance.WithTextLabel "Summary"
|> Validator.IsNotEmpty "Summary is required."
let textForm =
Controls.TextArea text
|> Enhance.WithTextLabel "Text"
|> Validator.IsNotEmpty "Text is required"
Formlet.Yield (fun title summary text ->
{
Id = id
Date = DateTime.UtcNow
Title = title
Summary = summary
Text = text
})
<*> titleForm
<*> summaryForm
<*> textForm
|> Enhance.WithLabelConfiguration {
Layout.LabelConfiguration.Default with
VerticalAlign = Layout.VerticalAlign.Top
}
|> Enhance.WithSubmitAndResetButtons
|> Enhance.WithFormContainer
|> Formlet.Run (fun newBlog ->
match blog with
| None -> ignore (CreateBlog newBlog)
| Some _ -> ignore (UpdateBlog newBlog)
Redirect homeLocation)
view raw BlogClient.fs hosted with ❤ by GitHub

Time to try it out:

And here is the complete code listing:

module My.Blogs
open System
open System.Collections.Generic
open System.Web
open IntelliFactory.WebSharper.Sitelets
type Id = int
type Html = string
type Blog =
{
Id : Id
Title : string
Date : DateTime
Summary : Html
Text : Html
}
type Action =
| ShowRecentBlogs
| CreateBlog
| ReadBlog of Id
| UpdateBlog of Id
| DeleteBlog of Id
module Model =
type Blogs =
{
GetRecentBlogs : unit -> seq<Blog>
CreateBlog : Blog -> Id
UpdateBlog : Blog -> bool
ReadBlog : Id -> option<Blog>
DeleteBlog : Id -> bool
}
let private data =
let d = Dictionary()
d.[0] <-
{
Id = 0
Title = "Blog-1"
Date = System.DateTime.Now
Summary = "summary.."
Text = "text.."
}
d.[1] <-
{
Id = 1
Title = "Blog-2"
Date = System.DateTime.Now
Summary = "summary.."
Text = "text.."
}
d :> IDictionary<_,_>
let WithBlogs action =
lock data <| fun () ->
let d = data
action {
GetRecentBlogs = fun blog ->
d.Values
|> Seq.toArray
|> Array.rev
:> seq<_>
CreateBlog = fun blog ->
let k = d.Count
d.[k] <- { blog with Id = k }
k
UpdateBlog = fun blog ->
if d.ContainsKey blog.Id then
d.[blog.Id] <- blog
true
else
false
ReadBlog = fun id ->
match d.TryGetValue id with
| true, blog -> Some blog
| _ -> None
DeleteBlog = fun id ->
d.Remove id
}
module Client =
open IntelliFactory.WebSharper
open IntelliFactory.WebSharper.Formlet
[<Rpc>]
let CreateBlog blog =
Model.WithBlogs <| fun db ->
db.CreateBlog { blog with Date = DateTime.UtcNow }
[<Rpc>]
let UpdateBlog blog =
Model.WithBlogs <| fun db ->
db.UpdateBlog { blog with Date = DateTime.UtcNow }
[<Inline "document.location = $location">]
let Redirect (location: string) = X<unit>
type BlogControl(homeLocation: string, blog: option<Blog>) =
inherit Web.Control()
new () = new BlogControl("?", None)
[<JavaScript>]
override this.Body =
let (id, title, summary, text) =
match blog with
| None -> (0, "", "", "")
| Some b -> (b.Id, b.Title, b.Summary, b.Text)
let titleForm =
Controls.Input title
|> Enhance.WithTextLabel "Title"
|> Validator.IsNotEmpty "Required."
let summaryForm =
Controls.TextArea summary
|> Enhance.WithTextLabel "Summary"
|> Validator.IsNotEmpty "Summary is required."
let textForm =
Controls.TextArea text
|> Enhance.WithTextLabel "Text"
|> Validator.IsNotEmpty "Text is required"
Formlet.Yield (fun title summary text ->
{
Id = id
Date = DateTime.UtcNow
Title = title
Summary = summary
Text = text
})
<*> titleForm
<*> summaryForm
<*> textForm
|> Enhance.WithLabelConfiguration {
Layout.LabelConfiguration.Default with
VerticalAlign = Layout.VerticalAlign.Top
}
|> Enhance.WithSubmitAndResetButtons
|> Enhance.WithFormContainer
|> Formlet.Run (fun newBlog ->
match blog with
| None -> ignore (CreateBlog newBlog)
| Some _ -> ignore (UpdateBlog newBlog)
Redirect homeLocation)
module View =
open IntelliFactory.Html
open IntelliFactory.WebSharper.Formlet
let ( => ) a b =
A [HRef b] -< [Text a]
let Page title makeBody =
PageContent <| fun ctx ->
{ Page.Default with
Title = Some title
Body =
[
UL [
LI ["Home" => ctx.Link ShowRecentBlogs]
LI ["Post" => ctx.Link CreateBlog]
]
Div (makeBody ctx)
]
}
let ShowRecentBlogs (blogs: seq<Blog>) =
Page "News" <| fun ctx ->
[
yield H1 [Text "News"]
for b in blogs do
yield A [HRef (ctx.Link (ReadBlog b.Id))]
-< [H2 [Text b.Title]]
yield P [Text b.Summary]
yield
UL [
LI [P ["Edit" => ctx.Link (UpdateBlog b.Id)]]
LI [P ["Delete" => ctx.Link (DeleteBlog b.Id)]]
]
]
let ReadBlog (blog: Blog) =
Page blog.Title <| fun ctx ->
[
H1 [Text blog.Title]
P [Text blog.Summary]
P [Text blog.Text]
UL [
LI [P ["Edit" => ctx.Link (UpdateBlog blog.Id)]]
LI [P ["Delete" => ctx.Link (DeleteBlog blog.Id)]]
]
]
let BlogDeleted (blog: Blog) =
Page "Blog Deleted" <| fun ctx ->
[
P [
Text "Successfully deleted blog: "
Text blog.Title
]
]
let CreateBlog () =
Page "New Post" <| fun ctx ->
let home = ctx.Link Action.ShowRecentBlogs
[
new Client.BlogControl(home, None)
]
let UpdateBlog (blog: Blog) =
Page "Update" <| fun ctx ->
let home = ctx.Link Action.ShowRecentBlogs
[
new Client.BlogControl(home, Some blog)
]
let Controller =
let handle = function
| ShowRecentBlogs ->
let blogs = Model.WithBlogs <| fun db -> db.GetRecentBlogs()
View.ShowRecentBlogs blogs
| CreateBlog ->
View.CreateBlog ()
| ReadBlog id ->
let blog = Model.WithBlogs <| fun db -> db.ReadBlog id
match blog with
| Some blog ->
View.ReadBlog blog
| None ->
Content.NotFound
| UpdateBlog id ->
let blog = Model.WithBlogs <| fun db -> db.ReadBlog id
match blog with
| None -> Content.Redirect Action.CreateBlog
| Some blog -> View.UpdateBlog blog
| DeleteBlog id ->
let blog = Model.WithBlogs <| fun db ->
let blog = db.ReadBlog id
if blog.IsSome then
db.DeleteBlog id
|> ignore
blog
match blog with
| Some blog ->
View.BlogDeleted blog
| None ->
Content.ServerError
{ Handle = handle }
let Router : Router<Action> =
Router.Table [ShowRecentBlogs, "/"]
<|> Router.Infer()
let Main : Sitelet<Action> =
{
Controller = Controller
Router = Router
}
view raw BlogEngine.fs hosted with ❤ by GitHub

Friday, January 21, 2011

WebSharper sitelets: building a two-page website

Let me show the simplest possible self-contained example involving WebSharper sitelets that are coming with the 2.1 release. You define a website with two pages (and two actions):

type Action =
| Home
| AboutUs

Let us quickly mash up a template for the website using F#-embedded HTML combinators. A template is just a function taking the body and decorating it:

module View =
let ( => ) a b =
A [HRef b] -< [Text a]
let Page title body =
PageContent <| fun ctx ->
{
Page.Default with
Title = Some title
Body =
H1 [Text title]
::
UL [
LI ["Home" => ctx.Link Home]
LI ["AboutUs" => ctx.Link AboutUs]
]
::
body
}
view raw Sitelet.View.fs hosted with ❤ by GitHub

Two things to notice:

  1. F# lets you define your own syntax, and the example makes liberal use of that (=>).
  2. Instead of generating URLs by hand, you ask the context to create a link to the action. This ensures safety: renaming Home action makes the project stop compiling, and moving it to a different URL keeps the links correct.

Now, you define the sitelets:

module Site =
let HomePage =
View.Page "Home" [
Div [Text "Welcome to our website!"]
]
|> Sitelet.Content "/" Home
let AboutUsPage =
View.Page "About Us" [
Div [Text "TODO: describe us."]
]
|> Sitelet.Content "/about" AboutUs
let Main =
Sitelet.Sum [
HomePage
AboutUsPage
]
view raw Sitelet.Site.fs hosted with ❤ by GitHub

HomePage and AboutUsPage are single-page sitelets, with a single URL and a single Action. They are combined into a website by the Sum operator.

Now, a little bit of administrative boilerplate:

type Website() =
interface IWebsite<Action> with
member this.Actions = []
member this.Sitelet = Site.Main
[<assembly: WebsiteAttribute(typeof<Website>)>]
do ()

And you are done! Let’s browse:

s2 s1

So far so good. The pages have the expected URLs and the menu links work.

The above could have been accomplished by any reasonable web framework. Let us push the limit and spice it up a bit now. Let us add a few lines of F# that will actually compile to JavaScript:

module Client =
open IntelliFactory.WebSharper.Html
[<JavaScript>]
let Button label =
Button [Text label]
|>! OnClick (fun button _ ->
button.Text <- "CLICKED")
type ButtonControl(label: string) =
inherit Web.Control()
new () = new ButtonControl("unlabelled")
[<JavaScript>]
override this.Body = Button label :> _

So there is a button that changes its title when clicked. AND there is a control. Now, this while the Button lives on the client-side completely (constructs DOM nodes, in fact), the Control executes a quantum leap: the constructor executes on the server, and the body on the client. But that means you can use the Control to glue the server and the client together. Let us update the AboutUs page:

let AboutUsPage =
View.Page "About Us" [
Div [Text "TODO: describe us."]
Div [new Client.ButtonControl("Click me!")]
]
|> Sitelet.Content "/about" AboutUs

That’s it. The user will now see a clickable, JavaScript-enabled, F#-implemented button, right where you expect it. No script tags to worry about, no dependency chasing, no “ondocumentready” worries, it just works:

s2

Below is the complete listing. As soon as the 2.1 release becomes public, you will able to enjoy running it yourself. Stay tuned!

namespace WebSharperSiteletsProject
open System
open System.IO
open System.Web
open IntelliFactory.Html
open IntelliFactory.WebSharper
open IntelliFactory.WebSharper.Sitelets
type Action =
| Home
| AboutUs
module View =
let ( => ) a b =
A [HRef b] -< [Text a]
let Page title body =
PageContent <| fun ctx ->
{
Page.Default with
Title = Some title
Body =
H1 [Text title]
::
UL [
LI ["Home" => ctx.Link Home]
LI ["AboutUs" => ctx.Link AboutUs]
]
::
body
}
module Client =
open IntelliFactory.WebSharper.Html
[<JavaScript>]
let Button label =
Button [Text label]
|>! OnClick (fun button _ ->
button.Text <- "CLICKED")
type ButtonControl(label: string) =
inherit Web.Control()
new () = new ButtonControl("unlabelled")
[<JavaScript>]
override this.Body = Button label :> _
module Site =
let HomePage =
View.Page "Home" [
Div [Text "Welcome to our website!"]
]
|> Sitelet.Content "/" Home
let AboutUsPage =
View.Page "About Us" [
Div [Text "TODO: describe us."]
Div [new Client.ButtonControl("Click me!")]
]
|> Sitelet.Content "/about" AboutUs
let Main =
Sitelet.Sum [
HomePage
AboutUsPage
]
type Website() =
interface IWebsite<Action> with
member this.Actions = []
member this.Sitelet = Site.Main
[<assembly: WebsiteAttribute(typeof<Website>)>]
do ()

Thursday, January 20, 2011

Can WebSharper beat ASP.NET MVC at the routing game?

At work we have been having a lot of fun designing WebSharper sitelets lately. A quick recap: WebSharper is our web development framework that is all about cross-compiling F# to neatly interoperating client-side JavaScript and server-side .NET assemblies - a GWT of sorts. Sitelets are the recently introduced server-side abstraction, the new kid on the block where the bully is of course the notorious ASP.NET MVC.

ASP.NET MVC operation is all about controllers and actions: a request comes in, is matched to a controller/action, and then gets handled. At first glance it appears that controllers are classes and actions are methods. But NO! By introducing something they consider an improvement, the MVC team decoupled actions from methods and insists now that both controllers and actions are strings:

http://haacked.com/archive/2008/08/29/how-a-method-becomes-an-action.aspx

If you now want to construct a link to a particular action in a view, you have to know it by its string name. And what if it changes? Bummer, the project compiles fine and then fails at runtime.

This is particularly sad given that they used to offer a safer way:

http://weblogs.asp.net/scottgu/archive/2007/12/03/asp-net-mvc-framework-part-2-url-routing.aspx 

view raw ActionLink.cs hosted with ❤ by GitHub

While ASP.NET MVC fans wait for this feature to come back, we can do better with F#. The basic idea is that we model everything first-class: an action is just a value of a user-defined Action data type, and a controller is a value as well. Consider:

type Action =
| HomePage
| AboutUs
| BlogHomePage
| Blog of BlogId

Now, a controller is simply a function of type Action –> Response. And a website? A website, or a sitelet, as we call it in WebSharper, is a controller coupled with a router, where a router is a bijection from locations to actions.

While the controller is fun to write, the router is not. One tedious way to write it is to write two F# functions, one pattern-matching a location and returning an Action, and the other doing the reverse. For actions that can be listed in a finite list, a less tedious is to write a table with a 1:1 correspondence between locations and actions. But the easiest is to automate it all:

Router.Infer<'T>() : Router<'T>
view raw RouterInfer.fs hosted with ❤ by GitHub

This little function will automatically infer locations such as “/HomePage”, “/AboutUs”, “/Blog/123”... All the tedious work done, time to write the controller!

And how about safe links in the view? Nothing is easier:

A [Href (ctx.Link (Blog 5))] [Text "Next blog"]
view raw ViewExample.fs hosted with ❤ by GitHub

And if anything happens to change, F# compiler will make sure the links are still correct.

Sitelets offer a lot more than this, such as easy embedding of F#-defined client-side (JavaScript) controls or the pre-generation of the static part of the web application as a set of HTML and resource files. But none of it can beat the fun of designing routing the functional way.