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.