Search Wiki:

F# for Silverlight

This download provides project templates for Silverlight 2 and Silverlight 3 class libraries with F#. Two samples give examples of how to use F# to define a new reusable Silverlight control, and how to implement the logic of a Silverlight applcation with F#.

Getting Started


Notes

The project templates are currently only available for VS2008 with the F# May CTP. The F# May CTP includes a Silverlight version of FSharp.Core.dll, which is deployed as part of the Silverlight application.

Samples


Silverlight 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.

http://lukeh.cloudapp.net/SilverlightConsoleEchoTestPage.aspx

<my:SilverlightConsole x:Name="console" Width="600" Height="400" FontFamily="Courier New" Foreground="GreenYellow" FontSize="13" FontWeight="Bold" Background="Black" Text="Loading..." />

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

  string line = null;
  while (line != "QUIT")
  {
    line = console.In.ReadLine();
    console.Out.WriteLine("ECHO: " + line);
  }

L-Systems


Lindenmayer Systems are an interesting way of generating a variety of fractals using a very simple set of rewrite rules. Check out the fascinating book The Algorithmic Beautry of Plants for details. The Silverlight application below uses an L-System rewriter and rendered written in F#.

http://lukeh.cloudapp.net/LSystemTestPage.aspx

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.of_seq expanded)
        for pt in normalize points do polyline.Points.Add(pt)
        isError

<UserControl 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit"
    xmlns:input="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Input.Toolkit"
    x:Class="LSystem.MainPage"
    Width="800" Height="2000">
    <StackPanel x:Name="LayoutRoot" Background="White">
        <Border BorderThickness="2" BorderBrush="Green" Margin="10"  controls:DockPanel.Dock="Top">
            <controls:DockPanel>
                <StackPanel controls:DockPanel.Dock="Left" Margin="10">
                    <controls:WrapPanel>
                        <controls:Label Content="Rules: " />
                        <TextBox x:Name="rules" Height="60" Width="200" Text="F->F+F-F-F+F" AcceptsReturn="True" LostFocus="UpdateLSystem"></TextBox>
                    </controls:WrapPanel>
                    <controls:WrapPanel>
                        <controls:Label Content="Initial:"/>
                        <TextBox x:Name="initial" Text="F" Width="200" LostFocus="UpdateLSystem"></TextBox>
                    </controls:WrapPanel>
                    <controls:WrapPanel>
                        <controls:Label Content="Angle:"/>
                        <input:NumericUpDown x:Name="angle" Minimum="-180" Maximum="180" Value="90" LostFocus="UpdateLSystem" ValueChanged="UpdateValue"/>
                    </controls:WrapPanel>
                    <controls:WrapPanel>
                        <controls:Label Content="Depth:"/>
                        <input:NumericUpDown x:Name="depth" Value="4"  Minimum="0" Maximum="100" LostFocus="UpdateLSystem" ValueChanged="UpdateValue"/>
                    </controls:WrapPanel>
                    <controls:WrapPanel>
                        <controls:Label Content="Step:  "/>
                        <input:NumericUpDown x:Name="step" Value="10" Minimum="1" Maximum="100" LostFocus="UpdateLSystem" ValueChanged="UpdateValue"/>
                    </controls:WrapPanel>
                </StackPanel>
                <StackPanel Margin="10" controls:DockPanel.Dock="Right" HorizontalAlignment="Right">
                    <controls:WrapPanel>
                        <controls:Label Content="Presets: "></controls:Label>
                        <ListBox x:Name="presets" SelectionChanged="presets_SelectionChanged">
                            <ListBoxItem Content="Koch Curve; F->F+F-F-F+F; F; 90; 4; 10"/>
                            <ListBoxItem Content="Sierpinski Triangle; A->B-A-B, B->A+B+A; A; 60; 6; 9"/>
                            <ListBoxItem Content="Dragon Curve; X->X+YF, Y->FX-Y; FX; 90; 12; 8"/>
                            <ListBoxItem Content="Koch Island; F->F-F+F+FF-F-F+F; F-F-F-F; 90; 3; 4"/>
                            <ListBoxItem Content="Hex Gosper; F->F+G++G-F--FF-G+, G->-F+GG++G+F--F-G; F; 60; 4; 9"/>
                            <ListBoxItem Content="Custom"/>
                        </ListBox>
                    </controls:WrapPanel>
                </StackPanel>
            </controls:DockPanel>
        </Border>
        <controls:Label x:Name="error"></controls:Label>
        <Canvas x:Name="drawingCanvas" Margin="10" Height="1000">
            <Polyline x:Name="polyline" Points="" Stroke="Blue" StrokeThickness="2"/>
        </Canvas>
    </StackPanel>
</UserControl>
Last edited Nov 7 2009 at 4:12 AM  by LukeH, version 12
Comments
damonwildercarr wrote  Jun 24 2009 at 7:42 PM  
Very nice!!!

Thorium wrote  Aug 27 2009 at 12:35 PM  
I wish it would work with VS2010 Beta...

Updating...
Page view tracker