FSharp.Desktop.UI


Basics

Numeric Up/Down control is simplified version of IntegerUpDown control from Extended WPF Toolkit. It provides a TextBox with button spinners that allow incrementing and decrementing int values by using the spinner buttons, keyboard up/down arrows, or mouse wheel.

To keep it simple we will build application rather than reusable control.

Let's go step-by-step through development process.

Model

Our model has single property Value bounded to input text box. The library provides quick canonical way to define custom models. Make sure to inherit model type from FSharp.Desktop.UI.Model and declare as abstract read/write properties you want to data-bind. Once that done INotifyPropertyChanged and INotifyDataErrorInfo will be auto-wired.

1: 
2: 
3: 
4: 
5: 
[<AbstractClass>]
type NumericUpDownModel() = 
    inherit Model()

    abstract Value: int with get, set

There are alternative methods to define custom models.

Events + View

Because view is essentially single event stream the best way to define variety of event is to use discriminated unions. It seems obvious that we need two events: Up and Down. Low level events like MouseMove or KeyUp are mapped into high-level ones. Although it is not too hard to provide implementation of IView<'Event, 'Model> interface, the library provides base helper classes like View and XamlView.

Traditional way to design actual WPF window is to use XAML designer. For simplicity in this particular application we will build it in a code. It also proves the point that WPF details abstracted so well that the library is agnostic to actual way WPF primitives assembled.

In addition to event sourcing View also responsible for setting up proper data bindings. The library introduce type-safe data-binding. The idea is map F# assignment statement quotation to data binding setup.
Other examples will expand on topic on type safe data binding.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 
34: 
35: 
36: 
37: 
38: 
39: 
40: 
41: 
42: 
43: 
44: 
45: 
46: 
47: 
48: 
49: 
50: 
51: 
52: 
53: 
54: 
55: 
56: 
type NumericUpDownEvents = Up | Down

type NumericUpDownView() as this = 
    inherit View<NumericUpDownEvents, NumericUpDownModel, Window>(Window())
    
    //Assembling WPF window in code. 
    do 
        this.Root.Width <- 250.
        this.Root.Height <- 80.
        this.Root.WindowStartupLocation <- WindowStartupLocation.CenterScreen
        this.Root.Title <- "Up/Down"

    let mainPanel = 
        let grid = Grid()
        [ RowDefinition(); RowDefinition() ] |> List.iter grid.RowDefinitions.Add
        [ ColumnDefinition(); ColumnDefinition(Width = GridLength.Auto) ] |> List.iter grid.ColumnDefinitions.Add
        grid

    let upButton = Button(Content = "^", Width = 50.)
    let downButton = Button(Content = "v", Width = 50.)
    let input = TextBox(TextAlignment = TextAlignment.Center, FontSize = 20., VerticalContentAlignment = VerticalAlignment.Center)
    
    do  
        upButton.SetValue(Grid.ColumnProperty, 1)
        downButton.SetValue(Grid.ColumnProperty, 1)
        downButton.SetValue(Grid.RowProperty, 1)

        input.SetValue(Grid.RowSpanProperty, 2)

        mainPanel.Children.Add upButton |> ignore
        mainPanel.Children.Add downButton |> ignore
        mainPanel.Children.Add input |> ignore

        this.Root.Content <- mainPanel

    //View implementation 
    override this.EventStreams = [
        upButton.Click |> Observable.map (fun _ -> Up)
        downButton.Click |> Observable.map (fun _ -> Down)

        input.KeyUp |> Observable.choose (fun args -> 
            match args.Key with 
            | Key.Up -> Some Up  
            | Key.Down -> Some Down
            | _ ->  None
        )

        input.MouseWheel |> Observable.map (fun args -> if args.Delta > 0 then Up else Down)
    ]

    override this.SetBindings model =   
        Binding.OfExpression 
            <@
                input.Text <- coerce model.Value 
                //'coerce' means "use WPF default conversions"
            @>

Controller

For this introductory example think of controller as callback that takes event and model. It processes event and apply changes to the model. Model state changes propagate back to view via data binding.

1: 
2: 
3: 
4: 
5: 
6: 
let eventHandler event (model: NumericUpDownModel) =
    match event with
    | Up -> model.Value <- model.Value + 1
    | Down -> model.Value <- model.Value - 1

let controller = Controller.Create eventHandler

Controllers in real-world application are more complex. But Controller.Create method exists in the library and can be used as a shortcut to build simple controllers.

Pattern matching in event inside controller callback is very interesting because it represents compiler checked event handlers map. To prove the point comment out one of the case, for example Down. You will immediately see a warning from compiler "Incomplete pattern matches on this expression. For example, the value 'Down' may indicate a case not covered by the pattern(s)."

Application

Application boot-strap code is trivial. Worth noting that model has be created via Create factory method.

1: 
2: 
3: 
4: 
5: 
6: 
7: 
[<STAThread>]
do
    let model = NumericUpDownModel.Create()
    let view = NumericUpDownView()
    let mvc = Mvc(model, view, controller)
    use eventLoop = mvc.Start()
    Application().Run( window = view.Root) |> ignore

Where Are We?

It is quite remarkable what we were able to archive with such small amount of code. Try to implement exactly same functionality using classic MVVM or one of the mvvm-style frameworks. Compare and make you own judgment. Next example is Calculator application.

namespace System
namespace System.Windows
namespace System.Windows.Controls
namespace System.Windows.Data
namespace System.Windows.Input
Multiple items
namespace FSharp

--------------------
namespace Microsoft.FSharp
namespace FSharp.Desktop
namespace FSharp.Desktop.UI
Multiple items
type AbstractClassAttribute =
  inherit Attribute
  new : unit -> AbstractClassAttribute

Full name: Microsoft.FSharp.Core.AbstractClassAttribute

--------------------
new : unit -> AbstractClassAttribute
type NumericUpDownEvents =
  | Up
  | Down

Full name: Tutorial_numeric_up_down.NumericUpDownEvents
union case NumericUpDownEvents.Up: NumericUpDownEvents
union case NumericUpDownEvents.Down: NumericUpDownEvents
Multiple items
type NumericUpDownView =
  inherit View<NumericUpDownEvents,NumericUpDownModel,Window>
  new : unit -> NumericUpDownView
  override SetBindings : model:NumericUpDownModel -> unit
  override EventStreams : IObservable<NumericUpDownEvents> list

Full name: Tutorial_numeric_up_down.NumericUpDownView

--------------------
new : unit -> NumericUpDownView
val this : NumericUpDownView
Multiple items
type View<'Event,'Model,'Window (requires 'Window :> Window)> =
  inherit PartialView<'Event,'Model,'Window>
  interface IView<'Event,'Model>
  new : window:'Window -> View<'Event,'Model,'Window>

Full name: FSharp.Desktop.UI.View<_,_,_>

--------------------
new : window:'Window -> View<'Event,'Model,'Window>
Multiple items
type NumericUpDownModel =
  inherit Model
  new : unit -> NumericUpDownModel
  abstract member Value : int
  abstract member Value : int with set

Full name: Tutorial_numeric_up_down.NumericUpDownModel

--------------------
new : unit -> NumericUpDownModel
Multiple items
type Window =
  inherit ContentControl
  new : unit -> Window
  member Activate : unit -> bool
  member AllowsTransparency : bool with get, set
  member Close : unit -> unit
  member DialogResult : Nullable<bool> with get, set
  member DragMove : unit -> unit
  member Hide : unit -> unit
  member Icon : ImageSource with get, set
  member IsActive : bool
  member Left : float with get, set
  ...

Full name: System.Windows.Window

--------------------
Window() : unit
type WindowStartupLocation =
  | Manual = 0
  | CenterScreen = 1
  | CenterOwner = 2

Full name: System.Windows.WindowStartupLocation
field WindowStartupLocation.CenterScreen = 1
val mainPanel : Grid
val grid : Grid
Multiple items
type Grid =
  inherit Panel
  new : unit -> Grid
  member ColumnDefinitions : ColumnDefinitionCollection
  member RowDefinitions : RowDefinitionCollection
  member ShouldSerializeColumnDefinitions : unit -> bool
  member ShouldSerializeRowDefinitions : unit -> bool
  member ShowGridLines : bool with get, set
  static val ShowGridLinesProperty : DependencyProperty
  static val ColumnProperty : DependencyProperty
  static val RowProperty : DependencyProperty
  static val ColumnSpanProperty : DependencyProperty
  ...

Full name: System.Windows.Controls.Grid

--------------------
Grid() : unit
Multiple items
type RowDefinition =
  inherit DefinitionBase
  new : unit -> RowDefinition
  member ActualHeight : float
  member Height : GridLength with get, set
  member MaxHeight : float with get, set
  member MinHeight : float with get, set
  member Offset : float
  static val HeightProperty : DependencyProperty
  static val MinHeightProperty : DependencyProperty
  static val MaxHeightProperty : DependencyProperty

Full name: System.Windows.Controls.RowDefinition

--------------------
RowDefinition() : unit
Multiple items
module List

from Microsoft.FSharp.Collections

--------------------
type List<'T> =
  | ( [] )
  | ( :: ) of Head: 'T * Tail: 'T list
  interface IEnumerable
  interface IEnumerable<'T>
  member GetSlice : startIndex:int option * endIndex:int option -> 'T list
  member Head : 'T
  member IsEmpty : bool
  member Item : index:int -> 'T with get
  member Length : int
  member Tail : 'T list
  static member Cons : head:'T * tail:'T list -> 'T list
  static member Empty : 'T list

Full name: Microsoft.FSharp.Collections.List<_>
val iter : action:('T -> unit) -> list:'T list -> unit

Full name: Microsoft.FSharp.Collections.List.iter
property Grid.RowDefinitions: RowDefinitionCollection
RowDefinitionCollection.Add(value: RowDefinition) : unit
Multiple items
type ColumnDefinition =
  inherit DefinitionBase
  new : unit -> ColumnDefinition
  member ActualWidth : float
  member MaxWidth : float with get, set
  member MinWidth : float with get, set
  member Offset : float
  member Width : GridLength with get, set
  static val WidthProperty : DependencyProperty
  static val MinWidthProperty : DependencyProperty
  static val MaxWidthProperty : DependencyProperty

Full name: System.Windows.Controls.ColumnDefinition

--------------------
ColumnDefinition() : unit
Multiple items
type GridLength =
  struct
    new : pixels:float -> GridLength + 1 overload
    member Equals : oCompare:obj -> bool + 1 overload
    member GetHashCode : unit -> int
    member GridUnitType : GridUnitType
    member IsAbsolute : bool
    member IsAuto : bool
    member IsStar : bool
    member ToString : unit -> string
    member Value : float
    static member Auto : GridLength
  end

Full name: System.Windows.GridLength

--------------------
GridLength()
GridLength(pixels: float) : unit
GridLength(value: float, type: GridUnitType) : unit
property GridLength.Auto: GridLength
property Grid.ColumnDefinitions: ColumnDefinitionCollection
ColumnDefinitionCollection.Add(value: ColumnDefinition) : unit
val upButton : Button
Multiple items
type Button =
  inherit ButtonBase
  new : unit -> Button
  member IsCancel : bool with get, set
  member IsDefault : bool with get, set
  member IsDefaulted : bool
  static val IsDefaultProperty : DependencyProperty
  static val IsCancelProperty : DependencyProperty
  static val IsDefaultedProperty : DependencyProperty

Full name: System.Windows.Controls.Button

--------------------
Button() : unit
val downButton : Button
val input : TextBox
Multiple items
type TextBox =
  inherit TextBoxBase
  new : unit -> TextBox
  member CaretIndex : int with get, set
  member CharacterCasing : CharacterCasing with get, set
  member Clear : unit -> unit
  member GetCharacterIndexFromLineIndex : lineIndex:int -> int
  member GetCharacterIndexFromPoint : point:Point * snapToText:bool -> int
  member GetFirstVisibleLineIndex : unit -> int
  member GetLastVisibleLineIndex : unit -> int
  member GetLineIndexFromCharacterIndex : charIndex:int -> int
  member GetLineLength : lineIndex:int -> int
  ...

Full name: System.Windows.Controls.TextBox

--------------------
TextBox() : unit
type TextAlignment =
  | Left = 0
  | Right = 1
  | Center = 2
  | Justify = 3

Full name: System.Windows.TextAlignment
field TextAlignment.Center = 2
type VerticalAlignment =
  | Top = 0
  | Center = 1
  | Bottom = 2
  | Stretch = 3

Full name: System.Windows.VerticalAlignment
field VerticalAlignment.Center = 1
DependencyObject.SetValue(key: DependencyPropertyKey, value: obj) : unit
DependencyObject.SetValue(dp: DependencyProperty, value: obj) : unit
field Grid.ColumnProperty
field Grid.RowProperty
field Grid.RowSpanProperty
property Panel.Children: UIElementCollection
UIElementCollection.Add(element: UIElement) : int
val ignore : value:'T -> unit

Full name: Microsoft.FSharp.Core.Operators.ignore
property PartialView.Root: Window
property ContentControl.Content: obj
override NumericUpDownView.EventStreams : IObservable<NumericUpDownEvents> list

Full name: Tutorial_numeric_up_down.NumericUpDownView.EventStreams
event Primitives.ButtonBase.Click: IEvent<RoutedEventHandler,RoutedEventArgs>
Multiple items
module Observable

from FSharp.Desktop.UI

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

from Microsoft.FSharp.Control
val map : mapping:('T -> 'U) -> source:IObservable<'T> -> IObservable<'U>

Full name: Microsoft.FSharp.Control.Observable.map
event UIElement.KeyUp: IEvent<KeyEventHandler,KeyEventArgs>
val choose : chooser:('T -> 'U option) -> source:IObservable<'T> -> IObservable<'U>

Full name: Microsoft.FSharp.Control.Observable.choose
val args : KeyEventArgs
property KeyEventArgs.Key: Key
type Key =
  | None = 0
  | Cancel = 1
  | Back = 2
  | Tab = 3
  | LineFeed = 4
  | Clear = 5
  | Return = 6
  | Enter = 6
  | Pause = 7
  | Capital = 8
  ...

Full name: System.Windows.Input.Key
field Key.Up = 24
union case Option.Some: Value: 'T -> Option<'T>
field Key.Down = 26
union case Option.None: Option<'T>
event UIElement.MouseWheel: IEvent<MouseWheelEventHandler,MouseWheelEventArgs>
val args : MouseWheelEventArgs
property MouseWheelEventArgs.Delta: int
override NumericUpDownView.SetBindings : model:NumericUpDownModel -> unit

Full name: Tutorial_numeric_up_down.NumericUpDownView.SetBindings
val model : NumericUpDownModel
Multiple items
type Binding =
  inherit BindingBase
  new : unit -> Binding + 1 overload
  member AsyncState : obj with get, set
  member BindsDirectlyToSource : bool with get, set
  member Converter : IValueConverter with get, set
  member ConverterCulture : CultureInfo with get, set
  member ConverterParameter : obj with get, set
  member ElementName : string with get, set
  member IsAsync : bool with get, set
  member Mode : BindingMode with get, set
  member NotifyOnSourceUpdated : bool with get, set
  ...

Full name: System.Windows.Data.Binding

--------------------
Binding() : unit
Binding(path: string) : unit
static member Binding.OfExpression : expr:Quotations.Expr * ?mode:BindingMode * ?updateSourceTrigger:UpdateSourceTrigger * ?fallbackValue:'a0 * ?targetNullValue:'a1 * ?validatesOnDataErrors:bool * ?validatesOnExceptions:bool -> unit
property TextBox.Text: string
val coerce : 'a -> 'b

Full name: FSharp.Desktop.UI.Binding.coerce
val eventHandler : event:NumericUpDownEvents -> model:NumericUpDownModel -> 'a

Full name: Tutorial_numeric_up_down.eventHandler
val event : NumericUpDownEvents
val controller : IController<NumericUpDownEvents,NumericUpDownModel>

Full name: Tutorial_numeric_up_down.controller
Multiple items
type Controller<'Event,'Model> =
  interface IController<'Event,'Model>
  new : unit -> Controller<'Event,'Model>
  abstract member InitModel : 'Model -> unit
  abstract member Dispatcher : ('Event -> EventHandler<'Model>)
  static member Create : callback:('Event -> 'Model -> unit) -> IController<'Event,'Model>
  static member Create : callback:('Event -> EventHandler<'Model>) -> IController<'Event,'Model>

Full name: FSharp.Desktop.UI.Controller<_,_>

--------------------
new : unit -> Controller<'Event,'Model>
static member Controller.Create : callback:('Event -> 'Model -> unit) -> IController<'Event,'Model>
static member Controller.Create : callback:('Event -> EventHandler<'Model>) -> IController<'Event,'Model>
Multiple items
type STAThreadAttribute =
  inherit Attribute
  new : unit -> STAThreadAttribute

Full name: System.STAThreadAttribute

--------------------
STAThreadAttribute() : unit
val view : NumericUpDownView
val mvc : obj
Multiple items
module Mvc

from FSharp.Desktop.UI

--------------------
type Mvc<'Event,'Model (requires 'Model :> INotifyPropertyChanged)> =
  new : model:'Model * view:IView<'Event,'Model> * controller:IController<'Event,'Model> -> Mvc<'Event,'Model>
  member Compose : childController:IController<'a2,'Model> * events:IObservable<'a2> -> Mvc<Choice<'Event,'a2>,'Model>
  member Compose : childController:IController<'EX,'MX> * childView:IPartialView<'EX,'MX> * childModelSelector:('Model -> 'MX) -> Mvc<Choice<'Event,'EX>,'Model>
  member Start : unit -> IDisposable
  member StartDialog : unit -> bool
  member StartWindow : unit -> Async<unit>
  member Error : (exn * 'Event -> unit)
  member Error : (exn * 'Event -> unit) with set
  static member ( <+> ) : mvc:Mvc<'a2,'a3> * (#IController<'a5,'a6> * #IPartialView<'a5,'a6> * ('a3 -> 'a6)) -> Mvc<Choice<'a2,'a5>,'a3> (requires 'a3 :> INotifyPropertyChanged)

Full name: FSharp.Desktop.UI.Mvc<_,_>

--------------------
new : model:'Model * view:IView<'Event,'Model> * controller:IController<'Event,'Model> -> Mvc<'Event,'Model>
val eventLoop : IDisposable
Multiple items
type Application =
  inherit DispatcherObject
  new : unit -> Application
  member FindResource : resourceKey:obj -> obj
  member MainWindow : Window with get, set
  member Properties : IDictionary
  member Resources : ResourceDictionary with get, set
  member Run : unit -> int + 1 overload
  member Shutdown : unit -> unit + 1 overload
  member ShutdownMode : ShutdownMode with get, set
  member StartupUri : Uri with get, set
  member TryFindResource : resourceKey:obj -> obj
  ...

Full name: System.Windows.Application

--------------------
Application() : unit
Fork me on GitHub