FSharp.Qualia


Tutorial - WinForms

A minimal Qualia app needs 5 types:

  • an Event discriminated union
  • a model, with reactive properties
  • a view, usually linked to a WinForms or WPF window, listening to the model changes and send events
  • a dispatcher, handling these events and transforming the model
  • an event loop, wiring all previous types

The example is a simple winforms numeric up down. The final app will look like this:

numericupdown

The + and - buttons increment or decrement the value, which is displayed in a label and a textbox. Changin the value in the textbox will update the value if it can be parsed to an int.

Event type

This is the core of Qualia - the type describing everything that happens in the app.

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
open System
open System.Windows.Forms
open FSharp.Qualia

type MainEvents = 
    | Up // + Button clicked
    | Down // - Button clicked
    | Edit of string // textbox content changed - the string is the new content of the textbox

Model

Only one property: the value. ReactiveProperty is basically a wrapper around Rx BehaviorSubject. It's an IObservable that will raise the last stored value on subscription. Its parameter is the initial value

1: 
2: 
type MainModel() = 
    member val Value = new ReactiveProperty<int>(0)

View

The view is split in two parts: the form itself and the Qualia View.

Form

The actual Form backing the view. all exposed members will be used by the view.

This could be a WinForms designer type, or a WPF one using the awesome FsXaml. There is a WPF sample of the NumericUpDown in the repo.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
type MainForm() =
    inherit Form()

    let p = new TableLayoutPanel()
    let addAt c i j =
        p.Controls.Add c
        p.SetCellPosition(c, TableLayoutPanelCellPosition(i,j))
        
    member val ButtonUp = new Button(Text="+")
    member val ButtonDown = new Button(Text="-")
    member val TextBox = new TextBox()
    member val Label = new Label()

    override x.OnLoad(e) =
        x.Controls.Add(p)
        addAt x.ButtonUp 1 0
        addAt x.ButtonDown 1 1
        addAt x.TextBox 0 0
        addAt x.Label 0 1

Qualia View

Qualia Views are templated with three types : the event one, the visual element one and the model.

1: 
2: 
type MainView(mw : MainForm, m) = 
    inherit View<MainEvents, Form, MainModel>(mw, m)

EventStreams returns a list of IObservable. Observable.mapTo maps an observable to a constant. There's also a --> operator defined for conciseness

1: 
2: 
3: 
4: 
    override this.EventStreams = 
        [ mw.ButtonUp.Click |> Observable.mapTo Up // map first button clicked event
          mw.ButtonDown.Click |> Observable.mapTo Down // second one
          mw.TextBox.TextChanged |> Observable.map (fun e -> Edit mw.TextBox.Text) ] // map textchanged events

SetBindings subscribe to property changes in the model and updates the ui accordingly. In this case, when the model value changes, we set both the label and the textbox content

1: 
2: 
3: 
    override this.SetBindings(m : MainModel) = 
        m.Value.Add(fun v -> mw.Label.Text <- (string v))
        m.Value.Add(fun v -> mw.TextBox.Text <- (string v))

Dispatcher

The dispatcher is a glorifed event handler. It reacts to events sent by the view by modifing the model. Each event can be handled synchronously or asynchronously. The Dispatcher method returns a wrapped handler.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
type MainDispatcher() = 
    // up and down events just increment or decrement the model's value
    let up (m : MainModel) = m.Value.Value <- m.Value.Value + 1
    let down (m : MainModel) = m.Value.Value <- m.Value.Value - 1
    // edit tries to parse the given string
    let edit str (m : MainModel) = 
        match Int32.TryParse str with
        | true, i -> m.Value.Value <- i
        | false, _ -> ()
    
    // the dispatcher does not deal with the view - just need the event and model types
    interface IDispatcher<MainEvents, MainModel> with
        member this.InitModel _ = () // nothing to do here
        member this.Dispatcher = 
            function 
            | Up -> Sync up
            | Down -> Sync down
            | Edit str -> Sync(edit str)

Event Loop

Finally, we need to use these types and run the application:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
[<STAThread>]
[<EntryPoint>]
let main argv = 
    let v = MainView(new MainForm(), MainModel())
    let mvc = EventLoop(v, MainDispatcher())
    use eventloop = mvc.Start() // wires all events/streams/...
    v.Root.ShowDialog() |> ignore // v.Root is the backing visual element provided, here the MainForm instance.
    0

WPF version

Let's convert the winforms app in a WPF one. We only need to replace the view.

First, let's create a MainWindow.xaml file in the project, set it's type to Resource and put this in there :

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="WPF NumericUpDown" Height="200" Width="400">
    <StackPanel>
        <Label x:Name="label"/>
        <Button x:Name="buttonUp">+</Button>
        <Button x:Name="buttonDown">-</Button>
        <TextBox x:Name="textBox"/>
    </StackPanel>
</Window>

We then use FsXaml to define a type for the window, allowing us to access to named elements :

1: 
2: 
3: 
open FsXaml

type MainWindow = XAML<"MainWindow.xaml", true>

The view:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
type MainWPFView(mw : MainWindow, m) = 
    inherit View<MainEvents, Window, MainModel>(mw.Root, m)
    
    override this.EventStreams = 
        [ mw.buttonUp.Click |> Observable.mapTo Up
          mw.buttonDown.Click |> Observable.mapTo Down
          mw.textBox.TextChanged |> Observable.map (fun e -> Edit mw.textBox.Text) ]
    
    override this.SetBindings(m : MainModel) = 
        m.Value.Add(fun v -> mw.label.Content <- v)
        m.Value.Add(fun v -> mw.textBox.Text <- (string v))

        

Now the main function, running a WPF System.Windows.Application:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
[<STAThread>]
[<EntryPoint>]
let main argv = 
    let app = Application()
    let v = MainView(MainWindow(), MainModel())
    let mvc = EventLoop(v, MainDispatcher())
    use eventloop = mvc.Start()
    app.Run(window = v.Root)
namespace System
namespace System.Windows
namespace System.Windows.Forms
namespace FSharp
namespace FSharp.Qualia
type MainEvents =
  | Up
  | Down
  | Edit of string

Full name: Tutorial.MainEvents
union case MainEvents.Up: MainEvents
union case MainEvents.Down: MainEvents
union case MainEvents.Edit: string -> MainEvents
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = String

Full name: Microsoft.FSharp.Core.string
Multiple items
type MainModel =
  new : unit -> MainModel
  member Value : ReactiveProperty<int>

Full name: Tutorial.MainModel

--------------------
new : unit -> MainModel
Multiple items
type ReactiveProperty<'a> =
  interface IObservable<'a>
  new : init:'a -> ReactiveProperty<'a>
  new : source:IObservable<'a> * init:'a -> ReactiveProperty<'a>
  override ToString : unit -> string
  member Value : 'a
  member private sub : BehaviorSubject<'a>
  member Value : 'a with set

Full name: FSharp.Qualia.ReactiveProperty<_>

--------------------
new : init:'a -> ReactiveProperty<'a>
new : source:IObservable<'a> * init:'a -> ReactiveProperty<'a>
Multiple items
val int : value:'T -> int (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.int

--------------------
type int = int32

Full name: Microsoft.FSharp.Core.int

--------------------
type int<'Measure> = int

Full name: Microsoft.FSharp.Core.int<_>
Multiple items
type MainForm =
  inherit Form
  new : unit -> MainForm
  override OnLoad : e:EventArgs -> unit
  member ButtonDown : Button
  member ButtonUp : Button
  member Label : Label
  member TextBox : TextBox

Full name: Tutorial.MainForm

--------------------
new : unit -> MainForm
Multiple items
type Form =
  inherit ContainerControl
  new : unit -> Form
  member AcceptButton : IButtonControl with get, set
  member Activate : unit -> unit
  member ActiveMdiChild : Form
  member AddOwnedForm : ownedForm:Form -> unit
  member AllowTransparency : bool with get, set
  member AutoScale : bool with get, set
  member AutoScaleBaseSize : Size with get, set
  member AutoScroll : bool with get, set
  member AutoSize : bool with get, set
  ...
  nested type ControlCollection

Full name: System.Windows.Forms.Form

--------------------
Form() : unit
val p : TableLayoutPanel
Multiple items
type TableLayoutPanel =
  inherit Panel
  new : unit -> TableLayoutPanel
  member BorderStyle : BorderStyle with get, set
  member CellBorderStyle : TableLayoutPanelCellBorderStyle with get, set
  member ColumnCount : int with get, set
  member ColumnStyles : TableLayoutColumnStyleCollection
  member Controls : TableLayoutControlCollection
  member GetCellPosition : control:Control -> TableLayoutPanelCellPosition
  member GetColumn : control:Control -> int
  member GetColumnSpan : control:Control -> int
  member GetColumnWidths : unit -> int[]
  ...

Full name: System.Windows.Forms.TableLayoutPanel

--------------------
TableLayoutPanel() : unit
val addAt : (Control -> int -> int -> unit)
val c : Control
val i : int
val j : int
property TableLayoutPanel.Controls: TableLayoutControlCollection
Control.ControlCollection.Add(value: Control) : unit
TableLayoutControlCollection.Add(control: Control, column: int, row: int) : unit
TableLayoutPanel.SetCellPosition(control: Control, position: TableLayoutPanelCellPosition) : unit
Multiple items
type TableLayoutPanelCellPosition =
  struct
    new : column:int * row:int -> TableLayoutPanelCellPosition
    member Column : int with get, set
    member Equals : other:obj -> bool
    member GetHashCode : unit -> int
    member Row : int with get, set
    member ToString : unit -> string
  end

Full name: System.Windows.Forms.TableLayoutPanelCellPosition

--------------------
TableLayoutPanelCellPosition()
TableLayoutPanelCellPosition(column: int, row: int) : unit
Multiple items
type Button =
  inherit ButtonBase
  new : unit -> Button
  member AutoSizeMode : AutoSizeMode with get, set
  member DialogResult : DialogResult with get, set
  member NotifyDefault : value:bool -> unit
  member PerformClick : unit -> unit
  member ToString : unit -> string
  event DoubleClick : EventHandler
  event MouseDoubleClick : MouseEventHandler

Full name: System.Windows.Forms.Button

--------------------
Button() : unit
namespace System.Text
Multiple items
type TextBox =
  inherit TextBoxBase
  new : unit -> TextBox
  member AcceptsReturn : bool with get, set
  member AutoCompleteCustomSource : AutoCompleteStringCollection with get, set
  member AutoCompleteMode : AutoCompleteMode with get, set
  member AutoCompleteSource : AutoCompleteSource with get, set
  member CharacterCasing : CharacterCasing with get, set
  member Multiline : bool with get, set
  member PasswordChar : char with get, set
  member Paste : text:string -> unit
  member ScrollBars : ScrollBars with get, set
  ...

Full name: System.Windows.Forms.TextBox

--------------------
TextBox() : unit
Multiple items
type Label =
  inherit Control
  new : unit -> Label
  member AutoEllipsis : bool with get, set
  member AutoSize : bool with get, set
  member BackgroundImage : Image with get, set
  member BackgroundImageLayout : ImageLayout with get, set
  member BorderStyle : BorderStyle with get, set
  member FlatStyle : FlatStyle with get, set
  member GetPreferredSize : proposedSize:Size -> Size
  member Image : Image with get, set
  member ImageAlign : ContentAlignment with get, set
  ...

Full name: System.Windows.Forms.Label

--------------------
Label() : unit
val x : MainForm
override MainForm.OnLoad : e:EventArgs -> unit

Full name: Tutorial.MainForm.OnLoad
val e : EventArgs
property Control.Controls: Control.ControlCollection
Control.ControlCollection.Add(value: Control) : unit
property MainForm.ButtonUp: Button
property MainForm.ButtonDown: Button
property MainForm.TextBox: TextBox
property MainForm.Label: Label
Multiple items
type MainView =
  inherit View<MainEvents,Form,MainModel>
  new : mw:MainForm * m:MainModel -> MainView
  override SetBindings : m:MainModel -> unit
  override EventStreams : IObservable<MainEvents> list

Full name: Tutorial.MainView

--------------------
new : mw:MainForm * m:MainModel -> MainView
val mw : MainForm
val m : MainModel
Multiple items
type View =
  | LargeIcon = 0
  | Details = 1
  | SmallIcon = 2
  | List = 3
  | Tile = 4

Full name: System.Windows.Forms.View

--------------------
type View<'Event,'Element,'Model> =
  inherit IViewWithModel<'Event,'Model>
  new : elt:'Element * m:'Model -> View<'Event,'Element,'Model>
  member Root : 'Element

Full name: FSharp.Qualia.View<_,_,_>

--------------------
new : elt:'Element * m:'Model -> View<'Event,'Element,'Model>
val this : MainView
override MainView.EventStreams : IObservable<MainEvents> list

Full name: Tutorial.MainView.EventStreams
event Control.Click: IEvent<EventHandler,EventArgs>
Multiple items
module Observable

from FSharp.Qualia

--------------------
module Observable

from Microsoft.FSharp.Control
val mapTo : value:'a -> (IObservable<'b> -> IObservable<'a>)

Full name: FSharp.Qualia.Observable.mapTo
event Control.TextChanged: IEvent<EventHandler,EventArgs>
val map : mapping:('T -> 'U) -> source:IObservable<'T> -> IObservable<'U>

Full name: Microsoft.FSharp.Control.Observable.map
property TextBox.Text: string
override MainView.SetBindings : m:MainModel -> unit

Full name: Tutorial.MainView.SetBindings
property MainModel.Value: ReactiveProperty<int>
member IObservable.Add : callback:('T -> unit) -> unit
val v : int
property Label.Text: string
Multiple items
type MainDispatcher =
  interface IDispatcher<MainEvents,MainModel>
  new : unit -> MainDispatcher

Full name: Tutorial.MainDispatcher

--------------------
new : unit -> MainDispatcher
val up : (MainModel -> unit)
property ReactiveProperty.Value: int
val down : (MainModel -> unit)
val edit : (string -> MainModel -> unit)
val str : string
type Int32 =
  struct
    member CompareTo : value:obj -> int + 1 overload
    member Equals : obj:obj -> bool + 1 overload
    member GetHashCode : unit -> int
    member GetTypeCode : unit -> TypeCode
    member ToString : unit -> string + 3 overloads
    static val MaxValue : int
    static val MinValue : int
    static member Parse : s:string -> int + 3 overloads
    static member TryParse : s:string * result:int -> bool + 1 overload
  end

Full name: System.Int32
Int32.TryParse(s: string, result: byref<int>) : bool
Int32.TryParse(s: string, style: Globalization.NumberStyles, provider: IFormatProvider, result: byref<int>) : bool
type IDispatcher<'Event,'Model> =
  interface
    abstract member InitModel : 'Model -> unit
    abstract member Dispatcher : ('Event -> EventHandler<'Model>)
  end

Full name: FSharp.Qualia.IDispatcher<_,_>
val this : MainDispatcher
override MainDispatcher.InitModel : MainModel -> unit

Full name: Tutorial.MainDispatcher.InitModel
override MainDispatcher.Dispatcher : (MainEvents -> EventHandler<MainModel>)

Full name: Tutorial.MainDispatcher.Dispatcher
union case EventHandler.Sync: ('Model -> unit) -> EventHandler<'Model>
Multiple items
type STAThreadAttribute =
  inherit Attribute
  new : unit -> STAThreadAttribute

Full name: System.STAThreadAttribute

--------------------
STAThreadAttribute() : unit
Multiple items
type EntryPointAttribute =
  inherit Attribute
  new : unit -> EntryPointAttribute

Full name: Microsoft.FSharp.Core.EntryPointAttribute

--------------------
new : unit -> EntryPointAttribute
val main : argv:string [] -> int

Full name: Tutorial.main
val argv : string []
val v : MainView
val mvc : EventLoop<MainModel,MainEvents,Form>
Multiple items
type EventLoop<'Model,'Event,'Element> =
  new : v:View<'Event,'Element,'Model> * c:IDispatcher<'Event,'Model> -> EventLoop<'Model,'Event,'Element>
  member Inject : e:'Event -> unit
  member Start : unit -> IDisposable
  member StartWithScheduler : f:((unit -> unit) -> unit) -> IDisposable

Full name: FSharp.Qualia.EventLoop<_,_,_>

--------------------
new : v:View<'Event,'Element,'Model> * c:IDispatcher<'Event,'Model> -> EventLoop<'Model,'Event,'Element>
val eventloop : IDisposable
member EventLoop.Start : unit -> IDisposable
property View.Root: Form
Form.ShowDialog() : DialogResult
Form.ShowDialog(owner: IWin32Window) : DialogResult
val ignore : value:'T -> unit

Full name: Microsoft.FSharp.Core.Operators.ignore
type MainWindow = obj

Full name: Tutorial.MainWindow
Multiple items
type MainWPFView =
  inherit obj
  new : mw:MainWindow * m:'a -> MainWPFView
  override SetBindings : m:MainModel -> 'a
  override EventStreams : 'a

Full name: Tutorial.MainWPFView

--------------------
new : mw:MainWindow * m:'a -> MainWPFView
val mw : MainWindow
val m : 'a
override MainWPFView.EventStreams : 'a

Full name: Tutorial.MainWPFView.EventStreams
override MainWPFView.SetBindings : m:MainModel -> 'a

Full name: Tutorial.MainWPFView.SetBindings
union case CollectionChanged.Add: seq<'a> -> CollectionChanged<'a>
val app : obj
type Application =
  static member AddMessageFilter : value:IMessageFilter -> unit
  static member AllowQuit : bool
  static member CommonAppDataPath : string
  static member CommonAppDataRegistry : RegistryKey
  static member CompanyName : string
  static member CurrentCulture : CultureInfo with get, set
  static member CurrentInputLanguage : InputLanguage with get, set
  static member DoEvents : unit -> unit
  static member EnableVisualStyles : unit -> unit
  static member ExecutablePath : string
  ...
  nested type MessageLoopCallback

Full name: System.Windows.Forms.Application
val v : View<MainEvents,obj,MainModel>
val mvc : EventLoop<MainModel,MainEvents,obj>
property View.Root: obj
Fork me on GitHub