F# and WPF/XAML

F# is a great language, but its support is a bit limited. For example, you can make a WPF project in F# but you cannot use XAML.

In this post I will demonstrate how to create a simple calculator using F#, WPF and XAML. I did not compare the efficiency to the C# version, but I’m quite certain that it won’t be as efficient as the C# version (but not far behind).

I use Visual Studio 2010 and .NET 4.0 on Windows XP (32-bit).

Begin by creating a new console application, then opening the project properties and change project type from console to windows (so the user won’t see the console. You need to add several references:

  • System.Windows.Presentation
  • System.Xaml
  • PresentationFramework
  • PresentationCore
  • WindowsBase

I created an abbreviations.fs file with simple type abbreviations:

namespace FSharp_XAML_demo

open System.Windows
open System.Windows.Controls

type form = Window
type txt = TextBox
type btn = Button
type ehandler = RoutedEventHandler
type eargs = RoutedEventArgs

And a operators.fs file with basic definitions:

module FSharp_XAML_demo.operators

open FSharp_XAML_demo

let appname = "FSharp_XAML"

let window name =
    System.Windows.Application.LoadComponent(System.Uri(sprintf "/%s;component/%s" appname name, System.UriKind.Relative)) :?> form

let (?) (window : form) name =
    window.FindName name
    |> unbox

let inline (+=) (event : IEvent<_, _>) handler =
    (event :> Microsoft.FSharp.Control.IDelegateEvent<_>).AddHandler(ehandler(handler))

Sadly, appname has to be set for each project (this is the name of the output assembly). window is a function which gets the name of a XAML file which was marked as “Resource” and returns a form (System.Windows.Window) object. The (?) operator is used to dynamically get a control from a window. (+=) acts just like += in C# for events (adds a handler).

We also need a class which will handle the computations. This isn’t a very good example, but I wanted to keep it very simple.

namespace FSharp_XAML_demo

type Calculator(callback : string -> unit) =
    let mutable value = ""
    let mutable oldvalue : float option = None
    let mutable op : (float -> float -> float) option = None
    let mutable append = true
    member private this.GetValue() =
        try
            float value
        with _ -> 0.

    member this.applyDigit d =
        if append then
            value <- value + d
        else
            append <- true
            value <- d
        callback value

    member this.setDot() =
        if append then
            value <- value + "."
        else
            append <- true
            value <- "."
        callback value

    member this.setOp newop =
        match op, oldvalue with
        | Some opV, Some oldvalueV ->
            oldvalue <- Some(opV oldvalueV (this.GetValue()))
            value <- ""
            op <- Some newop
        | _ ->
            oldvalue <- Some(this.GetValue())
            value <- ""
            op <- Some newop
        callback "0"

    member this.eq() =
        value <- 
            match op, oldvalue with
            | Some opV, Some oldvalueV ->
                opV oldvalueV (this.GetValue())
                |> string
            | _ -> value
        oldvalue <- None
        op <- None
        append <- false
        callback value

Now, create a new file called Calc.xaml (change the Build Action to “Resource” in the properties window”) which will contain this:

<Window
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  Width="300" Height="360" Title="F# Calculator">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="42*" />
            <RowDefinition Height="68*" />
            <RowDefinition Height="68*" />
            <RowDefinition Height="68*" />
            <RowDefinition Height="68*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <TextBox Grid.ColumnSpan="4" Height="36" HorizontalAlignment="Left" Margin="4,4,0,0" VerticalAlignment="Top" Width="270"  Name="txtResult" IsReadOnly="True"
                 FontFamily="Courier New" FontSize="30" Text="0" />
        <Button Content="1" FontSize="20" Grid.Row="1" Height="69" HorizontalAlignment="Left" VerticalAlignment="Top" Width="69" Name="num1" />
        <Button Content="2" FontSize="20" Height="69" HorizontalAlignment="Left" VerticalAlignment="Top" Width="69" Grid.Column="1" Grid.Row="1" Name="num2" />
        <Button Content="3" FontSize="20" Height="69" HorizontalAlignment="Left" VerticalAlignment="Top" Width="69" Grid.Column="2" Grid.Row="1" Name="num3" Grid.ColumnSpan="2" />
        <Button Content="4" FontSize="20" Height="69" HorizontalAlignment="Left" VerticalAlignment="Top" Width="69" Grid.Row="2" Name="num4" />
        <Button Content="5" FontSize="20" Height="69" HorizontalAlignment="Left" VerticalAlignment="Top" Width="69" Grid.Column="1" Grid.Row="2" Name="num5" />
        <Button Content="6" FontSize="20" Grid.Column="2" Height="69" HorizontalAlignment="Left" VerticalAlignment="Top" Width="69" Grid.Row="2" Name="num6" />
        <Button Content="7" FontSize="20" Height="69" HorizontalAlignment="Left" VerticalAlignment="Top" Width="69" Grid.Row="3" Name="num7" />
        <Button Content="8" FontSize="20" Grid.Column="1" Height="69" HorizontalAlignment="Left" VerticalAlignment="Top" Width="69" Grid.Row="3" Name="num8" />
        <Button Content="9" FontSize="20" Grid.Column="2" Grid.ColumnSpan="2" Height="69" HorizontalAlignment="Left" VerticalAlignment="Top" Width="69" Grid.Row="3" Name="num9" />
        <Button Content="+" FontSize="20" Height="69" HorizontalAlignment="Left" VerticalAlignment="Top" Width="69" Grid.Column="3" Grid.Row="1" Name="opAdd" />
        <Button Content="-" FontSize="20" Grid.Row="2" Height="69" HorizontalAlignment="Left" VerticalAlignment="Top" Width="69" Grid.Column="3" Name="opSub" />
        <Button Content="x" FontSize="20" Grid.Row="3" Height="69" HorizontalAlignment="Left" VerticalAlignment="Top" Width="69" Grid.Column="3" Name="opMul" />
        <Button Content="0" FontSize="20" Height="69" HorizontalAlignment="Left" VerticalAlignment="Top" Width="69" Grid.Row="4" Name="num0" />
        <Button Content="." FontSize="20" Grid.Column="1" Height="69" HorizontalAlignment="Left" VerticalAlignment="Top" Width="69" Grid.Row="4" Name="opDot" />
        <Button Content="=" FontSize="20" Grid.Column="2" Height="69" HorizontalAlignment="Left" VerticalAlignment="Top" Width="69" Grid.Row="4" Name="opEq" />
        <Button Content="÷" FontSize="20" Grid.Column="3" Height="69" HorizontalAlignment="Left" VerticalAlignment="Top" Width="69" Grid.Row="4" Name="opDiv" />
    </Grid>
</Window>

Notice that even though F# doesn’t have native support for XAML, Visual Studio still gives us the lovely XAML designer.

Now we have to create the matching code file, “Calc.xaml.fs”:

namespace FSharp_XAML_demo

open System.Windows
open FSharp_XAML_demo.operators

type Calc private (xaml : form) as this =
    let txtResult : txt = xaml?txtResult
    let num0 : btn = xaml?num0
    let num1 : btn = xaml?num1
    let num2 : btn = xaml?num2
    let num3 : btn = xaml?num3
    let num4 : btn = xaml?num4
    let num5 : btn = xaml?num5
    let num6 : btn = xaml?num6
    let num7 : btn = xaml?num7
    let num8 : btn = xaml?num8
    let num9 : btn = xaml?num9
    let opAdd : btn = xaml?opAdd
    let opSub : btn = xaml?opSub
    let opMul : btn = xaml?opMul
    let opDiv : btn = xaml?opDiv
    let opDot : btn = xaml?opDot
    let opEq : btn = xaml?opEq
    let calc = Calculator(fun value -> txtResult.Text <- value)

    do
        num0.Click += this.num_Click
        num1.Click += this.num_Click
        num2.Click += this.num_Click
        num3.Click += this.num_Click
        num4.Click += this.num_Click
        num5.Click += this.num_Click
        num6.Click += this.num_Click
        num7.Click += this.num_Click
        num8.Click += this.num_Click
        num9.Click += this.num_Click
        opAdd.Click += this.op_Click
        opSub.Click += this.op_Click
        opMul.Click += this.op_Click
        opDiv.Click += this.op_Click
        opDot.Click += this.opDot_Click
        opEq.Click += this.opEq_Click

    new () =
        Calc(window "Calc.xaml")

    member this.num_Click (sender : obj) (_ : eargs) =
        let asButton = sender :?> btn
        calc.applyDigit (asButton.Content.ToString())

    member this.op_Click (sender : obj) (_ : eargs) =
        let asButton = sender :?> btn
        match asButton.Content.ToString() with
        | "+" -> calc.setOp (+)
        | "-" -> calc.setOp (-)
        | "x" -> calc.setOp (*)
        | "÷" -> calc.setOp (/)
        | _ -> ()

    member this.opDot_Click (_ : obj) (_ : eargs) =
        calc.setDot()

    member this.opEq_Click (_ : obj) (_ : eargs) =
        calc.eq()

    member this.Run() =
        (new Application()).Run xaml

Let’s see what we got here. When a Calc object is created, all the fields are initialized and event handlers are attached to events. The public constructor calls the private constructor with the loaded XAML Window. The event handlers stand for themselves and the run member is quite simple, too.

And Program.fs is as simple as Program.cs:

open System
open FSharp_XAML_demo
open FSharp_XAML_demo.operators
[<EntryPoint>]
[<STAThread>]
let main _ =
    Calc().Run()

That’s it. Only thing we are missing is the automation process which other languages have, but in general – it is the same.

Download source here: FSharp_XAML

This entry was posted on Saturday, October 9th, 2010 at 7:54 PM and is filed under Default. You can follow any responses to this entry through the RSS 2.0 feed. You can skip to the end and leave a response. Pinging is currently not allowed.

Leave a Reply