F# in Silverlight
Over the last couple years, there has been an explosion of interest in Silverlight. As a .NET-based runtime, it is possible to compile Silverlight applications with any .NET language, and we’ve seen a lot of F# developers using F# in Silverlight. However, until recently this involved building an application using the desktop version of the F# runtime, which could result in some pitfalls and mixed levels of success.
With the recent F# May CTP though, we now provide a Silverlight version of the F# runtime, FSharp.Core.dll, along with the F# release. This enables building truly first-class Silverlight components using F#.
To make this easier, I’ve posted some Silverlight F# project templates and samples on Code Gallery.
Download
F# Templates and Samples for Silverlight
Templates
Samples
L-Systems
Lindenmayer Systems are an interesting way of generating a variety of fractals using a simple set of rewrite rules. Check out the fascinating book The Algorithmic Beauty of Plants for details. The Silverlight application below uses an L-System rewriter and rendered written in F#.
- F, G, A, B are drawn as a move forward.
- + is a turn right.
- - is a turn right.
- X, Yand anything else is skipped during rendering.
open System.Windows
open System.Windows.Shapes
let rec internal applyRulesInOrder rules c =
match rules with
| [] -> string c
| rule::rules' -> match rule c with | None -> applyRulesInOrder rules' c
| Some result -> result
let internal step rules current =
current
|> String.collect (applyRulesInOrder rules)
let internal rotate (x,y) theta =
let x' = x * cos theta - y * sin theta
let y' = x * sin theta + y * cos theta
(x',y')
let rec internal render (x,y) (dx,dy) angle points system =
match system with | [] -> (x,-y)::points
| 'A'::system' | 'B'::system' | 'F'::system' | 'G'::system' -> let x',y' = x+dx,y+dy
render (x',y') (dx,dy) angle ((x,-y)::points) system'
| '+'::system' -> let (dx',dy') = rotate (dx,dy) angle
render (x,y) (dx',dy') angle points system'
| '-'::system' -> let (dx',dy') = rotate (dx,dy) (-angle)
render (x,y) (dx',dy') angle points system'
| _::system' -> render (x,y) (dx,dy) angle points system' let rec internal applyN f n x =
if n = 0 then x
else f (applyN f (n-1) x)
let internal normalize points =
let minX = points |> Seq.map (fun (x,_) -> x) |> Seq.min
let minY = points |> Seq.map (fun (_,y) -> y) |> Seq.min
points |> List.map (fun (x,y) -> new Point(x-minX, y-minY)) type LSystem(rulesString:string, start:string, angle:int, stepSize:int, n:int) =
let expanded,isError =
try let rules =
rulesString.Split([|"\r";"\n"|], System.StringSplitOptions.RemoveEmptyEntries)
|> Array.map (fun line -> line.Split([|"->"|], System.StringSplitOptions.RemoveEmptyEntries))
|> Array.map (fun fromAndTo -> (fromAndTo.[0].[0], fromAndTo.[1]))
let ruleFunctions = [ for (c, s) in rules -> fun x -> if x = c then Some s else None]
applyN (step ruleFunctions) n start, false with | e -> "", true member this.Render(polyline : Polyline) =
let points = render (0.0,0.0) (float stepSize,0.0) (float angle * System.Math.PI / 180.0) [] (List.ofSeq expanded)
for pt in normalize points do polyline.Points.Add(pt)
isError
Console Control
A resuable Silverlight control providing a console emulation. The control exposes input and ouput streams akin to those on the System.Console class. Could be used to provide console input and output as part of a Silverlight application, or as a way to convert Windows Console apps to Silverlight apps.
This samples hooks the Console up to a simple echo loop.
namespace System.Windows.Controls
open System
open System.IO
open System.Windows
open System.Windows.Controls
open System.Windows.Input
open SilverlightContrib.Utilities.ClipboardHelper
open System.Text
// A shared base implementation of Stream for
// use by the console input and output streams
[<AbstractClass>] type private ConsoleStream(isRead) =
inherit Stream()
override this.CanRead = isRead
override this.CanWrite = not isRead
override this.CanSeek = false override this.Position
with get() = raise (new NotSupportedException("Console stream does not have a position"))
and set(v) = raise (new NotSupportedException("Console stream does not have a position"))
override this.Length = raise (new NotSupportedException("Console stream does not have a length"))
override this.Flush() = ()
override this.Seek(offset, origin) = raise (new NotSupportedException("Console stream cannot seek"))
override this.SetLength(v) = raise (new NotSupportedException("Console stream does not have a length"))
/// A control representing a Console window
/// Provides an InputStream and OutputStream
/// for reading an writing character input.
/// Also supports copy/paste on some browsers
type SilverlightConsole() as self =
inherit TextBox()
// The queue of user input which has been collected by the
// console, but not yet read from the input stream
let readQueue = new System.Collections.Generic.Queue<int>()
// A stream that reads characters from user input
let inputStream =
{ new ConsoleStream(true) with override this.Write(buffer,offset,count) =
raise (new NotSupportedException("Cannot write from Console input stream"))
override this.Read(buffer,offset,count) =
do System.Diagnostics.Debug.WriteLine("Starting to read {0} bytes", count)
let rec waitForAtLeastOneByte() =
let shouldSleep = ref true let ret = ref [||]
lock readQueue (fun () -> shouldSleep := readQueue.Count < 1
if not !shouldSleep then let lengthToRead = min readQueue.Count count
ret := Array.init lengthToRead (fun i -> byte (readQueue.Dequeue())))
if !shouldSleep
then System.Threading.Thread.Sleep(100); waitForAtLeastOneByte()
else !ret
let bytes = waitForAtLeastOneByte()
System.Array.Copy(bytes, 0, buffer, offset, bytes.Length)
do System.Diagnostics.Debug.WriteLine("Finished reading {0} bytes", bytes.Length)
bytes.Length
}
// A stream that sends character output onto the console screen
let outputStream =
{ new ConsoleStream(false) with override this.Read(buffer,offset,count) =
raise (new NotSupportedException("Cannot read from Console output stream"))
override this.Write(buffer,offset,count) =
let isDelete = offset < 0
let newText =
if isDelete
then "" else UnicodeEncoding.UTF8.GetString(buffer, offset, count)
let _ = self.Dispatcher.BeginInvoke(fun () -> if isDelete then if self.Text.Length >= count then self.Text <- self.Text.Substring(0, self.Text.Length - count)
else do self.Text <- self.Text + newText
do self.SelectionStart <- self.Text.Length
do self.SelectionLength <- 0)
()
}
let shiftNumbers = [|')';'!';'@';'#';'$';'%';'^';'&';'*';'('|]
let currentInputLine = new System.Collections.Generic.List<int>()
// Handles key down events
// Processes the pressed key and turns it into console input
// Also echos the pressed key to the console
let keyDownHandler(keyArgs : KeyEventArgs) =
try do keyArgs.Handled <- true let shiftDown = Keyboard.Modifiers &&& ModifierKeys.Shift <> (enum 0)
let ctrlDown = Keyboard.Modifiers &&& ModifierKeys.Control <> (enum 0)
let p = keyArgs.PlatformKeyCode
if ctrlDown || keyArgs.Key = Key.Ctrl then if keyArgs.Key = Key.V then lock currentInputLine (fun () -> let clipboard = new ClipboardHelper()
let fromClipboard = clipboard.GetData()
for c in fromClipboard do do currentInputLine.Add(int c)
outputStream.WriteByte(byte c)
if c = '\n' then for i in currentInputLine do do System.Diagnostics.Debug.WriteLine("Enqueued {0}", char i)
do readQueue.Enqueue(i)
do currentInputLine.Clear()
)
elif keyArgs.Key = Key.C then let text = self.SelectedText
let clipboard = new ClipboardHelper()
clipboard.SetData(text)
else System.Diagnostics.Debug.WriteLine("Got key {0} {1} {2}", p, char p, keyArgs.Key)
let ascii =
match p with | n when n >= 65 && n <= 90 -> if shiftDown then p else p+32
| n when n >= 48 && n <= 57 -> if shiftDown then int shiftNumbers.[p-48] else p
| 8 -> 8
// backspace
| 13 -> int '\n'
| 32 -> int ' '
| 186 -> if shiftDown then int ':' else int ';'
| 187 -> if shiftDown then int '+' else int '='
| 188 -> if shiftDown then int '<' else int ','
| 189 -> if shiftDown then int '_' else int '-'
| 190 -> if shiftDown then int '>' else int '.'
| 191 -> if shiftDown then int '?' else int '/'
| 192 -> if shiftDown then int '~' else int '`'
| 219 -> if shiftDown then int '{' else int '['
| 220 -> if shiftDown then int '|' else int '\\'
| 221 -> if shiftDown then int '}' else int ']'
| 222 -> if shiftDown then int '\"' else int '\''
| _ -> -1
if ascii = 8 then lock currentInputLine (fun () -> if currentInputLine.Count > 0 then currentInputLine.RemoveAt(currentInputLine.Count - 1)
outputStream.Write([||], -1, 1)
)
elif ascii > 0 then lock currentInputLine (fun () -> do currentInputLine.Add(ascii)
outputStream.WriteByte(byte ascii)
)
if ascii = int '\n' then lock currentInputLine (fun () -> for i in currentInputLine do do System.Diagnostics.Debug.WriteLine("Enqueued {0}", char i)
if i = 10 then do readQueue.Enqueue(13)
do readQueue.Enqueue(i)
do currentInputLine.Clear())
do self.SelectionStart <- self.Text.Length
do self.SelectionLength <- 0
with | e -> System.Diagnostics.Debug.WriteLine(e)
// Lazily initialized StreamReader/StreamWriter
let outReader = lazy (new System.IO.StreamWriter(outputStream, Encoding.UTF8, 256, AutoFlush=true))
let inReader = lazy (new System.IO.StreamReader(inputStream, Encoding.UTF8, false, 256))
// Manually handle the Return key so we can accept newlines
do self.AcceptsReturn <- true
// Make sure a vertical scrollbar appears when needed
do self.VerticalScrollBarVisibility <- ScrollBarVisibility.Auto
// Make the control read-only so that users cannot move the cusor or change the contents
// Unfortunatley, this also greys it out - ideally we could seperate theese two.
do self.IsReadOnly <- true
// Hookup the keyDownHandler
do self.KeyDown.Add(keyDownHandler)
/// The raw input stream for the Console
member this.InputStream = inputStream :> Stream
/// The raw ouput stream for the Console
member this.OutputStream = outputStream :> Stream
/// A StreamWriter for writing to the Console
member this.Out = outReader.Value
/// A StreamReader for reading from the Console
member this.In = inReader.Value
Summary
Try out F# with Silverlight using the F# May CTP and the F# for Silverlight templates.
Comments
Anonymous
June 25, 2009
That's a splendid idea! What about a LOGO implementation? I wished I had the time to do it. Anyway, well done and keep going.Anonymous
June 25, 2009
Great FUN, F# and SL! Thanks ArtAnonymous
June 25, 2009
Great article, Luke! Looking forward to part 2: Hosting F# Interactive in Silverlight. DannyAnonymous
June 25, 2009
This is just so COOL!!! I was waiting for a long time to use SL in F#, Thanks a lot!!!! :)Anonymous
June 26, 2009
Too bad silverlight didn't give anything back to the browser or the OS while it was thinking and thrashing -- it had to power-cycle my machine when trying a complex hex rendering. No broswer action, no ctrl-shift-esc, no ctrl-alt-del...Anonymous
July 10, 2009
Hi, Nice examples. Do you have any idea when we might see project templates for F# Silverlight libraries for Visual Studio 2010 beta 1? thanks, RolyAnonymous
July 10, 2009
Roly - We expect to include F# templates for Silverlight development along with Visual Studio 2010 Beta2. For Beta1 though, we unfortunatley don't have a version of FSharp.Core.dll for Silverlight which ships with the release, so we can't easily provide an interim set of templates here, aside from those for VS2008, linked above.Anonymous
July 10, 2009
No probs. I'll wait for for beta 2! Thanks again for the great examples - love the L-systems one.Anonymous
November 10, 2009
This article's translated into Russian.