├── .gitignore
├── Elmish.Xamarin.Forms.fsproj
├── Extensions
└── Seq.fs
├── LICENSE
├── README.md
├── Xamarin
├── App.fs
├── Attributes.fs
├── Create.fs
├── Event.fs
├── Update.fs
└── VirtualDOM.fs
├── cmd.fs
└── program.fs
/.gitignore:
--------------------------------------------------------------------------------
1 | bin
2 | obj
3 |
--------------------------------------------------------------------------------
/Elmish.Xamarin.Forms.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netstandard2.0
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Extensions/Seq.fs:
--------------------------------------------------------------------------------
1 | module internal Elmish.Extensions.Seq
2 |
3 | let bindOption mapper sequence =
4 | seq {
5 | for v in sequence do
6 | match mapper v with
7 | | Some r -> yield r
8 | | None -> ()
9 | }
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2017 elmish-xamarin-forms contributors
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Elmish.Xamarin.Forms
2 | Elmish architecture for Xamarin.Forms
3 |
4 | ## WARNING
5 | This should very much be considered a draft and at the PoC stage. It is not ready for production yet. The DSL is likely to change substantially. Actual performance at scale is unknown.
6 |
7 | ## Obvious issues
8 | **No structural equality for events**. When an event handler is present in the tree, it is always considered changed, as there's no structural equality for functions. This means pretty much the whole tree is traversed on every update, even when not strictly necessary. There needs to be a way to determine whether an event handler has changed.
9 |
10 | **Hacky setting of events**. Setting an event on the Xanarin.Forms object is extremely hacky as there's no way for a client to clear the collection of subscribed listeners. Plus it is probably pretty slow, but is easily fixed with memoization.
11 |
12 | **DSL is clunky.** Specifying callbacks is ugly. Due to many views having properties with the same name, it is often necessary to qualify with the discriminated union's name.
13 |
14 | **Writing bindings is repetitive and tedious**. An obvious next step is auto-generating bindings from the Xamarin.Forms assembly. This would facilitate a better DSL and it would be much easier to keep up with Xamarin.Forms features.
--------------------------------------------------------------------------------
/Xamarin/App.fs:
--------------------------------------------------------------------------------
1 | module Elmish.Xamarin.Forms.Application
2 |
3 | open Elmish.Extensions
4 | open Elmish
5 | open Elmish.Xamarin.Forms.Update
6 |
7 |
8 | type internal App<'model, 'msg>(program: Program) as this =
9 | inherit Xamarin.Forms.Application()
10 |
11 | // Requires App to be instantiated on the main thread to work.
12 | // This is happily the case.
13 | let mainThread = System.Threading.Thread.CurrentThread
14 |
15 | let isOnMainThread () = System.Object.ReferenceEquals(mainThread, System.Threading.Thread.CurrentThread)
16 |
17 | let mutable optLastView = None
18 |
19 | let program' = { program with setState = this.setState program }
20 |
21 | let updateViews view =
22 | match optLastView with
23 | | Some lastView ->
24 | Update.page view lastView this.MainPage
25 | |> Option.iter (fun page -> this.MainPage <- page)
26 | | None ->
27 | this.MainPage <- Create.page view
28 | optLastView <- Some view
29 |
30 | let runOnMainThread action =
31 | if isOnMainThread () then
32 | action ()
33 | else
34 | Xamarin.Forms.Device.BeginInvokeOnMainThread(fun () -> action())
35 |
36 | do Program.run program'
37 |
38 | member this.setState (program: Program) model dispatch =
39 | let view = program.view model dispatch
40 |
41 | runOnMainThread (fun () -> updateViews view)
42 |
43 |
44 | let asApplication<'model, 'msg> (program: Program) =
45 | new App<'model, 'msg>(program) :> Xamarin.Forms.Application
46 |
47 |
48 |
--------------------------------------------------------------------------------
/Xamarin/Attributes.fs:
--------------------------------------------------------------------------------
1 | module Elmish.Xamarin.Forms.Attributes
2 |
3 | open VirtualDOM
4 |
5 | let loadImage name =
6 | Xamarin.Forms.ImageSource.FromFile name :?> Xamarin.Forms.FileImageSource
7 |
8 | let contentPage (view : Xamarin.Forms.ContentPage) = function
9 | | Icon name ->
10 | view.Icon <- loadImage name
11 | | Title title ->
12 | view.Title <- title
13 |
14 | let masterDetailPage (view : Xamarin.Forms.MasterDetailPage) = function
15 | | IsGestureEnabled isGestureEnabled ->
16 | view.IsGestureEnabled <- isGestureEnabled
17 | | IsPresented isPresented ->
18 | view.IsPresented <- isPresented
19 | | MasterBehavior masterBehavior ->
20 | view.MasterBehavior <- masterBehavior
21 | | OnIsPresentedChanged (BoolEvent.Event boolEvent) ->
22 | Event.clear "IsPresentedChanged" view
23 | view.IsPresentedChanged.Add (fun _ -> boolEvent view.IsPresented)
24 |
25 | let stackLayout (view : Xamarin.Forms.StackLayout) = function
26 | | Orientation orientation ->
27 | view.Orientation <- orientation
28 | | Spacing spacing ->
29 | view.Spacing <- spacing
30 | | StackLayoutAttribute.HorizontalOptions options ->
31 | view.HorizontalOptions <- options
32 | | StackLayoutAttribute.VerticalOptions options ->
33 | view.VerticalOptions <- options
34 |
35 | let label (view : Xamarin.Forms.Label) = function
36 | | LabelAttribute.HorizontalOptions options ->
37 | view.HorizontalOptions <- options
38 | | LabelAttribute.VerticalOptions options ->
39 | view.VerticalOptions <- options
40 | | LabelAttribute.HorizontalTextAlignment align ->
41 | view.HorizontalTextAlignment <- align
42 | | LabelAttribute.Text text ->
43 | view.Text <- text
44 |
45 | let button (view : Xamarin.Forms.Button) = function
46 | | ButtonAttribute.HorizontalOptions options ->
47 | view.HorizontalOptions <- options
48 | | ButtonAttribute.VerticalOptions options ->
49 | view.VerticalOptions <- options
50 | | BorderColor color ->
51 | view.BorderColor <- color
52 | | BorderRadius radius ->
53 | view.BorderRadius <- radius
54 | | BorderWidth width ->
55 | view.BorderWidth <- width
56 | | ButtonAttribute.FontAttributes attributes ->
57 | view.FontAttributes <- attributes
58 | | ButtonAttribute.FontFamily family ->
59 | view.FontFamily <- family
60 | | ButtonAttribute.FontSize size ->
61 | view.FontSize <- size
62 | | Image name ->
63 | view.Image <- loadImage name
64 | | ButtonAttribute.Text text ->
65 | view.Text <- text
66 | | ButtonAttribute.TextColor color ->
67 | view.TextColor <- color
68 | | OnClicked (UnitEvent.Event fn) ->
69 | Event.clear "Clicked" view
70 | view.Clicked.Add (fun _ -> fn ())
71 |
72 | let entry (view : Xamarin.Forms.Entry) = function
73 | | EntryAttribute.HorizontalOptions options ->
74 | view.HorizontalOptions <- options
75 | | EntryAttribute.Margin thickness ->
76 | view.Margin <- thickness
77 | | EntryAttribute.VerticalOptions options ->
78 | view.VerticalOptions <- options
79 | | FontAttributes fontAttributes ->
80 | view.FontAttributes <- fontAttributes
81 | | FontFamily family ->
82 | view.FontFamily <- family
83 | | FontSize size ->
84 | view.FontSize <- size
85 | | HorizontalTextAlignment textAlignment ->
86 | view.HorizontalTextAlignment <- textAlignment
87 | | IsPassword isPassword ->
88 | view.IsPassword <- isPassword
89 | | Placeholder placeholder ->
90 | view.Placeholder <- placeholder
91 | | PlaceholderColor color ->
92 | view.PlaceholderColor <- color
93 | | Text text ->
94 | view.Text <- text
95 | | TextColor color ->
96 | view.TextColor <- color
97 | | OnCompleted (UnitEvent.Event fn) ->
98 | Event.clear "Completed" view
99 | view.Completed.Add (ignore >> fn)
100 | | OnTextChanged (StringEvent.Event fn) ->
101 | Event.clear "TextChanged" view
102 | view.TextChanged.Add (fun ev -> fn ev.NewTextValue)
103 |
104 | let tableSection (view : Xamarin.Forms.TableSection) = function
105 | | TableSectionAttribute.Title title ->
106 | view.Title <- title
107 |
108 | let tableView (view : Xamarin.Forms.TableView) = function
109 | | TableViewAttribute.HasUnevenRows hasUnevenRows ->
110 | view.HasUnevenRows <- hasUnevenRows
111 | | Intent intent ->
112 | view.Intent <- intent
113 | | RowHeight rowHeight ->
114 | view.RowHeight <- rowHeight
115 |
116 | let textCell (view : Xamarin.Forms.TextCell) = function
117 | | TextCellAttribute.Text text ->
118 | view.Text <- text
119 | | TextCellAttribute.TextColor textColor ->
120 | view.TextColor <- textColor
121 | | Detail detail ->
122 | view.Detail <- detail
123 | | DetailColor detailColor ->
124 | view.DetailColor <- detailColor
125 | | OnTapped (UnitEvent.Event event) ->
126 | Event.clear "Tapped" view
127 | view.Tapped.Add (ignore >> event)
128 |
129 | let updateAll updateFn attributes view =
130 | attributes |> Seq.iter (updateFn view)
131 |
132 | let setUpdated updateFn viewAttributes lastViewAttributes viewRoot =
133 | let updatedAttributes =
134 | Seq.zip viewAttributes lastViewAttributes
135 | |> Seq.filter (fun (v, l) -> v <> l)
136 | |> Seq.map fst
137 | updateAll updateFn updatedAttributes viewRoot
138 |
139 |
140 |
--------------------------------------------------------------------------------
/Xamarin/Create.fs:
--------------------------------------------------------------------------------
1 | module Elmish.Xamarin.Forms.Create
2 |
3 | open Elmish.Xamarin.Forms.VirtualDOM
4 | open Elmish.Xamarin.Forms
5 |
6 |
7 | let cell = function
8 | | TextCell attributes ->
9 | let cell = Xamarin.Forms.TextCell ()
10 | Attributes.updateAll Attributes.textCell attributes cell
11 | cell :> Xamarin.Forms.Cell
12 |
13 | | CellExtension ext -> ext.create ()
14 |
15 | let section = function
16 | | TableSection (attributes, cells) ->
17 | let section = new Xamarin.Forms.TableSection ()
18 | Attributes.updateAll Attributes.tableSection attributes section
19 |
20 | section.Add (cells |> Seq.map cell)
21 | section
22 |
23 | let tableRoot sections =
24 | let root = new Xamarin.Forms.TableRoot()
25 | root.Add (sections |> Seq.map section)
26 | root
27 |
28 | let inline createView<'t, 'a when 't : (new : unit -> 't) and 't :> Xamarin.Forms.View> (updateFn : 't -> 'a -> unit) (attributes : 'a seq) =
29 | let v = new 't ()
30 | Attributes.updateAll updateFn attributes v
31 | v :> Xamarin.Forms.View
32 |
33 | let rec view = function
34 | | Button attributes ->
35 | createView Attributes.button attributes
36 |
37 | | Entry attributes ->
38 | createView Attributes.entry attributes
39 |
40 | | Label attributes ->
41 | createView Attributes.label attributes
42 |
43 | | StackLayout (attributes, children) ->
44 | let stack = Xamarin.Forms.StackLayout ()
45 | Attributes.updateAll Attributes.stackLayout attributes stack
46 |
47 | children
48 | |> Seq.map view
49 | |> Seq.iter stack.Children.Add
50 |
51 | stack :> Xamarin.Forms.View
52 |
53 | | TableView (attributes, sections) ->
54 | let root = tableRoot sections
55 | let tableView = Xamarin.Forms.TableView ()
56 | Attributes.updateAll Attributes.tableView attributes tableView
57 |
58 | tableView.Root <- root
59 | tableView :> Xamarin.Forms.View
60 |
61 | | ViewExtension ext ->
62 | ext.create ()
63 |
64 |
65 | let rec page = function
66 | | ContentPage (attributes, content) ->
67 | let page = Xamarin.Forms.ContentPage(Content = view content)
68 | Attributes.updateAll Attributes.contentPage attributes page
69 | page :> Xamarin.Forms.Page
70 |
71 | | MasterDetailPage (attributes, master, detail) ->
72 | let page = Xamarin.Forms.MasterDetailPage(Master = page master, Detail = page detail)
73 | Attributes.updateAll Attributes.masterDetailPage attributes page
74 | page :> Xamarin.Forms.Page
75 |
76 | | NavigationPage content ->
77 | let navigationPage = new Xamarin.Forms.NavigationPage()
78 | content
79 | |> Seq.map page
80 | |> Seq.rev
81 | |> Seq.iter (fun p -> navigationPage.PushAsync(p) |> ignore)
82 |
83 | navigationPage :> Xamarin.Forms.Page
84 |
85 |
--------------------------------------------------------------------------------
/Xamarin/Event.fs:
--------------------------------------------------------------------------------
1 | module Elmish.Xamarin.Forms.Event
2 |
3 | open System.Reflection
4 | open System
5 |
6 | // This is a fairly hacky solution to clearing all registered listeners from an event,
7 | // based on this answer on Stack Overflow
8 | // https://stackoverflow.com/a/8108103/619821
9 |
10 |
11 | // Find events defined as field
12 | let private getFieldEvent eventName (typ : Type) =
13 | typ.GetField(eventName, BindingFlags.Static ||| BindingFlags.Instance ||| BindingFlags.NonPublic)
14 | |> Option.ofObj
15 |
16 | // Find events defined as property { add; remove; }
17 | let private getPropertyEvent (eventName : string) (typ : Type) =
18 | typ.GetField("EVENT_" + eventName.ToUpper(), BindingFlags.Static ||| BindingFlags.Instance ||| BindingFlags.NonPublic)
19 | |> Option.ofObj
20 |
21 | let rec private getEventField eventName (typ : Type) =
22 | match getFieldEvent eventName typ with
23 | | Some field as optField when field.FieldType = typeof || field.FieldType.IsSubclassOf(typeof) ->
24 | optField
25 | | _ ->
26 | match getPropertyEvent eventName typ with
27 | | Some _ as optField -> optField
28 | | _ -> getEventField eventName typ.BaseType
29 |
30 | let clear eventName object =
31 | getEventField eventName (object.GetType())
32 | |> Option.iter (fun fi -> fi.SetValue(object, null))
33 |
34 |
--------------------------------------------------------------------------------
/Xamarin/Update.fs:
--------------------------------------------------------------------------------
1 | module Elmish.Xamarin.Forms.Update
2 |
3 | open Elmish.Extensions
4 | open Elmish.Xamarin.Forms.VirtualDOM
5 |
6 | let updateChildren createFn updateFn viewChildren lastViewChildren (collection : System.Collections.Generic.IList<'a>) =
7 | let viewsToUpdate =
8 | Seq.zip3 viewChildren lastViewChildren collection
9 | |> Seq.mapi (fun i t -> i, (t |||> updateFn))
10 | |> Seq.bindOption (fun (i, optView) -> optView |> Option.map (fun view -> i, view))
11 | |> Seq.toArray // make collection eager
12 |
13 | viewsToUpdate |> Seq.iter (fun (i, view) -> collection.[i] <- view)
14 |
15 | let totalViews = viewChildren |> Seq.length
16 | if totalViews > collection.Count then
17 | viewChildren
18 | |> Seq.skip collection.Count
19 | |> Seq.iter (createFn >> collection.Add)
20 | else if totalViews < collection.Count then
21 | for i in collection.Count - 1 .. -1 .. totalViews do
22 | collection.RemoveAt i
23 |
24 | let rec cell newView lastView (root : Xamarin.Forms.Cell) : Xamarin.Forms.Cell option =
25 | if newView = lastView then
26 | None
27 | else
28 | match newView, lastView, root with
29 | | TextCell viewAttributes, TextCell lastViewAttributes, (:? Xamarin.Forms.TextCell as viewRoot) ->
30 | Attributes.setUpdated Attributes.textCell viewAttributes lastViewAttributes viewRoot
31 | None
32 | | CellExtension ext, CellExtension lastExt, _ ->
33 | ext.update lastExt root
34 |
35 | and section newView lastView (viewRoot : Xamarin.Forms.TableSection) : Xamarin.Forms.TableSection option =
36 | if newView = lastView then
37 | None
38 | else
39 | match newView, lastView with
40 | | TableSection (viewAttributes, viewChildren), TableSection (lastViewAttributes, lastViewChildren) ->
41 | updateChildren Create.cell cell viewChildren lastViewChildren viewRoot
42 | Attributes.setUpdated Attributes.tableSection viewAttributes lastViewAttributes viewRoot
43 | None
44 |
45 | and view newView lastView (root : Xamarin.Forms.View) : Xamarin.Forms.View option =
46 | if newView = lastView then
47 | None
48 | else
49 | match newView, lastView, root with
50 | | Button viewAttributes, Button lastViewAttributes, (:? Xamarin.Forms.Button as viewRoot) ->
51 | Attributes.setUpdated Attributes.button viewAttributes lastViewAttributes viewRoot
52 | None
53 |
54 | | Entry viewAttributes, Entry lastViewAttributes, (:? Xamarin.Forms.Entry as viewRoot) ->
55 | Attributes.setUpdated Attributes.entry viewAttributes lastViewAttributes viewRoot
56 | None
57 |
58 | | Label viewAttributes, Label lastViewAttributes, (:? Xamarin.Forms.Label as viewRoot) ->
59 | Attributes.setUpdated Attributes.label viewAttributes lastViewAttributes viewRoot
60 | None
61 |
62 | | StackLayout (viewAttributes, viewChildren), StackLayout (lastViewAttributes, lastViewChildren), (:? Xamarin.Forms.StackLayout as viewRoot) ->
63 | updateChildren Create.view view viewChildren lastViewChildren viewRoot.Children
64 | Attributes.setUpdated Attributes.stackLayout viewAttributes lastViewAttributes viewRoot
65 | None
66 |
67 | | TableView (viewAttributes, viewSections), TableView (lastViewAttributes, lastViewSections), (:? Xamarin.Forms.TableView as viewRoot) ->
68 | updateChildren Create.section section viewSections lastViewSections viewRoot.Root
69 | Attributes.setUpdated Attributes.tableView viewAttributes lastViewAttributes viewRoot
70 | None
71 |
72 | | ViewExtension viewExtension, ViewExtension lastViewExtension, _ ->
73 | viewExtension.update lastViewExtension root
74 |
75 | | _ ->
76 | Create.view newView |> Some
77 |
78 |
79 | let rec page newView lastView (root : Xamarin.Forms.Page) : Xamarin.Forms.Page option =
80 | if newView = lastView then
81 | None
82 | else
83 | match newView, lastView, root with
84 | | NavigationPage viewPages, NavigationPage lastViewPages, (:? Xamarin.Forms.NavigationPage as viewRoot) ->
85 | let stack = viewRoot.Navigation.NavigationStack |> Seq.rev |> Seq.toArray
86 | let updatedPages = Seq.zip3 viewPages lastViewPages stack |> Seq.map (fun t -> t |||> page) |> Seq.toArray
87 | if updatedPages |> Seq.exists (fun v -> v <> None) then
88 | viewRoot.PopToRootAsync(false) |> ignore
89 | Seq.zip stack updatedPages
90 | |> Seq.map (fun (v, vopt) -> defaultArg vopt v)
91 | |> Seq.rev
92 | |> Seq.iter (fun v -> viewRoot.PushAsync(v) |> ignore)
93 | None
94 |
95 | | MasterDetailPage (viewAttributes, viewMaster, viewDetail), MasterDetailPage (lastViewAttributes, lastViewMaster, lastViewDetail), (:? Xamarin.Forms.MasterDetailPage as viewRoot) ->
96 | Attributes.setUpdated Attributes.masterDetailPage viewAttributes lastViewAttributes viewRoot
97 |
98 | page viewMaster lastViewMaster viewRoot.Master
99 | |> Option.iter (fun page -> viewRoot.Master <- page)
100 |
101 | page viewDetail lastViewDetail viewRoot.Detail
102 | |> Option.iter (fun page -> viewRoot.Detail <- page)
103 |
104 | None
105 |
106 | | ContentPage (viewAttributes, viewContent), ContentPage (lastViewAttributes, lastViewContent), (:? Xamarin.Forms.ContentPage as viewRoot) ->
107 | view viewContent lastViewContent viewRoot.Content
108 | |> Option.iter (fun view -> viewRoot.Content <- view)
109 |
110 | Attributes.setUpdated Attributes.contentPage viewAttributes lastViewAttributes viewRoot
111 | None
112 |
--------------------------------------------------------------------------------
/Xamarin/VirtualDOM.fs:
--------------------------------------------------------------------------------
1 | module Elmish.Xamarin.Forms.VirtualDOM
2 |
3 | type Extension<'x> =
4 | abstract member create: unit -> 'x
5 | abstract member update: Extension<'x> -> 'x -> 'x option
6 |
7 | (* Event signatures *)
8 |
9 | []
10 | type Event<'t> =
11 | | Event of ('t -> unit)
12 | override this.Equals(that) = System.Object.ReferenceEquals(this, that)
13 | override this.GetHashCode() = 0
14 |
15 | type UnitEvent = Event
16 | type BoolEvent = Event
17 | type StringEvent = Event
18 |
19 |
20 | (* Cells *)
21 |
22 | type TextCellAttribute =
23 | | Text of string
24 | | TextColor of Xamarin.Forms.Color
25 | | Detail of string
26 | | DetailColor of Xamarin.Forms.Color
27 | | OnTapped of UnitEvent
28 |
29 | type Cell =
30 | | TextCell of TextCellAttribute list
31 | | CellExtension of (Extension)
32 |
33 | let textCell attributes = TextCell attributes
34 |
35 |
36 | (* Tables *)
37 |
38 | type TableSectionAttribute =
39 | | Title of string
40 |
41 | type TableSection =
42 | | TableSection of TableSectionAttribute list * Cell list
43 |
44 | let tableSection attributes children = TableSection (attributes, children)
45 |
46 |
47 | (* Views *)
48 |
49 | type StackLayoutAttribute =
50 | | Orientation of Xamarin.Forms.StackOrientation
51 | | Spacing of float
52 | | HorizontalOptions of Xamarin.Forms.LayoutOptions
53 | | VerticalOptions of Xamarin.Forms.LayoutOptions
54 |
55 | type LabelAttribute =
56 | | HorizontalOptions of Xamarin.Forms.LayoutOptions
57 | | VerticalOptions of Xamarin.Forms.LayoutOptions
58 | | HorizontalTextAlignment of Xamarin.Forms.TextAlignment
59 | | Text of string
60 |
61 | type TableViewAttribute =
62 | | HasUnevenRows of bool
63 | | Intent of Xamarin.Forms.TableIntent
64 | | RowHeight of int
65 |
66 | type ButtonAttribute =
67 | | HorizontalOptions of Xamarin.Forms.LayoutOptions
68 | | VerticalOptions of Xamarin.Forms.LayoutOptions
69 | | BorderColor of Xamarin.Forms.Color
70 | | BorderRadius of int
71 | | BorderWidth of float
72 | | FontAttributes of Xamarin.Forms.FontAttributes
73 | | FontFamily of string
74 | | FontSize of float
75 | | Image of string
76 | | Text of string
77 | | TextColor of Xamarin.Forms.Color
78 | | OnClicked of UnitEvent
79 |
80 | type EntryAttribute =
81 | | HorizontalOptions of Xamarin.Forms.LayoutOptions
82 | | Margin of Xamarin.Forms.Thickness
83 | | VerticalOptions of Xamarin.Forms.LayoutOptions
84 | | FontAttributes of Xamarin.Forms.FontAttributes
85 | | FontFamily of string
86 | | FontSize of float
87 | | HorizontalTextAlignment of Xamarin.Forms.TextAlignment
88 | | IsPassword of bool
89 | | Placeholder of string
90 | | PlaceholderColor of Xamarin.Forms.Color
91 | | Text of string
92 | | TextColor of Xamarin.Forms.Color
93 | | OnCompleted of UnitEvent
94 | | OnTextChanged of StringEvent
95 |
96 | type View =
97 | | Button of ButtonAttribute list
98 | | Entry of EntryAttribute list
99 | | Label of LabelAttribute list
100 | | StackLayout of StackLayoutAttribute list * View list
101 | | TableView of TableViewAttribute list * TableSection list
102 | | ViewExtension of Extension
103 |
104 |
105 | let button attributes = Button attributes
106 |
107 | let entry attributes = Entry attributes
108 |
109 | let label attributes = Label attributes
110 |
111 | let stackLayout attributes children = StackLayout (attributes, children)
112 |
113 | let tableView attributes children = TableView (attributes, children)
114 |
115 |
116 |
117 | (* Pages *)
118 |
119 | type ContentPageAttribute =
120 | | Icon of string
121 | | Title of string
122 |
123 | type MasterDetailPageAttribute =
124 | | IsGestureEnabled of bool
125 | | IsPresented of bool
126 | | MasterBehavior of Xamarin.Forms.MasterBehavior
127 | | OnIsPresentedChanged of BoolEvent
128 |
129 | type Page =
130 | | ContentPage of ContentPageAttribute list * View
131 | | MasterDetailPage of MasterDetailPageAttribute list * Page * Page
132 | | NavigationPage of Page list
133 |
134 |
135 | let contentPage attributes content = ContentPage (attributes, content)
136 |
137 | let masterDetailPage attributes master detail = MasterDetailPage (attributes, master, detail)
138 |
139 | let navigationPage content = NavigationPage content
140 |
141 |
142 |
--------------------------------------------------------------------------------
/cmd.fs:
--------------------------------------------------------------------------------
1 | (*
2 | Copyright 2016 fable-elmish contributors
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 | use this file except in compliance with the License. You may obtain a copy of
6 | the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 | License for the specific language governing permissions and limitations under
14 | the License.
15 | *)
16 |
17 | (*
18 | In accordance with the Apache License, Version 2.0, section 4, this is a
19 | notice to inform the recipient that this file has been modified by the
20 | elmish-xamarin-forms contributors.
21 | *)
22 |
23 | (**
24 | Cmd
25 | ---------
26 | Core abstractions for dispatching messages in Elmish.
27 |
28 | *)
29 |
30 | namespace Elmish
31 |
32 | open System
33 |
34 | /// Dispatch - feed new message into the processing loop
35 | type Dispatch<'msg> = 'msg -> unit
36 |
37 | /// Subscription - return immediately, but may schedule dispatch of a message at any time
38 | type Sub<'msg> = Dispatch<'msg> -> unit
39 |
40 | /// Cmd - container for subscriptions that may produce messages
41 | type Cmd<'msg> = Sub<'msg> list
42 |
43 | /// Cmd module for creating and manipulating commands
44 | []
45 | module Cmd =
46 | /// None - no commands, also known as `[]`
47 | let none : Cmd<'msg> =
48 | []
49 |
50 | /// Command to issue a specific message
51 | let ofMsg (msg:'msg) : Cmd<'msg> =
52 | [fun dispatch -> dispatch msg]
53 |
54 | /// When emitting the message, map to another type
55 | let map (f: 'a -> 'msg) (cmd: Cmd<'a>) : Cmd<'msg> =
56 | cmd |> List.map (fun g -> (fun dispatch -> f >> dispatch) >> g)
57 |
58 | /// Aggregate multiple commands
59 | let batch (cmds: #seq>) : Cmd<'msg> =
60 | cmds |> List.concat
61 |
62 | /// Command that will evaluate an async block and map the result
63 | /// into success or error (of exception)
64 | let ofAsync (task: 'a -> Async<_>)
65 | (arg: 'a)
66 | (ofSuccess: _ -> 'msg)
67 | (ofError: _ -> 'msg) : Cmd<'msg> =
68 | let bind dispatch =
69 | async {
70 | let! r = task arg |> Async.Catch
71 | dispatch (match r with
72 | | Choice1Of2 x -> ofSuccess x
73 | | Choice2Of2 x -> ofError x)
74 | }
75 | [bind >> Async.StartImmediate]
76 |
77 | /// Command to evaluate a simple function and map the result
78 | /// into success or error (of exception)
79 | let ofFunc (task: 'a -> _) (arg: 'a) (ofSuccess: _ -> 'msg) (ofError: _ -> 'msg) : Cmd<'msg> =
80 | let bind dispatch =
81 | try
82 | task arg
83 | |> (ofSuccess >> dispatch)
84 | with x ->
85 | x |> (ofError >> dispatch)
86 | [bind]
87 |
88 | /// Command to evaluate a simple function and map the success to a message
89 | /// discarding any possible error
90 | let performFunc (task: 'a -> _) (arg: 'a) (ofSuccess: _ -> 'msg) : Cmd<'msg> =
91 | let bind dispatch =
92 | try
93 | task arg
94 | |> (ofSuccess >> dispatch)
95 | with x ->
96 | ()
97 | [bind]
98 |
99 | /// Command to evaluate a simple function and map the error (in case of exception)
100 | let attemptFunc (task: 'a -> unit) (arg: 'a) (ofError: _ -> 'msg) : Cmd<'msg> =
101 | let bind dispatch =
102 | try
103 | task arg
104 | with x ->
105 | x |> (ofError >> dispatch)
106 | [bind]
107 |
108 | /// Command to call the subscriber
109 | let ofSub (sub: Sub<'msg>) : Cmd<'msg> =
110 | [sub]
111 |
112 |
--------------------------------------------------------------------------------
/program.fs:
--------------------------------------------------------------------------------
1 | (*
2 | Copyright 2016 fable-elmish contributors
3 |
4 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 | use this file except in compliance with the License. You may obtain a copy of
6 | the License at
7 |
8 | http://www.apache.org/licenses/LICENSE-2.0
9 |
10 | Unless required by applicable law or agreed to in writing, software
11 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 | License for the specific language governing permissions and limitations under
14 | the License.
15 | *)
16 |
17 | (*
18 | In accordance with the Apache License, Version 2.0, section 4, this is a
19 | notice to inform the recipient that this file has been modified by the
20 | elmish-xamarin-forms contributors.
21 | *)
22 |
23 | (**
24 | Program
25 | ---------
26 | Core abstractions for creating and running the dispatch loop.
27 |
28 | *)
29 |
30 | namespace Elmish
31 |
32 | open System
33 |
34 | /// Program type captures various aspects of program behavior
35 | type Program<'arg, 'model, 'msg, 'view> = {
36 | init : 'arg -> 'model * Cmd<'msg>
37 | update : 'msg -> 'model -> 'model * Cmd<'msg>
38 | subscribe : 'model -> Cmd<'msg>
39 | view : 'model -> Dispatch<'msg> -> 'view
40 | setState : 'model -> Dispatch<'msg> -> unit
41 | onError : (string*exn) -> unit
42 | }
43 |
44 | /// Program module - functions to manipulate program instances
45 | []
46 | []
47 | module Program =
48 | let internal onError (text: string, ex: exn) =
49 | System.Console.Error.WriteLine (sprintf "%s: %A" text ex)
50 |
51 | /// Typical program, new commands are produced by `init` and `update` along with the new state.
52 | let mkProgram
53 | (init : 'arg -> 'model * Cmd<'msg>)
54 | (update : 'msg -> 'model -> 'model * Cmd<'msg>)
55 | (view : 'model -> Dispatch<'msg> -> 'view) =
56 | { init = init
57 | update = update
58 | view = view
59 | setState = fun model -> view model >> ignore
60 | subscribe = fun _ -> Cmd.none
61 | onError = onError }
62 |
63 | /// Simple program that produces only new state with `init` and `update`.
64 | let mkSimple
65 | (init : 'arg -> 'model)
66 | (update : 'msg -> 'model -> 'model)
67 | (view : 'model -> Dispatch<'msg> -> 'view) =
68 | { init = init >> fun state -> state,Cmd.none
69 | update = fun msg -> update msg >> fun state -> state,Cmd.none
70 | view = view
71 | setState = fun model -> view model >> ignore
72 | subscribe = fun _ -> Cmd.none
73 | onError = onError }
74 |
75 | /// Subscribe to external source of events.
76 | /// The subscription is called once - with the initial model, but can dispatch new messages at any time.
77 | let withSubscription (subscribe : 'model -> Cmd<'msg>) (program: Program<'arg, 'model, 'msg, 'view>) =
78 | let sub model =
79 | Cmd.batch [ program.subscribe model
80 | subscribe model ]
81 | { program with subscribe = sub }
82 |
83 | /// Trace all the updates to the console
84 | let withConsoleTrace (program: Program<'arg, 'model, 'msg, 'view>) =
85 | let traceInit (arg:'arg) =
86 | let initModel,cmd = program.init arg
87 | System.Console.Out.WriteLine (sprintf "Initial state: %A" initModel)
88 | initModel,cmd
89 |
90 | let traceUpdate msg model =
91 | System.Console.Out.WriteLine (sprintf "New message: %A" msg)
92 | let newModel,cmd = program.update msg model
93 | System.Console.Out.WriteLine (sprintf "Updated state: %A" newModel)
94 | newModel,cmd
95 |
96 | { program with
97 | init = traceInit
98 | update = traceUpdate }
99 |
100 | /// Trace all the messages as they update the model
101 | let withTrace trace (program: Program<'arg, 'model, 'msg, 'view>) =
102 | { program
103 | with update = fun msg model -> trace msg model; program.update msg model}
104 |
105 | /// Handle dispatch loop exceptions
106 | let withErrorHandler onError (program: Program<'arg, 'model, 'msg, 'view>) =
107 | { program
108 | with onError = onError }
109 |
110 | /// Start the program loop.
111 | /// arg: argument to pass to the init() function.
112 | /// program: program created with 'mkSimple' or 'mkProgram'.
113 | let runWith (arg: 'arg) (program: Program<'arg, 'model, 'msg, 'view>) =
114 | let (model,cmd) = program.init arg
115 | let inbox = MailboxProcessor.Start(fun (mb:MailboxProcessor<'msg>) ->
116 | let rec loop (state:'model) =
117 | async {
118 | let! msg = mb.Receive()
119 | try
120 | let (model',cmd') = program.update msg state
121 | program.setState model' mb.Post
122 | cmd' |> List.iter (fun sub -> sub mb.Post)
123 | return! loop model'
124 | with ex ->
125 | program.onError ("Unable to process a message:", ex)
126 | return! loop state
127 | }
128 | loop model
129 | )
130 | program.setState model inbox.Post
131 | program.subscribe model
132 | @ cmd |> List.iter (fun sub -> sub inbox.Post)
133 |
134 | /// Start the dispatch loop with `unit` for the init() function.
135 | let run (program: Program) = runWith () program
136 |
137 |
--------------------------------------------------------------------------------