-
-
Notifications
You must be signed in to change notification settings - Fork 122
/
ViewHelpers.fs
182 lines (151 loc) · 9.23 KB
/
ViewHelpers.fs
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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
// Copyright 2018-2019 Fabulous contributors. See LICENSE.md for license.
namespace Fabulous.XamarinForms
open Fabulous
open Xamarin.Forms
open System
open System.Collections.Concurrent
open System.Threading
[<AutoOpen>]
module ViewHelpers =
/// Checks whether two objects are reference-equal
let identical (x: 'T) (y:'T) = System.Object.ReferenceEquals(x, y)
let identicalVOption (x: 'T voption) (y: 'T voption) =
match struct (x, y) with
| struct (ValueNone, ValueNone) -> true
| struct (ValueSome x1, ValueSome y1) when identical x1 y1 -> true
| _ -> false
/// Checks whether an underlying control can be reused given the previous and new view elements
let rec canReuseView (prevChild: ViewElement) (newChild: ViewElement) =
if prevChild.TargetType = newChild.TargetType && canReuseAutomationId prevChild newChild then
if newChild.TargetType.IsAssignableFrom(typeof<NavigationPage>) then
canReuseNavigationPage prevChild newChild
elif newChild.TargetType.IsAssignableFrom(typeof<CustomEffect>) then
canReuseCustomEffect prevChild newChild
else
true
else
false
/// Checks whether an underlying NavigationPage control can be reused given the previous and new view elements
//
// NavigationPage can be reused only if the pages don't change their type (added/removed pages don't prevent reuse)
// E.g. If the first page switch from ContentPage to TabbedPage, the NavigationPage can't be reused.
and internal canReuseNavigationPage (prevChild:ViewElement) (newChild:ViewElement) =
let prevPages = prevChild.TryGetAttributeKeyed(ViewAttributes.PagesAttribKey)
let newPages = newChild.TryGetAttributeKeyed(ViewAttributes.PagesAttribKey)
match prevPages, newPages with
| ValueSome prevPages, ValueSome newPages -> (prevPages, newPages) ||> Seq.forall2 canReuseView
| _, _ -> true
/// Checks whether the control can be reused given the previous and the new AutomationId.
/// Xamarin.Forms can't change an already set AutomationId
and internal canReuseAutomationId (prevChild: ViewElement) (newChild: ViewElement) =
let prevAutomationId = prevChild.TryGetAttributeKeyed(ViewAttributes.AutomationIdAttribKey)
let newAutomationId = newChild.TryGetAttributeKeyed(ViewAttributes.AutomationIdAttribKey)
match prevAutomationId with
| ValueSome _ when prevAutomationId <> newAutomationId -> false
| _ -> true
/// Checks whether the CustomEffect can be reused given the previous and the new Effect name
/// The effect is instantiated by Effect.Resolve and can't be reused when asking for a new effect
and internal canReuseCustomEffect (prevChild:ViewElement) (newChild:ViewElement) =
let prevName = prevChild.TryGetAttributeKeyed(ViewAttributes.NameAttribKey)
let newName = newChild.TryGetAttributeKeyed(ViewAttributes.NameAttribKey)
match prevName with
| ValueSome _ when prevName <> newName -> false
| _ -> true
/// Debounce multiple calls to a single function
let debounce<'T> =
let memoization = ConcurrentDictionary<obj, CancellationTokenSource>(HashIdentity.Structural)
fun (timeout: int) (fn: 'T -> unit) value ->
let key = fn.GetType()
match memoization.TryGetValue(key) with
| true, previousCts -> previousCts.Cancel()
| _ -> ()
let cts = new CancellationTokenSource()
memoization.[key] <- cts
Device.StartTimer(TimeSpan.FromMilliseconds(float timeout), (fun () ->
match cts.IsCancellationRequested with
| true -> ()
| false ->
memoization.TryRemove(key) |> ignore
fn value
false // Do not let the timer trigger a second time
))
/// Looks for a view element with the given Automation ID in the view hierarchy.
/// This function is not optimized for efficiency and may execute slowly.
let rec tryFindViewElement automationId (element:ViewElement) =
let elementAutomationId = element.TryGetAttributeKeyed(ViewAttributes.AutomationIdAttribKey)
match elementAutomationId with
| ValueSome automationIdValue when automationIdValue = automationId -> Some element
| _ ->
let childElements =
match element.TryGetAttributeKeyed(ViewAttributes.ContentAttribKey) with
| ValueSome content -> [| content |]
| ValueNone ->
match element.TryGetAttributeKeyed(ViewAttributes.PagesAttribKey) with
| ValueSome pages -> pages
| ValueNone ->
match element.TryGetAttributeKeyed(ViewAttributes.ChildrenAttribKey) with
| ValueNone -> [||]
| ValueSome children -> children
childElements
|> Seq.choose (tryFindViewElement automationId)
|> Seq.tryHead
/// Looks for a view element with the given Automation ID in the view hierarchy
/// Throws an exception if no element is found
let findViewElement automationId element =
match tryFindViewElement automationId element with
| None -> failwithf "No element with automation id '%s' found" automationId
| Some viewElement -> viewElement
/// Try to retrieve the value of the "Key" property
let tryGetKey (x: ViewElement) = x.TryGetKey()
let ContentsAttribKey = AttributeKey<(obj -> ViewElement)> "Stateful_Contents"
let localStateTable = System.Runtime.CompilerServices.ConditionalWeakTable<obj, obj option>()
type View with
/// Describes an element in the view which uses localized mutable state unrelated to the model
/// (and hence un-persisted), and can optionally access the underlying control. The 'init'
/// function is called only when the underlying control is created (and each time it is re-created,
/// if ever). The generated state object is associated with the underlying control.
static member Stateful (init: (unit -> 'State), contents: 'State -> ViewElement, ?onCreate: ('State -> obj -> unit), ?onUpdate: ('State -> obj -> unit)) : _ when 'State : not struct =
let attribs = AttributesBuilder(1)
attribs.Add(ContentsAttribKey, (fun stateObj -> contents (unbox (stateObj))))
// The create method
let create () =
let state = init()
let desc = contents state
let item = desc.Create()
localStateTable.Add(item, Some (box state))
match onCreate with None -> () | Some f -> f state item
item
// The update method
let update (prevOpt: ViewElement voption) (source: ViewElement) (target: obj) =
let state = unbox<'State> ((snd (localStateTable.TryGetValue(target))).Value)
let contents = source.TryGetAttributeKeyed(ContentsAttribKey).Value
let realSource = contents state
realSource.Update(prevOpt, source, target)
match onUpdate with None -> () | Some f -> f state target
let updateAttachedProperties _key _prevOpt _source _target = ()
// The element
ViewElement.Create(create, update, updateAttachedProperties, attribs)
static member OnCreate (contents : ViewElement, onCreate: (obj -> unit)) =
View.Stateful (init = (fun () -> ()), contents = (fun _ -> contents), onCreate = (fun _ obj -> onCreate obj))
static member WithInternalModel(init: (unit -> 'InternalModel),
update: ('InternalMessage -> 'InternalModel -> 'InternalModel),
view : ('InternalModel -> ('InternalMessage -> unit) -> ViewElement)) =
let internalDispatch (state: 'InternalModel ref) msg = state.Value <- update msg state.Value
View.Stateful (init = (fun () -> ref (init ())), contents = (fun state -> view state.Value (internalDispatch state)))
// Keep a table to make sure we create a unique ViewElement for each external object
let externalsTable = System.Runtime.CompilerServices.ConditionalWeakTable<obj, obj>()
type View with
/// Describes an element in the view implemented by an external object, e.g. an external
/// Xamarin.Forms Page or View. The element must have a type appropriate for the place in
/// the view where the object occurs.
static member External (externalObj: 'T) : _ when 'T : not struct =
match externalsTable.TryGetValue(externalObj) with
| true, v -> (v :?> ViewElement)
| _ ->
let attribs = AttributesBuilder(0)
let create () = box externalObj
let update (_prevOpt: ViewElement voption) (_source: ViewElement) (_target: obj) = ()
let updateAttachedProperties _key _prevOpt _curr _target = ()
let res = ViewElement(externalObj.GetType(), create, update, updateAttachedProperties, attribs)
externalsTable.Add(externalObj, res)
res