F# Implementation of The Elm Architecture - Part 2
01 Jul 2016This is the second part of a prototype of The Elm Architecture in F#. The first post covered the logical UI and this covers using it with WPF and Xamarin.
Native UI Implementation
Below is the WPF implementation of the INativeUI
interface.
The Xamarin implementation is very similar.
Threading proves to be simple with all the updates being performed on the UI thread in a single call.
UIUpdate
maps well to the operations required to locate and update the native UI elements.
module WPF =
let CreateNaiveUI (root:ContentControl) =
let rec createUI ui : UIElement =
match ui with
|Text text ->
let c = Label(Content=string text)
upcast c
|Input (text,event) ->
let c = TextBox(Text=string text)
let event = !event
c.TextChanged.Add(fun _ -> let t = c.Text
async { !event t } |> Async.Start)
upcast c
|Button (text,event) ->
let c = Button(Content=string text)
let event = !event
c.Click.Add(fun _ -> async { (!event)() } |> Async.Start)
upcast c
|Div (layout,list) ->
let children = List.map createUI list
let c = StackPanel(Orientation=
match layout with
|Vertical->Orientation.Vertical
|Horizontal->Orientation.Horizontal)
List.iter (c.Children.Add>>ignore) children
upcast c
let rec locatePanel loc : Panel =
match loc with
|[] -> root.Content :?> _
|i::xs -> (locatePanel xs).Children.Item i :?> _
let uiUpdate u =
match u with
| InsertUI (loc,ui) ->
match loc with
|[] -> root.Content <- createUI ui
|i::xs -> (locatePanel xs).Children.Insert(i,createUI ui)
| UpdateUI (loc,ui) ->
let element = match loc with
|[] -> root.Content :?> _
|i::xs -> (locatePanel xs).Children.Item i
match ui with
| Text text -> (element :?> Label).Content <- string text
| Input (text,_) -> (element :?> TextBox).Text <- string text
| Button (text,_) -> (element :?> Button).Content <- string text
| Div _ -> ()
| ReplaceUI (loc,ui) ->
match loc with
|[] -> root.Content <- createUI ui
|i::xs ->
let c = (locatePanel xs).Children
c.RemoveAt i
c.Insert(i,createUI ui)
| RemoveUI loc ->
match loc with
|[] -> ()
|i::xs -> (locatePanel xs).Children.RemoveAt i
| EventUI _ -> ()
{ new INativeUI with
member __.Send list =
root.Dispatcher.Invoke (fun () -> List.iter uiUpdate list)
}
Results
The same CounterList
UI application from the previous post has been used across a number of native UIs.
The example source produces the following mobile and desktop UIs.
Conclusion
The Elm Architecture continues to look to be a very promising pattern.
The INativeUI
implementation is a single place for native UI element creation and is much more DRY than other UI models.
So far, styling has not been considered, but this single place should make it easier with both an Elm and a CSS model being possible.
The Elm Architecture moves the view and event logic away from the native UI. This has the benefit of making the UI more testable and at the same time making migration of the native UI easier.
These benefits, combined with the type safety and composability outlined in the previous post, make this pattern compelling.
UPDATED:
module List
from Microsoft.FSharp.Collections
--------------------
type List<'T> =
| ( [] )
| ( :: ) of Head: 'T * Tail: 'T list
interface IReadOnlyList<'T>
interface IReadOnlyCollection<'T>
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
...
val list : 'a list
--------------------
type 'T list = List<'T>
module Event
from Microsoft.FSharp.Control
--------------------
type 'msg Event = ('msg -> unit) ref ref
Message event used on the primative UI components.
--------------------
type Event<'Delegate,'Args (requires delegate and 'Delegate :> Delegate)> =
new : unit -> Event<'Delegate,'Args>
member Trigger : sender:obj * args:'Args -> unit
member Publish : IEvent<'Delegate,'Args>
--------------------
new : unit -> Event<'Delegate,'Args>
val ref : value:'T -> 'T ref
--------------------
type 'T ref = Ref<'T>
| Horizontal
| Vertical
Layout for a section of UI components.
| Text of string
| Input of string * string Event
| Button of string * unit Event
| Div of Layout * UI list
Primative UI components.
val string : value:'T -> string
--------------------
type string = System.String
| InsertUI of int list * UI
| UpdateUI of int list * UI
| ReplaceUI of int list * UI
| RemoveUI of int list
| EventUI of (unit -> unit)
UI component update and event redirection.
val int : value:'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
type UI =
| Text of string
| Input of string * string Event
| Button of string * unit Event
| Div of Layout * UI list
Primative UI components.
--------------------
type 'msg UI =
{UI: UI;
mutable Event: 'msg -> unit;}
UI component including a message event.
UI.UI: UI
--------------------
type UI =
| Text of string
| Input of string * string Event
| Button of string * unit Event
| Div of Layout * UI list
Primative UI components.
--------------------
type 'msg UI =
{UI: UI;
mutable Event: 'msg -> unit;}
UI component including a message event.
UI.Event: 'msg -> unit
--------------------
module Event
from Microsoft.FSharp.Control
--------------------
type 'msg Event = ('msg -> unit) ref ref
Message event used on the primative UI components.
--------------------
type Event<'Delegate,'Args (requires delegate and 'Delegate :> Delegate)> =
new : unit -> Event<'Delegate,'Args>
member Trigger : sender:obj * args:'Args -> unit
member Publish : IEvent<'Delegate,'Args>
--------------------
new : unit -> Event<'Delegate,'Args>
{Model: 'model;
Update: 'msg -> 'model -> 'model;
View: 'model -> 'msg UI;}
UI application.
interface
abstract member Send : UIUpdate list -> unit
end
Native UI interface.
type CompilationRepresentationAttribute =
inherit Attribute
new : flags:CompilationRepresentationFlags -> CompilationRepresentationAttribute
member Flags : CompilationRepresentationFlags
--------------------
new : flags:CompilationRepresentationFlags -> CompilationRepresentationAttribute
| None = 0
| Static = 1
| Instance = 2
| ModuleSuffix = 4
| UseNullAsTrueValue = 8
| Event = 16
Memoize view generation from model object references.
type ConditionalWeakTable<'TKey,'TValue (requires reference type and reference type)> =
new : unit -> ConditionalWeakTable<'TKey, 'TValue>
member Add : key:'TKey * value:'TValue -> unit
member GetOrCreateValue : key:'TKey -> 'TValue
member GetValue : key:'TKey * createValueCallback:CreateValueCallback<'TKey, 'TValue> -> 'TValue
member Remove : key:'TKey -> bool
member TryGetValue : key:'TKey * value:'TValue -> bool
nested type CreateValueCallback
--------------------
System.Runtime.CompilerServices.ConditionalWeakTable() : System.Runtime.CompilerServices.ConditionalWeakTable<'TKey,'TValue>
Returns a Text display UI component.
Returns a text Input UI component.
Returns a Button UI component.
Returns a section of UI components given a layout.
The name div comes from HTML and represents a division (or section) of UI components.
val list : 'a UI list
--------------------
type 'T list = List<'T>
module List
from Main
--------------------
module List
from Microsoft.FSharp.Collections
--------------------
type List<'T> =
| ( [] )
| ( :: ) of Head: 'T * Tail: 'T list
interface IReadOnlyList<'T>
interface IReadOnlyCollection<'T>
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
...
Returns a new UI component mapping the message event using the given function.
Returns a list of UI updates from two UI components.
from Microsoft.FSharp.Core
Returns a UI application from a UI model, update and view.
Runs a UI application given a native UI.
| Increment
| Decrement
module UI
from Main
--------------------
type UI =
| Text of string
| Input of string * string Event
| Button of string * unit Event
| Div of Layout * UI list
Primative UI components.
--------------------
type 'msg UI =
{UI: UI;
mutable Event: 'msg -> unit;}
UI component including a message event.
| Reset
| Top of Msg
| Bottom of Msg
from Main
{Top: Model;
Bottom: Model;}
| Insert
| Remove
| Modify of int * Msg
{Counters: Model list;}
from Main
type ContentControl =
inherit Control
new : unit -> ContentControl
member Content : obj with get, set
member ContentStringFormat : string with get, set
member ContentTemplate : DataTemplate with get, set
member ContentTemplateSelector : DataTemplateSelector with get, set
member HasContent : bool
member ShouldSerializeContent : unit -> bool
static val ContentProperty : DependencyProperty
static val HasContentProperty : DependencyProperty
static val ContentTemplateProperty : DependencyProperty
...
--------------------
ContentControl() : ContentControl
type UIElement =
inherit Visual
new : unit -> UIElement
member AddHandler : routedEvent:RoutedEvent * handler:Delegate -> unit + 1 overload
member AddToEventRoute : route:EventRoute * e:RoutedEventArgs -> unit
member AllowDrop : bool with get, set
member ApplyAnimationClock : dp:DependencyProperty * clock:AnimationClock -> unit + 1 overload
member AreAnyTouchesCaptured : bool
member AreAnyTouchesCapturedWithin : bool
member AreAnyTouchesDirectlyOver : bool
member AreAnyTouchesOver : bool
member Arrange : finalRect:Rect -> unit
...
--------------------
UIElement() : UIElement
type Label =
inherit ContentControl
new : unit -> Label
member Target : UIElement with get, set
static val TargetProperty : DependencyProperty
--------------------
Label() : Label
union case UI.Input: string * string Event -> UI
--------------------
namespace System.Windows.Input
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
...
--------------------
TextBox() : TextBox
type Async =
static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate)
static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
static member AwaitTask : task:Task -> Async<unit>
static member AwaitTask : task:Task<'T> -> Async<'T>
static member AwaitWaitHandle : waitHandle:WaitHandle * ?millisecondsTimeout:int -> Async<bool>
static member CancelDefaultToken : unit -> unit
static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>>
static member Choice : computations:seq<Async<'T option>> -> Async<'T option>
static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
...
--------------------
type Async<'T> =
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
--------------------
Button() : Button
val list : UI list
--------------------
type 'T list = List<'T>
type StackPanel =
inherit Panel
new : unit -> StackPanel
member CanHorizontallyScroll : bool with get, set
member CanVerticallyScroll : bool with get, set
member ExtentHeight : float
member ExtentWidth : float
member HorizontalOffset : float
member LineDown : unit -> unit
member LineLeft : unit -> unit
member LineRight : unit -> unit
member LineUp : unit -> unit
...
--------------------
StackPanel() : StackPanel
| Horizontal = 0
| Vertical = 1
inherit FrameworkElement
member Background : Brush with get, set
member Children : UIElementCollection
member HasLogicalOrientationPublic : bool
member IsItemsHost : bool with get, set
member LogicalOrientationPublic : Orientation
member ShouldSerializeChildren : unit -> bool
static val BackgroundProperty : DependencyProperty
static val IsItemsHostProperty : DependencyProperty
static val ZIndexProperty : DependencyProperty
static member GetZIndex : element:UIElement -> int
...
val list : UIUpdate list
--------------------
type 'T list = List<'T>
(+0 other overloads)
Threading.Dispatcher.Invoke(callback: System.Action) : unit
(+0 other overloads)
Threading.Dispatcher.Invoke(method: System.Delegate, [<System.ParamArray>] args: obj []) : obj
(+0 other overloads)
Threading.Dispatcher.Invoke(priority: Threading.DispatcherPriority, method: System.Delegate) : obj
(+0 other overloads)
Threading.Dispatcher.Invoke<'TResult>(callback: System.Func<'TResult>, priority: Threading.DispatcherPriority) : 'TResult
(+0 other overloads)
Threading.Dispatcher.Invoke(callback: System.Action, priority: Threading.DispatcherPriority) : unit
(+0 other overloads)
Threading.Dispatcher.Invoke(method: System.Delegate, timeout: System.TimeSpan, [<System.ParamArray>] args: obj []) : obj
(+0 other overloads)
Threading.Dispatcher.Invoke(method: System.Delegate, priority: Threading.DispatcherPriority, [<System.ParamArray>] args: obj []) : obj
(+0 other overloads)
Threading.Dispatcher.Invoke(priority: Threading.DispatcherPriority, timeout: System.TimeSpan, method: System.Delegate) : obj
(+0 other overloads)
Threading.Dispatcher.Invoke(priority: Threading.DispatcherPriority, method: System.Delegate, arg: obj) : obj
(+0 other overloads)