diff --git a/Fabulous.CodeGen/src/Fabulous.CodeGen/Generator/CodeGenerator.fs b/Fabulous.CodeGen/src/Fabulous.CodeGen/Generator/CodeGenerator.fs index 9367dba45..7c88b8181 100644 --- a/Fabulous.CodeGen/src/Fabulous.CodeGen/Generator/CodeGenerator.fs +++ b/Fabulous.CodeGen/src/Fabulous.CodeGen/Generator/CodeGenerator.fs @@ -108,9 +108,9 @@ module CodeGenerator = // Check if the property is a collection match p.CollectionDataElementType with | Some collectionDataElementType when not hasApply -> - w.printfn " ChildrenUpdaters.updateChildren prev%sOpt curr%sOpt target.%s" p.UniqueName p.UniqueName p.Name + w.printfn " Collections.updateChildren prev%sOpt curr%sOpt target.%s" p.UniqueName p.UniqueName p.Name w.printfn " (fun x -> x.Create() :?> %s)" collectionDataElementType - w.printfn " ChildrenUpdaters.updateChild" + w.printfn " Collections.updateChild" w.printfn " (match registry.TryGetValue(%s.KeyValue) with true, func -> func | false, _ -> (fun _ _ _ -> ()))" attributeKey | Some _ when hasApply -> diff --git a/Fabulous.XamarinForms/extensions/Maps/ViewUpdaters.fs b/Fabulous.XamarinForms/extensions/Maps/ViewUpdaters.fs index 9b60a5180..9db482a37 100644 --- a/Fabulous.XamarinForms/extensions/Maps/ViewUpdaters.fs +++ b/Fabulous.XamarinForms/extensions/Maps/ViewUpdaters.fs @@ -11,7 +11,7 @@ module ViewUpdaters = | _ -> () let updatePolygonGeopath (prevCollOpt: Position array voption) (currCollOpt: Xamarin.Forms.Maps.Position array voption) (target: Xamarin.Forms.Maps.Polygon) _ = - ItemsUpdaters.updateItems prevCollOpt currCollOpt target.Geopath + Collections.updateItems prevCollOpt currCollOpt target.Geopath (fun _ -> ValueNone) (fun prev curr -> prev = curr) id @@ -19,7 +19,7 @@ module ViewUpdaters = (fun _ _ _ -> ()) let updatePolylineGeopath (prevCollOpt: Xamarin.Forms.Maps.Position array voption) (currCollOpt: Xamarin.Forms.Maps.Position array voption) (target: Xamarin.Forms.Maps.Polyline) _ = - ItemsUpdaters.updateItems prevCollOpt currCollOpt target.Geopath + Collections.updateItems prevCollOpt currCollOpt target.Geopath (fun _ -> ValueNone) (fun prev curr -> prev = curr) id diff --git a/Fabulous.XamarinForms/samples/TicTacToe/TicTacToe/TicTacToe.fs b/Fabulous.XamarinForms/samples/TicTacToe/TicTacToe/TicTacToe.fs index ec51ee2fa..688227cbd 100644 --- a/Fabulous.XamarinForms/samples/TicTacToe/TicTacToe/TicTacToe.fs +++ b/Fabulous.XamarinForms/samples/TicTacToe/TicTacToe/TicTacToe.fs @@ -182,9 +182,10 @@ module App = for ((row,col) as pos) in positions -> let item = if canPlay model model.Board.[pos] then - View.Button(command=(fun () -> dispatch (Play pos)), backgroundColor=Color.LightBlue) + View.Button(key = sprintf "Button%i_%i" row col, command=(fun () -> dispatch (Play pos)), backgroundColor=Color.LightBlue) else - View.Image(source=imageForPos model.Board.[pos], + View.Image(key = sprintf "Image%i_%i" row col, + source=imageForPos model.Board.[pos], margin=Thickness(10.0), horizontalOptions=LayoutOptions.Center, verticalOptions=LayoutOptions.Center) item.Row(row*2).Column(col*2) ], diff --git a/Fabulous.XamarinForms/src/Fabulous.XamarinForms.Core/ChildrenUpdaters.fs b/Fabulous.XamarinForms/src/Fabulous.XamarinForms.Core/ChildrenUpdaters.fs deleted file mode 100644 index 8257583bd..000000000 --- a/Fabulous.XamarinForms/src/Fabulous.XamarinForms.Core/ChildrenUpdaters.fs +++ /dev/null @@ -1,230 +0,0 @@ -namespace Fabulous.XamarinForms - -open Fabulous -open System.Collections.Generic -open Xamarin.Forms - -/// This module contains the update logic for the controls with children -module ChildrenUpdaters = - /// Incremental list maintenance: given a collection, and a previous version of that collection, perform - /// a reduced number of clear/move/create/update/remove operations - /// - /// The algorithm will try in priority to update elements sharing the same instance (usually achieved with dependsOn) - /// or sharing the same key. All other elements will try to reuse previous elements where possible. - /// If no reuse is possible, the element will create a new control. - let updateChildrenInternal - (prevCollOpt: 'T[] voption) - (collOpt: 'T[] voption) - (keyOf: 'T -> string voption) - (canReuse: 'T -> 'T -> bool) - (clear: unit -> unit) - (create: int -> 'T -> unit) - (update: int -> 'T -> 'T -> unit) - (move: int -> int -> unit) - (remove: int -> unit) - = - - match prevCollOpt, collOpt with - | ValueNone, ValueNone -> () - | ValueSome prevColl, ValueSome newColl when identical prevColl newColl -> () - | ValueSome prevColl, ValueSome newColl when prevColl <> null && newColl <> null && prevColl.Length = 0 && newColl.Length = 0 -> () - | ValueSome _, ValueNone -> clear () - | ValueSome _, ValueSome coll when (coll = null || coll.Length = 0) -> clear () - | _, ValueSome coll -> - let currentState = match prevCollOpt with ValueSome x -> List(x) | _ -> List() - - let create newIndex newChild = - currentState.Insert(newIndex, newChild) - create newIndex newChild - - let move prevIndex newIndex = - let child = currentState.[prevIndex] - currentState.RemoveAt(prevIndex) - currentState.Insert(newIndex, child) - move prevIndex newIndex - - let remove index = - currentState.RemoveAt(index) - remove index - - // Separate the previous elements into 3 lists - // The ones whose instances have been reused (dependsOn) - // The ones whose keys have been reused - // The rest which can be reused by any other element - let identicalElements = HashSet<'T>() - let keyedElements = Dictionary() - let reusableElements = ResizeArray<'T>() - if prevCollOpt.IsSome then - for prevChild in prevCollOpt.Value do - if coll |> Array.exists (identical prevChild) then - identicalElements.Add(prevChild) |> ignore - else - let canReuseChildOf key = - coll - |> Array.exists (fun newChild -> - keyOf newChild = ValueSome key - && canReuse prevChild newChild - ) - - match keyOf prevChild with - | ValueSome key when canReuseChildOf key -> - keyedElements.Add(key, prevChild) - | _ -> - reusableElements.Add(prevChild) - - // Reuse the first element from reusableElements that returns true to canReuse - // Otherwise create a new element - let reuseOrCreate newIndex newChild = - match reusableElements |> Seq.tryFind(fun c -> canReuse c newChild) with - | Some prevChild -> - reusableElements.Remove prevChild |> ignore - - let prevIndex = currentState.IndexOf(prevChild) - update prevIndex prevChild newChild - - if prevIndex <> newIndex then - move prevIndex newIndex - - | None -> - create newIndex newChild - - for i in 0 .. coll.Length - 1 do - let newChild = coll.[i] - - // Check if the same instance was reused (dependsOn), if so just move the element to the correct index - if identicalElements.Contains(newChild) then - let prevIndex = currentState.IndexOf(newChild) - if prevIndex <> i then - move prevIndex i - else - // If the key existed previously, reuse the previous element - match keyOf newChild with - | ValueSome key when keyedElements.ContainsKey(key) -> - let prevChild = keyedElements.[key] - let prevIndex = currentState.IndexOf(prevChild) - update prevIndex prevChild newChild - - if prevIndex <> i then - move prevIndex i - - // Otherwise, reuse an old element if possible or create a new one - | _ -> - reuseOrCreate i newChild - - // If we still have old elements that were not reused, delete them - if reusableElements.Count > 0 then - for remainingElement in reusableElements do - let prevIndex = currentState.IndexOf(remainingElement) - remove prevIndex - - /// Incremental list maintenance: given a collection, and a previous version of that collection, perform - /// a reduced number of clear/add/remove/insert operations - let updateChildren - (prevCollOpt: ViewElement[] voption) - (collOpt: ViewElement[] voption) - (targetColl: IList<'TargetT>) - (create: ViewElement -> 'TargetT) - (update: ViewElement -> ViewElement -> 'TargetT -> unit) // Incremental element-wise update, only if element reuse is allowed - (attach: ViewElement voption -> ViewElement -> 'TargetT -> unit) // adjust attached properties - = - - let create index child = - let targetChild = create child - attach ValueNone child targetChild - targetColl.Insert(index, targetChild) - - let update index prevChild newChild = - let targetChild = targetColl.[index] - update prevChild newChild targetChild - attach (ValueSome prevChild) newChild targetChild - - let move prevIndex newIndex = - let targetChild = targetColl.[prevIndex] - targetColl.RemoveAt(prevIndex) - targetColl.Insert(newIndex, targetChild) - - updateChildrenInternal - prevCollOpt collOpt ViewHelpers.tryGetKey ViewHelpers.canReuseView - (fun () -> targetColl.Clear()) - create update move - (fun index -> targetColl.RemoveAt(index)) - - /// Update a control given the previous and new view elements - let inline updateChild (prevChild: ViewElement) (newChild: ViewElement) targetChild = - newChild.UpdateIncremental(prevChild, targetChild) - - /// Update the items of a TableSectionBase<'T> control, given previous and current view elements - let inline updateTableSectionBaseOfTItems<'T when 'T :> BindableObject> prevCollOpt collOpt (target: TableSectionBase<'T>) attach = - updateChildren prevCollOpt collOpt target (fun c -> c.Create() :?> 'T) updateChild attach - - /// Update the items of a Shell, given previous and current view elements - let inline updateShellItems prevCollOpt collOpt (target: Shell) attach = - let createChild (desc: ViewElement) = - match desc.Create() with - | :? ShellContent as shellContent -> ShellItem.op_Implicit shellContent - | :? TemplatedPage as templatedPage -> ShellItem.op_Implicit templatedPage - | :? ShellSection as shellSection -> ShellItem.op_Implicit shellSection - | :? MenuItem as menuItem -> ShellItem.op_Implicit menuItem - | :? ShellItem as shellItem -> shellItem - | child -> failwithf "%s is not compatible with the type ShellItem" (child.GetType().Name) - - let updateChild prevViewElement (currViewElement: ViewElement) (target: ShellItem) = - let realTarget = - match currViewElement.TargetType with - | t when t = typeof -> target.Items.[0].Items.[0] :> Element - | t when t = typeof -> target.Items.[0].Items.[0] :> Element - | t when t = typeof -> target.Items.[0] :> Element - | t when t = typeof -> target.GetType().GetProperty("MenuItem").GetValue(target) :?> Element // MenuShellItem is marked as internal - | _ -> target :> Element - updateChild prevViewElement currViewElement realTarget - - updateChildren prevCollOpt collOpt target.Items createChild updateChild attach - - /// Update the menu items of a ShellContent, given previous and current view elements - let inline updateShellContentMenuItems prevCollOpt collOpt (target: ShellContent) = - updateChildren prevCollOpt collOpt target.MenuItems (fun c -> c.Create() :?> MenuItem) updateChild (fun _ _ _ -> ()) - - /// Update the items of a ShellItem, given previous and current view elements - let inline updateShellItemItems prevCollOpt collOpt (target: ShellItem) attach = - let createChild (desc: ViewElement) = - match desc.Create() with - | :? ShellContent as shellContent -> ShellSection.op_Implicit shellContent - | :? TemplatedPage as templatedPage -> ShellSection.op_Implicit templatedPage - | :? ShellSection as shellSection -> shellSection - | child -> failwithf "%s is not compatible with the type ShellSection" (child.GetType().Name) - - let updateChild prevViewElement (currViewElement: ViewElement) (target: ShellSection) = - let realTarget = - match currViewElement.TargetType with - | t when t = typeof -> target.Items.[0] :> BaseShellItem - | t when t = typeof -> target.Items.[0] :> BaseShellItem - | _ -> target :> BaseShellItem - updateChild prevViewElement currViewElement realTarget - - updateChildren prevCollOpt collOpt target.Items createChild updateChild attach - - /// Update the items of a ShellSection, given previous and current view elements - let inline updateShellSectionItems prevCollOpt collOpt (target: ShellSection) attach = - updateChildren prevCollOpt collOpt target.Items (fun c -> c.Create() :?> ShellContent) updateChild attach - - /// Update the items of a SwipeItems, given previous and current view elements - let inline updateSwipeItems prevCollOpt collOpt (target: SwipeItems) = - updateChildren prevCollOpt collOpt target (fun c -> c.Create() :?> ISwipeItem) updateChild (fun _ _ _ -> ()) - - /// Update the children of a Menu, given previous and current view elements - let inline updateMenuChildren prevCollOpt collOpt (target: Menu) attach = - updateChildren prevCollOpt collOpt target (fun c -> c.Create() :?> Menu) updateChild attach - - /// Update the effects of an Element, given previous and current view elements - let inline updateElementEffects prevCollOpt collOpt (target: Element) attach = - let createChild (desc: ViewElement) = - match desc.Create() with - | :? CustomEffect as customEffect -> Effect.Resolve(customEffect.Name) - | effect -> effect :?> Effect - - updateChildren prevCollOpt collOpt target.Effects createChild updateChild attach - - /// Update the toolbar items of a Page, given previous and current view elements - let inline updatePageToolbarItems prevCollOpt collOpt (target: Page) attach = - updateChildren prevCollOpt collOpt target.ToolbarItems (fun c -> c.Create() :?> ToolbarItem) updateChild attach - diff --git a/Fabulous.XamarinForms/src/Fabulous.XamarinForms.Core/Collections.fs b/Fabulous.XamarinForms/src/Fabulous.XamarinForms.Core/Collections.fs new file mode 100644 index 000000000..6ec33a626 --- /dev/null +++ b/Fabulous.XamarinForms/src/Fabulous.XamarinForms.Core/Collections.fs @@ -0,0 +1,383 @@ +namespace Fabulous.XamarinForms + +open Fabulous +open System.Collections +open System.Collections.Generic +open System.Collections.ObjectModel +open Xamarin.Forms + +/// This module contains the update logic for the controls with children +module Collections = + type Operation<'T> = + | Insert of index: int * element: 'T + | Move of oldIndex: int * newIndex: int + | Update of index: int * prev: 'T * curr: 'T + | MoveAndUpdate of oldIndex: int * prev: 'T * newIndex: int * curr: 'T + | Delete of oldIndex: int + + type DiffResult<'T> = + | NoChange + | ClearCollection + | Operations of Operation<'T> list + + /// Returns a list of operations to apply to go from the initial list to the new list + /// + /// The algorithm will try in priority to update elements sharing the same instance (usually achieved with dependsOn) + /// or sharing the same key. All other elements will try to reuse previous elements where possible. + /// If no reuse is possible, the element will create a new control. + /// + /// In aggressive reuse mode, the algorithm will try to reuse any reusable element. + /// In the non-aggressive reuse mode, the algorithm will try to reuse a reusable element only if it is at the same index + let diff<'T when 'T : equality> + (aggressiveReuseMode: bool) + (prevCollOpt: 'T[] voption) + (collOpt: 'T[] voption) + (keyOf: 'T -> string voption) + (canReuse: 'T -> 'T -> bool) + = + + match prevCollOpt, collOpt with + | ValueNone, ValueNone -> NoChange + | ValueSome prevColl, ValueSome newColl when identical prevColl newColl -> NoChange + | ValueSome prevColl, ValueSome newColl when prevColl <> null && newColl <> null && prevColl.Length = 0 && newColl.Length = 0 -> NoChange + | ValueSome _, ValueNone -> ClearCollection + | ValueSome _, ValueSome coll when (coll = null || coll.Length = 0) -> ClearCollection + | _, ValueSome coll -> + // Separate the previous elements into 4 lists + // The ones whose instances have been reused (dependsOn) + // The ones whose keys have been reused and should be updated + // The ones whose keys have not been reused and should be discarded + // The rest which can be reused by any other element + let identicalElements = Dictionary<'T, int>() + let keyedElements = Dictionary() + let reusableElements = ResizeArray() + let discardedElements = ResizeArray() + if prevCollOpt.IsSome && prevCollOpt.Value.Length > 0 then + for prevIndex in 0 .. prevCollOpt.Value.Length - 1 do + let prevChild = prevCollOpt.Value.[prevIndex] + if coll |> Array.exists (identical prevChild) then + identicalElements.Add(prevChild, prevIndex) |> ignore + else + let canReuseChildOf key = + coll + |> Array.exists (fun newChild -> + keyOf newChild = ValueSome key + && canReuse prevChild newChild + ) + + match keyOf prevChild with + | ValueSome key when canReuseChildOf key -> + keyedElements.Add(key, (prevIndex, prevChild)) + | ValueNone -> + reusableElements.Add((prevIndex, prevChild)) + | ValueSome _ -> + discardedElements.Add(prevIndex) + + let operations = + [ for i in 0 .. coll.Length - 1 do + let newChild = coll.[i] + + // Check if the same instance was reused (dependsOn), if so just move the element to the correct index + match identicalElements.TryGetValue(newChild) with + | (true, prevIndex) -> + if prevIndex <> i then yield Move (prevIndex, i) + | _ -> + // If the key existed previously, reuse the previous element + match keyOf newChild with + | ValueSome key when keyedElements.ContainsKey(key) -> + let prevIndex, prevChild = keyedElements.[key] + if prevIndex <> i then + yield MoveAndUpdate (prevIndex, prevChild, i, newChild) + else + yield Update (i, prevChild, newChild) + + // Otherwise, reuse an old element if possible or create a new one + | _ -> + let isMatch (index, reusableChild) = + canReuse reusableChild newChild + && (aggressiveReuseMode || index = i) + + match reusableElements |> Seq.tryFind isMatch with + | Some ((prevIndex, prevChild) as item) -> + reusableElements.Remove item |> ignore + if prevIndex <> i then + yield MoveAndUpdate (prevIndex, prevChild, i, newChild) + else + yield Update (i, prevChild, newChild) + + | None -> + yield Insert (i, newChild) + + // If we have discarded elements, delete them + if discardedElements.Count > 0 then + for prevIndex in discardedElements do + yield Delete prevIndex + + // If we still have old elements that were not reused, delete them + if reusableElements.Count > 0 then + for prevIndex, _ in reusableElements do + yield Delete prevIndex ] + + if operations.Length = 0 then + NoChange + else + Operations operations + + /// Reduces the operations of the DiffResult to be applicable to an ObservableCollection. + /// + /// diff returns all the operations to move from List A to List B. + /// Except with ObservableCollection, we're forced to apply the changes one after the other, changing the indices + /// So this algorithm compensates this offsetting + let adaptDiffForObservableCollection (prevCollLength: int) (diffResult: DiffResult<'T>) : DiffResult<'T> = + match diffResult with + | NoChange -> NoChange + | ClearCollection -> ClearCollection + | Operations operations -> + let prevIndices = Array.init prevCollLength id + + // Shift all old indices by 1 (down the list) on insert after the inserted position + let shiftForInsert index = + for i in 0 .. prevIndices.Length - 1 do + if prevIndices.[i] >= index then + prevIndices.[i] <- prevIndices.[i] + 1 + + // Shift all old indices by -1 (up the list) on delete after the deleted position + let shiftForDelete originalIndexInPrevColl prevIndex = + for i in 0 .. prevIndices.Length - 1 do + if prevIndices.[i] > prevIndex then + prevIndices.[i] <- prevIndices.[i] - 1 + prevIndices.[originalIndexInPrevColl] <- -1 + + // Shift all old indices between the previous and new position on move + let shiftForMove originalIndexInPrevColl prevIndex newIndex = + for i in 0 .. prevIndices.Length - 1 do + if prevIndex < prevIndices.[i] && prevIndices.[i] <= newIndex then + prevIndices.[i] <- prevIndices.[i] - 1 + else if newIndex <= prevIndices.[i] && prevIndices.[i] < prevIndex then + prevIndices.[i] <- prevIndices.[i] + 1 + prevIndices.[originalIndexInPrevColl] <- newIndex + + // Return an update operation preceded by a move only if actual indices don't match + let moveAndUpdate oldIndex prev newIndex curr = + let prevIndex = prevIndices.[oldIndex] + if prevIndex = newIndex then + Update (newIndex, prev, curr) + else + shiftForMove oldIndex prevIndex newIndex + MoveAndUpdate (prevIndex, prev, newIndex, curr) + + let operations = + [ for op in operations do + match op with + | Insert (index, element) -> + yield Insert (index, element) + shiftForInsert index + + | Move (oldIndex, newIndex) -> + // Prevent a move if the actual indices match + let prevIndex = prevIndices.[oldIndex] + if prevIndex <> newIndex then + yield (Move (prevIndex, newIndex)) + shiftForMove oldIndex prevIndex newIndex + + | Update (index, prev, curr) -> + yield moveAndUpdate index prev index curr + + | MoveAndUpdate (oldIndex, prev, newIndex, curr) -> + yield moveAndUpdate oldIndex prev newIndex curr + + | Delete oldIndex -> + let prevIndex = prevIndices.[oldIndex] + yield Delete prevIndex + shiftForDelete oldIndex prevIndex ] + + if operations.Length = 0 then + NoChange + else + Operations operations + + /// Incremental list maintenance: given a collection, and a previous version of that collection, perform + /// a reduced number of clear/add/remove/insert operations + let updateCollection + (aggressiveReuseMode: bool) + (prevCollOpt: 'T[] voption) + (collOpt: 'T[] voption) + (targetColl: IList<'TargetT>) + (keyOf: 'T -> string voption) + (canReuse: 'T -> 'T -> bool) + (create: 'T -> 'TargetT) + (update: 'T -> 'T -> 'TargetT -> unit) // Incremental element-wise update, only if element reuse is allowed + (attach: 'T voption -> 'T -> 'TargetT -> unit) // adjust attached properties + = + + let diffResult = + diff aggressiveReuseMode prevCollOpt collOpt keyOf canReuse + |> adaptDiffForObservableCollection (match prevCollOpt with ValueNone -> 0 | ValueSome c -> c.Length) + + match diffResult with + | NoChange -> () + | ClearCollection -> targetColl.Clear() + | Operations operations -> + for op in operations do + match op with + | Insert (index, element) -> + let child = create element + attach ValueNone element child + targetColl.Insert(index, child) + + | Move (prevIndex, newIndex) -> + let child = targetColl.[prevIndex] + targetColl.RemoveAt(prevIndex) + targetColl.Insert(newIndex, child) + + | Update (index, prev, curr) -> + let child = targetColl.[index] + update prev curr child + attach (ValueSome prev) curr child + + | MoveAndUpdate (prevIndex, prev, newIndex, curr) -> + let child = targetColl.[prevIndex] + targetColl.RemoveAt(prevIndex) + targetColl.Insert(newIndex, child) + update prev curr child + attach (ValueSome prev) curr child + + | Delete index -> + targetColl.RemoveAt(index) |> ignore + + let updateChildren prevCollOpt collOpt target create update attach = + updateCollection true prevCollOpt collOpt target ViewHelpers.tryGetKey ViewHelpers.canReuseView create update attach + + let updateItems prevCollOpt collOpt target keyOf canReuse create update attach = + updateCollection false prevCollOpt collOpt target keyOf canReuse create update attach + + /// Update a control given the previous and new view elements + let inline updateChild (prevChild: ViewElement) (newChild: ViewElement) targetChild = + newChild.UpdateIncremental(prevChild, targetChild) + + /// Update the items of a TableSectionBase<'T> control, given previous and current view elements + let inline updateTableSectionBaseOfTItems<'T when 'T :> BindableObject> prevCollOpt collOpt (target: TableSectionBase<'T>) attach = + updateChildren prevCollOpt collOpt target (fun c -> c.Create() :?> 'T) updateChild attach + + /// Update the items of a Shell, given previous and current view elements + let inline updateShellItems prevCollOpt collOpt (target: Shell) attach = + let createChild (desc: ViewElement) = + match desc.Create() with + | :? ShellContent as shellContent -> ShellItem.op_Implicit shellContent + | :? TemplatedPage as templatedPage -> ShellItem.op_Implicit templatedPage + | :? ShellSection as shellSection -> ShellItem.op_Implicit shellSection + | :? MenuItem as menuItem -> ShellItem.op_Implicit menuItem + | :? ShellItem as shellItem -> shellItem + | child -> failwithf "%s is not compatible with the type ShellItem" (child.GetType().Name) + + let updateChild prevViewElement (currViewElement: ViewElement) (target: ShellItem) = + let realTarget = + match currViewElement.TargetType with + | t when t = typeof -> target.Items.[0].Items.[0] :> Element + | t when t = typeof -> target.Items.[0].Items.[0] :> Element + | t when t = typeof -> target.Items.[0] :> Element + | t when t = typeof -> target.GetType().GetProperty("MenuItem").GetValue(target) :?> Element // MenuShellItem is marked as internal + | _ -> target :> Element + updateChild prevViewElement currViewElement realTarget + + updateChildren prevCollOpt collOpt target.Items createChild updateChild attach + + /// Update the menu items of a ShellContent, given previous and current view elements + let inline updateShellContentMenuItems prevCollOpt collOpt (target: ShellContent) = + updateChildren prevCollOpt collOpt target.MenuItems (fun c -> c.Create() :?> MenuItem) updateChild (fun _ _ _ -> ()) + + /// Update the items of a ShellItem, given previous and current view elements + let inline updateShellItemItems prevCollOpt collOpt (target: ShellItem) attach = + let createChild (desc: ViewElement) = + match desc.Create() with + | :? ShellContent as shellContent -> ShellSection.op_Implicit shellContent + | :? TemplatedPage as templatedPage -> ShellSection.op_Implicit templatedPage + | :? ShellSection as shellSection -> shellSection + | child -> failwithf "%s is not compatible with the type ShellSection" (child.GetType().Name) + + let updateChild prevViewElement (currViewElement: ViewElement) (target: ShellSection) = + let realTarget = + match currViewElement.TargetType with + | t when t = typeof -> target.Items.[0] :> BaseShellItem + | t when t = typeof -> target.Items.[0] :> BaseShellItem + | _ -> target :> BaseShellItem + updateChild prevViewElement currViewElement realTarget + + updateChildren prevCollOpt collOpt target.Items createChild updateChild attach + + /// Update the items of a ShellSection, given previous and current view elements + let inline updateShellSectionItems prevCollOpt collOpt (target: ShellSection) attach = + updateChildren prevCollOpt collOpt target.Items (fun c -> c.Create() :?> ShellContent) updateChild attach + + /// Update the items of a SwipeItems, given previous and current view elements + let inline updateSwipeItems prevCollOpt collOpt (target: SwipeItems) = + updateChildren prevCollOpt collOpt target (fun c -> c.Create() :?> ISwipeItem) updateChild (fun _ _ _ -> ()) + + /// Update the children of a Menu, given previous and current view elements + let inline updateMenuChildren prevCollOpt collOpt (target: Menu) attach = + updateChildren prevCollOpt collOpt target (fun c -> c.Create() :?> Menu) updateChild attach + + /// Update the effects of an Element, given previous and current view elements + let inline updateElementEffects prevCollOpt collOpt (target: Element) attach = + let createChild (desc: ViewElement) = + match desc.Create() with + | :? CustomEffect as customEffect -> Effect.Resolve(customEffect.Name) + | effect -> effect :?> Effect + + updateChildren prevCollOpt collOpt target.Effects createChild updateChild attach + + /// Update the toolbar items of a Page, given previous and current view elements + let inline updatePageToolbarItems prevCollOpt collOpt (target: Page) attach = + updateChildren prevCollOpt collOpt target.ToolbarItems (fun c -> c.Create() :?> ToolbarItem) updateChild attach + + let inline updateViewElementHolderItems (prevCollOpt: ViewElement[] voption) (collOpt: ViewElement[] voption) (targetColl: IList) = + updateItems prevCollOpt collOpt targetColl + ViewHelpers.tryGetKey ViewHelpers.canReuseView + ViewElementHolder (fun _ curr holder -> holder.ViewElement <- curr) + + let inline getCollection<'T> (coll: IEnumerable) (set: ObservableCollection<'T> -> unit) = + match coll with + | :? ObservableCollection<'T> as oc -> oc + | _ -> let oc = ObservableCollection<'T>() in set oc; oc + + /// Update the items in a ItemsView control, given previous and current view elements + let inline updateItemsViewItems prevCollOpt collOpt (target: ItemsView) = + let targetColl = getCollection target.ItemsSource (fun oc -> target.ItemsSource <- oc) + updateViewElementHolderItems prevCollOpt collOpt targetColl (fun _ _ _ -> ()) + + /// Update the items in a ItemsView<'T> control, given previous and current view elements + let inline updateItemsViewOfTItems<'T when 'T :> BindableObject> prevCollOpt collOpt (target: ItemsView<'T>) = + let targetColl = getCollection target.ItemsSource (fun oc -> target.ItemsSource <- oc) + updateViewElementHolderItems prevCollOpt collOpt targetColl (fun _ _ _ -> ()) + + /// Update the items in a SearchHandler control, given previous and current view elements + let inline updateSearchHandlerItems _ collOpt (target: SearchHandler) = + let targetColl = List() + updateViewElementHolderItems ValueNone collOpt targetColl (fun _ _ _ -> ()) + target.ItemsSource <- targetColl + + /// Update the items in a GroupedListView control, given previous and current view elements + let inline updateListViewGroupedItems (prevCollOpt: (string * ViewElement * ViewElement[])[] voption) (collOpt: (string * ViewElement * ViewElement[])[] voption) (target: ListView) = + let updateViewElementHolderGroup (_prevShortName: string, _prevKey, prevColl: ViewElement[]) (currShortName: string, currKey, currColl: ViewElement[]) (target: ViewElementHolderGroup) = + target.ShortName <- currShortName + target.ViewElement <- currKey + updateViewElementHolderItems (ValueSome prevColl) (ValueSome currColl) target (fun _ _ _ -> ()) + + let targetColl = getCollection target.ItemsSource (fun oc -> target.ItemsSource <- oc) + updateItems prevCollOpt collOpt targetColl + (fun (key, _, _) -> ValueSome key) + (fun (_, prevHeader, _) (_, currHeader, _) -> ViewHelpers.canReuseView prevHeader currHeader) + ViewElementHolderGroup updateViewElementHolderGroup + (fun _ _ _ -> ()) + + /// Update the selected items in a SelectableItemsView control, given previous and current indexes + let inline updateSelectableItemsViewSelectedItems (prevCollOptOpt: int[] option voption) (collOptOpt: int[] option voption) (target: SelectableItemsView) = + let prevCollOpt = match prevCollOptOpt with ValueNone | ValueSome None -> ValueNone | ValueSome (Some x) -> ValueSome x + let collOpt = match collOptOpt with ValueNone | ValueSome None -> ValueNone | ValueSome (Some x) -> ValueSome x + let targetColl = getCollection target.SelectedItems (fun oc -> target.SelectedItems <- oc) + + let findItem idx = + let itemsSource = target.ItemsSource :?> IList + itemsSource.[idx] :> obj + + updateItems prevCollOpt collOpt targetColl (fun _ -> ValueNone) (fun x y -> x = y) findItem (fun _ _ _ -> ()) (fun _ _ _ -> ()) \ No newline at end of file diff --git a/Fabulous.XamarinForms/src/Fabulous.XamarinForms.Core/Fabulous.XamarinForms.Core.fsproj b/Fabulous.XamarinForms/src/Fabulous.XamarinForms.Core/Fabulous.XamarinForms.Core.fsproj index 29204738c..5f7b6f9c0 100644 --- a/Fabulous.XamarinForms/src/Fabulous.XamarinForms.Core/Fabulous.XamarinForms.Core.fsproj +++ b/Fabulous.XamarinForms/src/Fabulous.XamarinForms.Core/Fabulous.XamarinForms.Core.fsproj @@ -11,8 +11,7 @@ - - + diff --git a/Fabulous.XamarinForms/src/Fabulous.XamarinForms.Core/ItemsUpdaters.fs b/Fabulous.XamarinForms/src/Fabulous.XamarinForms.Core/ItemsUpdaters.fs deleted file mode 100644 index a699b3982..000000000 --- a/Fabulous.XamarinForms/src/Fabulous.XamarinForms.Core/ItemsUpdaters.fs +++ /dev/null @@ -1,205 +0,0 @@ -namespace Fabulous.XamarinForms - -open Fabulous -open System.Collections -open System.Collections.Generic -open System.Collections.ObjectModel -open Xamarin.Forms - -/// This module contains the update logic for the controls with a virtualized ItemsSource -module ItemsUpdaters = - /// Incremental list maintenance: given a collection, and a previous version of that collection, perform - /// a reduced number of clear/move/create/update/remove operations - /// - /// The algorithm will try in priority to update elements sharing the same instance (usually achieved with dependsOn) - /// or sharing the same key. All other elements will be reused only if they are still at the same index. - /// If no reuse is possible, the element will create a new control. - let updateItemsInternal - (prevCollOpt: 'T[] voption) - (collOpt: 'T[] voption) - (keyOf: 'T -> string voption) - (canReuse: 'T -> 'T -> bool) - (clear: unit -> unit) - (create: int -> 'T -> unit) - (update: int -> 'T -> 'T -> unit) - (move: int -> int -> unit) - (remove: int -> unit) - = - - match prevCollOpt, collOpt with - | ValueNone, ValueNone -> () - | ValueSome prevColl, ValueSome newColl when identical prevColl newColl -> () - | ValueSome prevColl, ValueSome newColl when prevColl <> null && newColl <> null && prevColl.Length = 0 && newColl.Length = 0 -> () - | ValueSome _, ValueNone -> clear () - | ValueSome _, ValueSome coll when (coll = null || coll.Length = 0) -> clear () - | _, ValueSome coll -> - let currentState = match prevCollOpt with ValueSome x -> List(x) | _ -> List() - - let create newIndex newChild = - currentState.Insert(newIndex, newChild) - create newIndex newChild - - let move prevIndex newIndex = - let child = currentState.[prevIndex] - currentState.RemoveAt(prevIndex) - currentState.Insert(newIndex, child) - move prevIndex newIndex - - let remove index = - currentState.RemoveAt(index) - remove index - - // Separate the previous elements into 2 lists - // The ones whose instances have been reused (dependsOn) - // The ones whose keys have been reused - let identicalElements = HashSet<'T>() - let keyedElements = Dictionary() - if prevCollOpt.IsSome then - for prevChild in prevCollOpt.Value do - if coll |> Array.exists (identical prevChild) then - identicalElements.Add(prevChild) |> ignore - else - let canReuseChildOf key = - coll - |> Array.exists (fun newChild -> - keyOf newChild = ValueSome key - && canReuse prevChild newChild - ) - - match keyOf prevChild with - | ValueSome key when canReuseChildOf key -> - keyedElements.Add(key, prevChild) - | _ -> () - - // Reuse the element from the same index if possible (not already reused and reusable) - // Otherwise create a new element - let reuseOrCreate index newChild = - if prevCollOpt.IsSome && prevCollOpt.Value.Length > index then - let prevChild = prevCollOpt.Value.[index] - let key = keyOf prevChild - - // Reuse the previous element at the same index only if it was not reused elsewhere - if not (identicalElements.Contains(prevChild)) - && (key.IsNone || not (keyedElements.ContainsKey(key.Value))) - && (canReuse prevChild newChild) then - update index prevChild newChild - - else - create index newChild - else - create index newChild - - for i in 0 .. coll.Length - 1 do - let newChild = coll.[i] - - // Check if the same instance was reused (dependsOn), if so just move the element to the correct index - if identicalElements.Contains(newChild) then - let prevIndex = currentState.IndexOf(newChild) - if prevIndex <> i then - move prevIndex i - else - // If the key existed previously, reuse the previous element - match keyOf newChild with - | ValueSome key when keyedElements.ContainsKey(key) -> - let prevChild = keyedElements.[key] - let prevIndex = currentState.IndexOf(prevChild) - update prevIndex prevChild newChild - - if prevIndex <> i then - move prevIndex i - - // Otherwise, reuse an old element if possible or create a new one - | _ -> - reuseOrCreate i newChild - - // Remove all the excess elements - if prevCollOpt.IsSome && prevCollOpt.Value.Length > coll.Length then - while currentState.Count > coll.Length do - remove (currentState.Count - 1) - - let updateItems - (prevCollOpt: 'T[] voption) - (collOpt: 'T[] voption) - (targetColl: IList<'TargetT>) - (keyOf: 'T -> string voption) - (canReuse: 'T -> 'T -> bool) - (create: 'T -> 'TargetT) - (update: 'T -> 'T -> 'TargetT -> unit) - (attach: 'T voption -> 'T -> 'TargetT -> unit) // adjust attached properties - = - - let create index child = - let targetChild = create child - attach ValueNone child targetChild - if targetColl.Count > index then - targetColl.[index] <- targetChild - else - targetColl.Insert(index, targetChild) - - let update index prevChild newChild = - let targetChild = targetColl.[index] - update prevChild newChild targetChild - attach (ValueSome prevChild) newChild targetChild - - let move prevIndex newIndex = - let targetChild = targetColl.[prevIndex] - targetColl.RemoveAt(prevIndex) - targetColl.Insert(newIndex, targetChild) - - updateItemsInternal - prevCollOpt collOpt keyOf canReuse - (fun () -> targetColl.Clear()) - create update move - (fun index -> targetColl.RemoveAt(index)) - - let inline updateViewElementHolderItems (prevCollOpt: ViewElement[] voption) (collOpt: ViewElement[] voption) (targetColl: IList) = - updateItems prevCollOpt collOpt targetColl - ViewHelpers.tryGetKey ViewHelpers.canReuseView - ViewElementHolder (fun _ curr holder -> holder.ViewElement <- curr) - - let inline getCollection<'T> (coll: IEnumerable) (set: ObservableCollection<'T> -> unit) = - match coll with - | :? ObservableCollection<'T> as oc -> oc - | _ -> let oc = ObservableCollection<'T>() in set oc; oc - - /// Update the items in a ItemsView control, given previous and current view elements - let inline updateItemsViewItems prevCollOpt collOpt (target: ItemsView) = - let targetColl = getCollection target.ItemsSource (fun oc -> target.ItemsSource <- oc) - updateViewElementHolderItems prevCollOpt collOpt targetColl (fun _ _ _ -> ()) - - /// Update the items in a ItemsView<'T> control, given previous and current view elements - let inline updateItemsViewOfTItems<'T when 'T :> BindableObject> prevCollOpt collOpt (target: ItemsView<'T>) = - let targetColl = getCollection target.ItemsSource (fun oc -> target.ItemsSource <- oc) - updateViewElementHolderItems prevCollOpt collOpt targetColl (fun _ _ _ -> ()) - - /// Update the items in a SearchHandler control, given previous and current view elements - let inline updateSearchHandlerItems _ collOpt (target: SearchHandler) = - let targetColl = List() - updateViewElementHolderItems ValueNone collOpt targetColl (fun _ _ _ -> ()) - target.ItemsSource <- targetColl - - /// Update the items in a GroupedListView control, given previous and current view elements - let inline updateListViewGroupedItems (prevCollOpt: (string * ViewElement * ViewElement[])[] voption) (collOpt: (string * ViewElement * ViewElement[])[] voption) (target: ListView) = - let updateViewElementHolderGroup (_prevShortName: string, _prevKey, prevColl: ViewElement[]) (currShortName: string, currKey, currColl: ViewElement[]) (target: ViewElementHolderGroup) = - target.ShortName <- currShortName - target.ViewElement <- currKey - updateViewElementHolderItems (ValueSome prevColl) (ValueSome currColl) target (fun _ _ _ -> ()) - - let targetColl = getCollection target.ItemsSource (fun oc -> target.ItemsSource <- oc) - updateItems prevCollOpt collOpt targetColl - (fun (key, _, _) -> ValueSome key) - (fun (_, prevHeader, _) (_, currHeader, _) -> ViewHelpers.canReuseView prevHeader currHeader) - ViewElementHolderGroup updateViewElementHolderGroup - (fun _ _ _ -> ()) - - /// Update the selected items in a SelectableItemsView control, given previous and current indexes - let inline updateSelectableItemsViewSelectedItems (prevCollOptOpt: int[] option voption) (collOptOpt: int[] option voption) (target: SelectableItemsView) = - let prevCollOpt = match prevCollOptOpt with ValueNone | ValueSome None -> ValueNone | ValueSome (Some x) -> ValueSome x - let collOpt = match collOptOpt with ValueNone | ValueSome None -> ValueNone | ValueSome (Some x) -> ValueSome x - let targetColl = getCollection target.SelectedItems (fun oc -> target.SelectedItems <- oc) - - let findItem idx = - let itemsSource = target.ItemsSource :?> IList - itemsSource.[idx] :> obj - - updateItems prevCollOpt collOpt targetColl (fun _ -> ValueNone) (fun x y -> x = y) findItem (fun _ _ _ -> ()) (fun _ _ _ -> ()) diff --git a/Fabulous.XamarinForms/src/Fabulous.XamarinForms.Core/ViewExtensions.fs b/Fabulous.XamarinForms/src/Fabulous.XamarinForms.Core/ViewExtensions.fs index 43deefd82..13cc91b7b 100644 --- a/Fabulous.XamarinForms/src/Fabulous.XamarinForms.Core/ViewExtensions.fs +++ b/Fabulous.XamarinForms/src/Fabulous.XamarinForms.Core/ViewExtensions.fs @@ -58,6 +58,6 @@ module ViewExtensions = let prevCollOpt = match prevOpt with ValueNone -> ValueNone | ValueSome prev -> prev.TryGetAttributeKeyed<_>(attribKey) let collOpt = source.TryGetAttributeKeyed<_>(attribKey) - ChildrenUpdaters.updateChildren + Collections.updateChildren (ValueOption.map Seq.toArray prevCollOpt) (ValueOption.map Seq.toArray collOpt) targetCollection - (fun x -> x.Create() :?> 'T) ChildrenUpdaters.updateChild (fun _ _ _ -> ()) + (fun x -> x.Create() :?> 'T) Collections.updateChild (fun _ _ _ -> ()) diff --git a/Fabulous.XamarinForms/src/Fabulous.XamarinForms/Fabulous.XamarinForms.fsproj b/Fabulous.XamarinForms/src/Fabulous.XamarinForms/Fabulous.XamarinForms.fsproj index 12dca7dfe..a0d4014b4 100644 --- a/Fabulous.XamarinForms/src/Fabulous.XamarinForms/Fabulous.XamarinForms.fsproj +++ b/Fabulous.XamarinForms/src/Fabulous.XamarinForms/Fabulous.XamarinForms.fsproj @@ -13,8 +13,7 @@ - - + diff --git a/Fabulous.XamarinForms/src/Fabulous.XamarinForms/Xamarin.Forms.Core.json b/Fabulous.XamarinForms/src/Fabulous.XamarinForms/Xamarin.Forms.Core.json index 79ee33c21..21fbd9468 100644 --- a/Fabulous.XamarinForms/src/Fabulous.XamarinForms/Xamarin.Forms.Core.json +++ b/Fabulous.XamarinForms/src/Fabulous.XamarinForms/Xamarin.Forms.Core.json @@ -92,7 +92,7 @@ }, { "source": "Effects", - "updateCode": "ChildrenUpdaters.updateElementEffects", + "updateCode": "Collections.updateElementEffects", "collection": { "elementType": "Xamarin.Forms.Effect" } @@ -122,7 +122,7 @@ "source": null, "name": "Children", "inputType": "ViewElement list", - "updateCode": "ChildrenUpdaters.updateMenuChildren", + "updateCode": "Collections.updateMenuChildren", "collection": { "elementType": "Xamarin.Forms.Menu" } @@ -665,7 +665,7 @@ { "name": "MenuItems", "inputType": "ViewElement list", - "updateCode": "ChildrenUpdaters.updateShellContentMenuItems" + "updateCode": "Collections.updateShellContentMenuItems" } ], "primaryConstructorMembers": [ @@ -690,7 +690,7 @@ { "source": "Items", "inputType": "ViewElement list", - "updateCode": "ChildrenUpdaters.updateShellItemItems" + "updateCode": "Collections.updateShellItemItems" } ], "primaryConstructorMembers": [ @@ -718,7 +718,7 @@ { "source": "Items", "inputType": "ViewElement list", - "updateCode": "ChildrenUpdaters.updateShellSectionItems" + "updateCode": "Collections.updateShellSectionItems" } ], "primaryConstructorMembers": [ @@ -867,7 +867,7 @@ "source": null, "name": "ToolbarItems", "inputType": "ViewElement list", - "updateCode": "ChildrenUpdaters.updatePageToolbarItems", + "updateCode": "Collections.updatePageToolbarItems", "collection": { "elementType": "Xamarin.Forms.ToolbarItem" } @@ -1108,7 +1108,7 @@ "source": null, "name": "Items", "inputType": "ViewElement list", - "updateCode": "ChildrenUpdaters.updateShellItems", + "updateCode": "Collections.updateShellItems", "collection": { "elementType": "Xamarin.Forms.ShellItem" } @@ -1574,7 +1574,7 @@ "source": "ItemsSource", "shortName": "items", "inputType": "ViewElement list", - "updateCode": "ItemsUpdaters.updateItemsViewItems" + "updateCode": "Collections.updateItemsViewItems" }, { "source": "RemainingItemsThreshold" @@ -1691,7 +1691,7 @@ "inputType": "int list option", "modelType": "int array option", "convertInputToModel": "(Option.map Array.ofList)", - "updateCode": "ItemsUpdaters.updateSelectableItemsViewSelectedItems" + "updateCode": "Collections.updateSelectableItemsViewSelectedItems" }, { "source": "SelectionMode" @@ -1737,12 +1737,7 @@ "genericConstraint": "'T when 'T :> Xamarin.Forms.BindableObject", "canBeInstantiated": false, "properties": [ - { - "source": null, - "name": "Items", - "inputType": "ViewElement list", - "updateCode": "ItemsUpdaters.updateItemsViewOfTItems<'T>" - } + ] }, { @@ -1845,6 +1840,14 @@ { "type": "Fabulous.XamarinForms.CustomListView", "name": "ListView", + "properties": [ + { + "source": null, + "name": "Items", + "inputType": "ViewElement list", + "updateCode": "Collections.updateItemsViewOfTItems" + } + ], "primaryConstructorMembers": [ "Items" ] @@ -1860,7 +1863,7 @@ "inputType": "(string * ViewElement * ViewElement list) list", "modelType": "(string * ViewElement * ViewElement[])[]", "convertInputToModel": "(fun es -> es |> Array.ofList |> Array.map (fun (g, e, l) -> (g, e, Array.ofList l)))", - "updateCode": "ItemsUpdaters.updateListViewGroupedItems" + "updateCode": "Collections.updateListViewGroupedItems" }, { "source": null, @@ -2663,7 +2666,7 @@ "source": "ItemsSource", "shortName": "items", "inputType": "ViewElement list", - "updateCode": "ItemsUpdaters.updateSearchHandlerItems" + "updateCode": "Collections.updateSearchHandlerItems" }, { "source": "Keyboard" @@ -2750,7 +2753,7 @@ "source": null, "name": "Items", "inputType": "ViewElement list", - "updateCode": "ChildrenUpdaters.updateTableSectionBaseOfTItems<'T>", + "updateCode": "Collections.updateTableSectionBaseOfTItems<'T>", "collection": { "elementType": "'T" } @@ -2847,7 +2850,7 @@ "source": null, "name": "Items", "inputType": "ViewElement list", - "updateCode": "ChildrenUpdaters.updateSwipeItems" + "updateCode": "Collections.updateSwipeItems" } ], "events": [ diff --git a/Fabulous.XamarinForms/tests/Fabulous.XamarinForms.Tests/Collections/AdaptDiffTests.fs b/Fabulous.XamarinForms/tests/Fabulous.XamarinForms.Tests/Collections/AdaptDiffTests.fs new file mode 100644 index 000000000..0329af813 --- /dev/null +++ b/Fabulous.XamarinForms/tests/Fabulous.XamarinForms.Tests/Collections/AdaptDiffTests.fs @@ -0,0 +1,143 @@ +namespace Fabulous.XamarinForms.Tests.Collections + +open Fabulous +open Fabulous.XamarinForms +open Fabulous.XamarinForms.Collections +open NUnit.Framework +open FsUnit + +module AdaptDiffTests = + [] + let ``Test Reduce``() = + let previous = + [ View.Button(key = "Button0_0") + View.Button(key = "Button0_1") + View.Button(key = "Button0_2") + View.Button(key = "Button1_0") + View.Button(key = "Button1_1") + View.Button(key = "Button1_2") + View.Button(key = "Button2_0") + View.Button(key = "Button2_1") + View.Button(key = "Button2_2") ] + + let current = + [ View.Button(key = "Button0_0") + View.Button(key = "Button0_1") + View.Button(key = "Button0_2") + View.Button(key = "Button1_0") + View.Image(key = "Image1_1") + View.Button(key = "Button1_2") + View.Button(key = "Button2_0") + View.Button(key = "Button2_1") + View.Button(key = "Button2_2") ] + + let diffResult = DiffResult.Operations [ + Update (0, previous.[0], current.[0]) + Update (1, previous.[1], current.[1]) + Update (2, previous.[2], current.[2]) + Update (3, previous.[3], current.[3]) + Insert (4, current.[4]) + Update (5, previous.[5], current.[5]) + Update (6, previous.[6], current.[6]) + Update (7, previous.[7], current.[7]) + Update (8, previous.[8], current.[8]) + Delete 4 + ] + + Collections.adaptDiffForObservableCollection previous.Length diffResult + |> should equal (DiffResult.Operations [ + Update (0, previous.[0], current.[0]) + Update (1, previous.[1], current.[1]) + Update (2, previous.[2], current.[2]) + Update (3, previous.[3], current.[3]) + Insert (4, current.[4]) + MoveAndUpdate (6, previous.[5], 5, current.[5]) + MoveAndUpdate (7, previous.[6], 6,current.[6]) + MoveAndUpdate (8, previous.[7], 7, current.[7]) + MoveAndUpdate (9, previous.[8], 8, current.[8]) + Delete 9 + ]) + + [] + let ``Test Reduce 2``() = + let previous = + [ View.Button(key = "Button0_0") + View.Button(key = "Button0_1") + View.Button(key = "Button0_2") + View.Button(key = "Button1_0") + View.Image(key = "Image1_1") ] + + let current = + [ View.Label(key = "Button0_0") + View.Button(key = "Button0_1") + View.Image(key = "Button0_2") ] + + let diffResult = DiffResult.Operations [ + Insert (0, current.[0]) + Update (1, previous.[1], current.[1]) + MoveAndUpdate (4, previous.[4], 2, current.[2]) + Delete 0 + Delete 2 + Delete 3 + ] + + Collections.adaptDiffForObservableCollection previous.Length diffResult + |> should equal (DiffResult.Operations [ + Insert (0, current.[0]) + MoveAndUpdate (2, previous.[1], 1, current.[1]) + MoveAndUpdate (5, previous.[4], 2, current.[2]) + Delete 3 + Delete 3 + Delete 3 + ]) + + [] + let ``Test Reduce 3``() = + let previous = + [ View.Label(key = "0") + View.Label() + View.Button() + View.Button(key = "2") + View.Label() + View.Label(key = "3") + View.Label() + View.Button() + View.Button() + View.Button() + View.Button() ] + + let current = + [ View.Button(key = "0") + View.Label() ] + + let diffResult = DiffResult.Operations [ + MoveAndUpdate (2, previous.[2], 0, current.[0]) + MoveAndUpdate (0, previous.[0], 1, current.[1]) + Delete 1 + Delete 3 + Delete 4 + Delete 5 + Delete 6 + Delete 7 + Delete 8 + Delete 9 + Delete 10 + ] + + Collections.adaptDiffForObservableCollection previous.Length diffResult + |> should equal (DiffResult.Operations [ + MoveAndUpdate (2, previous.[2], 0, current.[0]) + Update (1, previous.[0], current.[1]) + Delete 2 + Delete 2 + Delete 2 + Delete 2 + Delete 2 + Delete 2 + Delete 2 + Delete 2 + Delete 2 + ]) + + + diff --git a/Fabulous.XamarinForms/tests/Fabulous.XamarinForms.Tests/ChildrenUpdaters/UpdateChildrenInternalTests.fs b/Fabulous.XamarinForms/tests/Fabulous.XamarinForms.Tests/Collections/DiffTests.fs similarity index 65% rename from Fabulous.XamarinForms/tests/Fabulous.XamarinForms.Tests/ChildrenUpdaters/UpdateChildrenInternalTests.fs rename to Fabulous.XamarinForms/tests/Fabulous.XamarinForms.Tests/Collections/DiffTests.fs index 6fbcfa031..f8add448d 100644 --- a/Fabulous.XamarinForms/tests/Fabulous.XamarinForms.Tests/ChildrenUpdaters/UpdateChildrenInternalTests.fs +++ b/Fabulous.XamarinForms/tests/Fabulous.XamarinForms.Tests/Collections/DiffTests.fs @@ -1,60 +1,32 @@ -namespace Fabulous.XamarinForms.Tests.ChildrenUpdaters +namespace Fabulous.XamarinForms.Tests.Collections open Fabulous -open System.Collections.Generic open Fabulous.XamarinForms +open Fabulous.XamarinForms.Collections open NUnit.Framework open FsUnit // 1 & 1' means its the same ViewElement (same property values), except it's not the same .NET reference // Tx & Ty means its 2 different ViewElement types that can't be reused between themselves (e.g. Label vs Button) // 1k/Txk means its a ViewElement with a key value -module UpdateChildrenInternalTests = - type Operation = - | Clear - | Create of index: int * newChild: ViewElement - | Update of index: int * prevChild: ViewElement * newChild: ViewElement - | Move of prevIndex: int * newIndex: int - | Remove of index: int - - /// Call updateChildrenInternal and accumulate all requested operations based on their index of effect - let private testUpdateChildren (previousCollection: ViewElement list voption) (newCollection: ViewElement list voption) = - let operations = List() - - let mockClear () = - operations.Add(Clear) +module DiffTests = + let testUpdateChildren prev curr = + let prevArray = + match prev with + | ValueNone -> ValueNone + | ValueSome list -> ValueSome (Array.ofList list) + let currArray = + match curr with + | ValueNone -> ValueNone + | ValueSome list -> ValueSome (Array.ofList list) + + Collections.diff true prevArray currArray (fun v -> v.TryGetKey()) (fun prev curr -> canReuseView prev curr) - let mockCreate index child = - operations.Add(Create (index, child)) - - let mockUpdate index previousChild newChild = - operations.Add(Update (index, previousChild, newChild)) - - let mockMove prevIndex newIndex = - operations.Add(Move (prevIndex, newIndex)) - - let mockRemove index = - operations.Add(Remove index) - - do ChildrenUpdaters.updateChildrenInternal - (previousCollection |> ValueOption.map List.toArray) - (newCollection |> ValueOption.map List.toArray) - ViewHelpers.tryGetKey - ViewHelpers.canReuseView - mockClear - mockCreate - mockUpdate - mockMove - mockRemove - - operations - |> Seq.toArray - /// Going from an undefined state to another undefined state should do nothing [] - let ``Given previous state = None / current state = None, updateChildren should do nothing``() = + let ``Given previous state = None / current state = None, updateChildren should do nothing``() = testUpdateChildren ValueNone ValueNone - |> should equal [| |] + |> should equal DiffResult.NoChange /// Not defining a previously existing list clears all previous controls [] @@ -65,7 +37,7 @@ module UpdateChildrenInternalTests = View.Label() ] testUpdateChildren (ValueSome previous) ValueNone - |> should equal [| Clear |] + |> should equal DiffResult.ClearCollection /// A non-changing empty list should do nothing [] @@ -74,7 +46,7 @@ module UpdateChildrenInternalTests = let current = [] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal [||] + |> should equal DiffResult.NoChange /// Adding a new element to an empty list should create the associated control [] @@ -84,7 +56,9 @@ module UpdateChildrenInternalTests = testUpdateChildren (ValueSome previous) (ValueSome current) |> should equal - [| Create (0, current.[0]) |] + (DiffResult.Operations [ + Insert (0, current.[0]) + ]) /// Keeping the exact same state (same instance) should do nothing [] @@ -95,7 +69,7 @@ module UpdateChildrenInternalTests = let current = [ label ] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal [||] + |> should equal (DiffResult.NoChange) /// Keeping the same state (not same instance) should update the existing control nonetheless [] @@ -104,8 +78,9 @@ module UpdateChildrenInternalTests = let current = [ View.Label() ] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal - [| Update (0, previous.[0], current.[0]) |] + |> should equal (DiffResult.Operations [ + Update (0, previous.[0], current.[0]) + ]) /// Replacing an element by another one (same control type) should update the existing control [] @@ -114,8 +89,9 @@ module UpdateChildrenInternalTests = let current = [ View.Label(text = "B") ] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal - [| Update (0, previous.[0], current.[0]) |] + |> should equal (DiffResult.Operations [ + Update (0, previous.[0], current.[0]) + ]) /// Emptying a list should clear all controls [] @@ -128,7 +104,7 @@ module UpdateChildrenInternalTests = [] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal [| Clear |] + |> should equal DiffResult.ClearCollection /// Keeping elements at the start (not same instance) and removing elements at the end should update the remaining /// controls and remove the others @@ -141,11 +117,12 @@ module UpdateChildrenInternalTests = let current = [ View.Label(text = "A") ] - let res = testUpdateChildren (ValueSome previous) (ValueSome current) - res|> should equal - [| Update (0, previous.[0], current.[0]) - Remove 1 - Remove 1 |] + testUpdateChildren (ValueSome previous) (ValueSome current) + |> should equal (DiffResult.Operations [ + Update (0, previous.[0], current.[0]) + Delete 1 + Delete 2 + ]) /// Keeping elements at the start (not same instance) and adding elements at the end should update the existing /// controls and add the others at the end @@ -158,9 +135,10 @@ module UpdateChildrenInternalTests = View.Label(text = "B") ] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal - [ Update (0, previous.[0], current.[0]) - Create (1, current.[1]) ] + |> should equal (DiffResult.Operations [ + Update (0, previous.[0], current.[0]) + Insert (1, current.[1]) + ]) /// Adding a new element at the start and keeping the existing elements after (not same instances) should reuse /// the existing controls based on their position and create the missing ones @@ -175,10 +153,11 @@ module UpdateChildrenInternalTests = View.Label(text = "B") ] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal - [| Update (0, previous.[0], current.[0]) - Update (1, previous.[1], current.[1]) - Create (2, current.[2]) |] + |> should equal (DiffResult.Operations [ + Update (0, previous.[0], current.[0]) + Update (1, previous.[1], current.[1]) + Insert (2, current.[2]) + ]) /// Removing elements in the middle of others (not the same instances) should reuse the existing controls based /// on their position and remove the superfluous ones @@ -195,11 +174,12 @@ module UpdateChildrenInternalTests = View.Label(text = "D") ] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal - [| Update (0, previous.[0], current.[0]) - Update (1, previous.[1], current.[1]) - Update (2, previous.[2], current.[2]) - Remove 3 |] + |> should equal (DiffResult.Operations [ + Update (0, previous.[0], current.[0]) + Update (1, previous.[1], current.[1]) + Update (2, previous.[2], current.[2]) + Delete 3 + ]) /// Replacing an element with an element of another type should create the new control in place of the old one [] @@ -210,9 +190,10 @@ module UpdateChildrenInternalTests = [ View.Button() ] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal - [| Create (0, current.[0]) - Remove 1 |] + |> should equal (DiffResult.Operations [ + Insert (0, current.[0]) + Delete 0 + ]) /// Adding a keyed element to an empty list should create the associated control [] @@ -221,8 +202,9 @@ module UpdateChildrenInternalTests = let current = [ View.Label(key = "KeyA") ] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal - [| Create (0, current.[0]) |] + |> should equal (DiffResult.Operations [ + Insert (0, current.[0]) + ]) /// Emptying a list containing keyed elements should clear the list [] @@ -235,7 +217,7 @@ module UpdateChildrenInternalTests = [] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal [| Clear |] + |> should equal DiffResult.ClearCollection /// Keeping the exact same state (keyed + same instance) should do nothing [] @@ -245,7 +227,7 @@ module UpdateChildrenInternalTests = let current = [ label ] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal [||] + |> should equal DiffResult.NoChange /// Keeping the same state (keyed + not same instance) should update the existing control nonetheless [] @@ -254,8 +236,9 @@ module UpdateChildrenInternalTests = let current = [ View.Label(key = "KeyA") ] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal - [| Update (0, previous.[0], current.[0]) |] + |> should equal (DiffResult.Operations [ + Update (0, previous.[0], current.[0]) + ]) /// Replacing a keyed element by another one (not same key + same control type) should update the existing control [] @@ -264,8 +247,10 @@ module UpdateChildrenInternalTests = let current = [ View.Label(key = "KeyB") ] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal - [| Update (0, previous.[0], current.[0]) |] + |> should equal (DiffResult.Operations [ + Insert (0, current.[0]) + Delete 0 + ]) /// Removing elements in the middle of others (not the same instances) should reuse the existing controls based /// on their keys and remove the superfluous ones @@ -280,11 +265,11 @@ module UpdateChildrenInternalTests = View.Label(key = "KeyC") ] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal - [| Update (0, previous.[0], current.[0]) - Update (2, previous.[2], current.[1]) - Move (2, 1) - Remove 2 |] + |> should equal (DiffResult.Operations [ + Update (0, previous.[0], current.[0]) + MoveAndUpdate (2, previous.[2], 1, current.[1]) + Delete 1 + ]) /// Reordering keyed elements should reuse the correct controls [] @@ -298,11 +283,11 @@ module UpdateChildrenInternalTests = View.Label(key = "KeyA") ] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal - [| Update (2, previous.[2], current.[0]) - Move (2, 0) - Update (1, previous.[0], current.[1]) - Remove 2 |] + |> should equal (DiffResult.Operations [ + MoveAndUpdate (2, previous.[2], 0, current.[0]) + MoveAndUpdate (0, previous.[0], 1, current.[1]) + Delete 1 + ]) /// New keyed elements should reuse discarded elements even though the keys are not matching, /// independently of their position @@ -317,13 +302,13 @@ module UpdateChildrenInternalTests = View.Label(key = "KeyD") View.Label(key = "KeyA") ] - let res = testUpdateChildren (ValueSome previous) (ValueSome current) - res |> should equal - [| Update (2, previous.[2], current.[0]) - Move (2, 0) - Update (2, previous.[1], current.[1]) - Move (2, 1) - Update (2, previous.[0], current.[2]) |] + testUpdateChildren (ValueSome previous) (ValueSome current) + |> should equal (DiffResult.Operations [ + MoveAndUpdate (2, previous.[2], 0, current.[0]) + Insert (1, current.[1]) + MoveAndUpdate (0, previous.[0], 2, current.[2]) + Delete 1 + ]) /// Complex use cases with reordering and remove/add of keyed elements should reuse controls efficiently [] @@ -340,13 +325,13 @@ module UpdateChildrenInternalTests = View.Label(key = "KeyD") View.Label(key = "KeyC") ] - let res= testUpdateChildren (ValueSome previous) (ValueSome current) - res |> should equal - [| Update (1, previous.[1], current.[0]) - Move (1, 0) - Update (3, previous.[3], current.[2]) - Move (3, 2) - Update (3, previous.[2], current.[3]) |] + testUpdateChildren (ValueSome previous) (ValueSome current) + |> should equal (DiffResult.Operations [ + MoveAndUpdate (1, previous.[1], 0, current.[0]) + Move (0, 1) + MoveAndUpdate (3, previous.[3], 2, current.[2]) + MoveAndUpdate (2, previous.[2], 3, current.[3]) + ]) /// Replacing an element with one from another type, even with the same key, should create the new control /// in place of the old one @@ -358,9 +343,10 @@ module UpdateChildrenInternalTests = [ View.Button(key = "KeyA") ] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal - [| Create (0, current.[0]) - Remove 1 |] + |> should equal (DiffResult.Operations [ + Insert (0, current.[0]) + Delete 0 + ]) /// Replacing a keyed element with one of another type and another key, should create the new control /// in place of the old one @@ -372,9 +358,10 @@ module UpdateChildrenInternalTests = [ View.Button(key = "KeyB") ] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal - [| Create (0, current.[0]) - Remove 1 |] + |> should equal (DiffResult.Operations [ + Insert (0, current.[0]) + Delete 0 + ]) /// Replacing a keyed element with a non-keyed one should reuse the discarded element [] @@ -385,8 +372,10 @@ module UpdateChildrenInternalTests = [ View.Label() ] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal - [| Update (0, previous.[0], current.[0]) |] + |> should equal (DiffResult.Operations [ + Insert (0, current.[0]) + Delete 0 + ]) /// Replacing a non-keyed element with another when a keyed element is present should reuse the discarded element [] @@ -399,9 +388,10 @@ module UpdateChildrenInternalTests = View.Label(text = "C") ] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal - [| Update (0, previous.[0], current.[0]) - Update (1, previous.[1], current.[1]) |] + |> should equal (DiffResult.Operations [ + Update (0, previous.[0], current.[0]) + Update (1, previous.[1], current.[1]) + ]) /// Removing an element at the start of a list with keyed elements present should reuse the correct controls [] @@ -413,10 +403,10 @@ module UpdateChildrenInternalTests = [ View.Label(key = "KeyB") ] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal - [| Update (1, previous.[1], current.[0]) - Move (1, 0) - Remove 1 |] + |> should equal (DiffResult.Operations [ + MoveAndUpdate (1, previous.[1], 0, current.[0]) + Delete 0 + ]) /// Complex use cases with reordering and remove/add of mixed elements should reuse controls efficiently [] @@ -434,12 +424,13 @@ module UpdateChildrenInternalTests = View.Label(key = "KeyB") ] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal - [| Move (3, 0) - Update (1, previous.[0], current.[1]) - Update (2, previous.[1], current.[2]) - Remove 3 - Remove 3 |] + |> should equal (DiffResult.Operations [ + Move (3, 0) + MoveAndUpdate (0, previous.[0], 1, current.[1]) + MoveAndUpdate (1, previous.[1], 2, current.[2]) + Delete 2 + Delete 4 + ]) open Xamarin.Forms @@ -451,7 +442,7 @@ module UpdateChildrenInternalTests = View.Button(automationId="IncrementButton", text="Increment", command= (fun () -> ())) View.Button(automationId="DecrementButton", text="Decrement", command= (fun () -> ())) View.StackLayout(padding = Thickness 20.0, orientation=StackOrientation.Horizontal, horizontalOptions=LayoutOptions.Center, children = [ ]) - View.Slider(automationId="StepSlider", minimumMaximum=(0.0, 10.0), value=1., valueChanged=(fun args -> ())) + View.Slider(automationId="StepSlider", minimumMaximum=(0.0, 10.0), value=1., valueChanged=(fun _ -> ())) View.Label(automationId="StepSizeLabel", text="Step size: 1", horizontalOptions=LayoutOptions.Center) View.Button(text="Reset", horizontalOptions=LayoutOptions.Center, command=(fun () -> ()), commandCanExecute = false) ] @@ -461,20 +452,21 @@ module UpdateChildrenInternalTests = View.Button(automationId="IncrementButton", text="Increment", command= (fun () -> ())) View.Button(automationId="DecrementButton", text="Decrement", command= (fun () -> ())) View.StackLayout(padding = Thickness 20.0, orientation=StackOrientation.Horizontal, horizontalOptions=LayoutOptions.Center, children = [ ]) - View.Slider(automationId="StepSlider", minimumMaximum=(0.0, 10.0), value=1., valueChanged=(fun args -> ())) + View.Slider(automationId="StepSlider", minimumMaximum=(0.0, 10.0), value=1., valueChanged=(fun _ -> ())) View.Label(automationId="StepSizeLabel", text="Step size: 1", horizontalOptions=LayoutOptions.Center) View.Button(text="Reset", horizontalOptions=LayoutOptions.Center, command=(fun () -> ()), commandCanExecute = true) ] testUpdateChildren (ValueSome previous) (ValueSome current) - |> should equal - [| Update (0, previous.[0], current.[0]) - Update (1, previous.[1], current.[1]) - Update (2, previous.[2], current.[2]) - Update (3, previous.[3], current.[3]) - Update (4, previous.[4], current.[4]) - Update (5, previous.[5], current.[5]) - Update (6, previous.[6], current.[6]) |] + |> should equal (DiffResult.Operations [ + Update (0, previous.[0], current.[0]) + Update (1, previous.[1], current.[1]) + Update (2, previous.[2], current.[2]) + Update (3, previous.[3], current.[3]) + Update (4, previous.[4], current.[4]) + Update (5, previous.[5], current.[5]) + Update (6, previous.[6], current.[6]) + ]) [] let ``Test TicTacToe``() = @@ -507,13 +499,114 @@ module UpdateChildrenInternalTests = backgroundColor=Color.LightBlue ).Row(0).Column(1) ] - let res = testUpdateChildren (ValueSome previous) (ValueSome current) - res - |> should equal - [| Update (0, previous.[0], current.[0]) - Update (1, previous.[1], current.[1]) - Update (2, previous.[2], current.[2]) - Update (3, previous.[3], current.[3]) - Create (4, current.[4]) - Update (5, previous.[4], current.[5]) - Remove 6 |] \ No newline at end of file + testUpdateChildren (ValueSome previous) (ValueSome current) + |> should equal (DiffResult.Operations [ + Update (0, previous.[0], current.[0]) + Update (1, previous.[1], current.[1]) + Update (2, previous.[2], current.[2]) + Update (3, previous.[3], current.[3]) + Insert (4, current.[4]) + MoveAndUpdate (4, previous.[4], 5, current.[5]) + Delete 5 + ]) + + [] + let ``Test TicTacToe 3``() = + let previous = + [ View.Button(key = "Button0_0") + View.Button(key = "Button0_1") + View.Button(key = "Button0_2") + View.Button(key = "Button1_0") + View.Button(key = "Button1_1") + View.Button(key = "Button1_2") + View.Button(key = "Button2_0") + View.Button(key = "Button2_1") + View.Button(key = "Button2_2") ] + + let current = + [ View.Button(key = "Button0_0") + View.Button(key = "Button0_1") + View.Button(key = "Button0_2") + View.Button(key = "Button1_0") + View.Image(key = "Image1_1") + View.Button(key = "Button1_2") + View.Button(key = "Button2_0") + View.Button(key = "Button2_1") + View.Button(key = "Button2_2") ] + + testUpdateChildren (ValueSome previous) (ValueSome current) + |> should equal (DiffResult.Operations [ + Update (0, previous.[0], current.[0]) + Update (1, previous.[1], current.[1]) + Update (2, previous.[2], current.[2]) + Update (3, previous.[3], current.[3]) + Insert (4, current.[4]) + Update (5, previous.[5], current.[5]) + Update (6, previous.[6], current.[6]) + Update (7, previous.[7], current.[7]) + Update (8, previous.[8], current.[8]) + Delete 4 + ]) + + [] + let ``Test Random``() = + let previous = + [ View.Button(key = "Button0_0") + View.Button(key = "Button0_1") + View.Button(key = "Button0_2") + View.Button(key = "Button1_0") + View.Image(key = "Image1_1") ] + + let current = + [ View.Label(key = "Button0_0") + View.Button(key = "Button0_1") + View.Image(key = "Button0_2") ] + + testUpdateChildren (ValueSome previous) (ValueSome current) + |> should equal (DiffResult.Operations [ + Insert (0, current.[0]) + Update (1, previous.[1], current.[1]) + Insert (2, current.[2]) + Delete 0 + Delete 2 + Delete 3 + Delete 4 + ]) + + [] + let ``Test Random 2``() = + let previous = + [ View.Label(key = "0") + View.Label() + View.Button() + View.Button(key = "2") + View.Label() + View.Label(key = "3") + View.Label() + View.Button() + View.Button() + View.Button() + View.Button() ] + + let current = + [ View.Button(key = "0") + View.Label() ] + + testUpdateChildren (ValueSome previous) (ValueSome current) + |> should equal (DiffResult.Operations [ + MoveAndUpdate (2, previous.[2], 0, current.[0]) + Update (1, previous.[1], current.[1]) + + // Discarded elements = had a key that was not reused + Delete 0 + Delete 3 + Delete 5 + + // Not reused elements + Delete 4 + Delete 6 + Delete 7 + Delete 8 + Delete 9 + Delete 10 + ]) \ No newline at end of file diff --git a/Fabulous.XamarinForms/tests/Fabulous.XamarinForms.Tests/Collections/UpdateChildrenTests.fs b/Fabulous.XamarinForms/tests/Fabulous.XamarinForms.Tests/Collections/UpdateChildrenTests.fs new file mode 100644 index 000000000..095449bf0 --- /dev/null +++ b/Fabulous.XamarinForms/tests/Fabulous.XamarinForms.Tests/Collections/UpdateChildrenTests.fs @@ -0,0 +1,42 @@ +namespace Fabulous.XamarinForms.Tests.Collections + +open System.Collections.ObjectModel +open Fabulous +open Fabulous.XamarinForms +open Fabulous.XamarinForms.Collections +open NUnit.Framework +open FsUnit + +module UpdateChildrenTests = + [] + let ``Test UpdateChildren``() = + let previous = + [ View.Button(key = "Button0_0") + View.Button(key = "Button0_1") + View.Button(key = "Button0_2") + View.Button(key = "Button1_0") + View.Image(key = "Image1_1") ] + + let current = + [ View.Label(key = "Button0_0") + View.Button(key = "Button0_1") + View.Image(key = "Button0_2") ] + + let collection = ObservableCollection() + collection.Add(Xamarin.Forms.Button()) + collection.Add(Xamarin.Forms.Button()) + collection.Add(Xamarin.Forms.Button()) + collection.Add(Xamarin.Forms.Button()) + collection.Add(Xamarin.Forms.Image()) + + Collections.updateChildren + (ValueSome (Array.ofList previous)) + (ValueSome (Array.ofList current)) + collection + (fun x -> x.Create() :?> Xamarin.Forms.Element) + (fun _ _ _ -> ()) + (fun _ _ _ -> ()) + + let x = collection + () + diff --git a/Fabulous.XamarinForms/tests/Fabulous.XamarinForms.Tests/Fabulous.XamarinForms.Tests.fsproj b/Fabulous.XamarinForms/tests/Fabulous.XamarinForms.Tests/Fabulous.XamarinForms.Tests.fsproj index faeb32236..7526b3bc1 100644 --- a/Fabulous.XamarinForms/tests/Fabulous.XamarinForms.Tests/Fabulous.XamarinForms.Tests.fsproj +++ b/Fabulous.XamarinForms/tests/Fabulous.XamarinForms.Tests/Fabulous.XamarinForms.Tests.fsproj @@ -9,7 +9,9 @@ - + + + diff --git a/docs/Fabulous.XamarinForms/views-perf.md b/docs/Fabulous.XamarinForms/views-perf.md index a2cea322a..43bf21c66 100644 --- a/docs/Fabulous.XamarinForms/views-perf.md +++ b/docs/Fabulous.XamarinForms/views-perf.md @@ -109,14 +109,15 @@ For all of the above, the typical, naive implementation of the `view` function r instance on each invocation. The incremental update of dynamic views maintains a corresponding mutable target (e.g. the `Children` property of a `Xamarin.Forms.StackLayout`, or an `ObservableCollection` to use as an `ItemsSource` to a `ListView`) based on the previous (PREV) list and the new (NEW) list. -Fabulous first prioritizes the reuse of the same ViewElement instances, when using dependsOn for instance. +Fabulous prioritizes reuse in the following order: +1. Same ViewElement instance (when using dependsOn) ```fsharp View.Grid([ dependsOn () (fun _ _ -> View.Label(text = "Hello, World!")) ]) ``` -Then, it will try to reuse ViewElements sharing the same key, if `canReuseView` returns `true`. +2. Same key and control type (aka. `canReuseView` returns true) ```fsharp // Previous View @@ -125,7 +126,6 @@ View.Grid([ View.Label(key = "body", text = "Previous body") ]) - // New View View.Grid([ View.Label(key = "header", text = "New Header") // Will reuse previous header @@ -133,8 +133,11 @@ View.Grid([ ]) ``` -If there's no matching instance or key, it will try to reuse one of the remaining previous elements to find the first one for which `canReuseView` returns `true`. -If it finds one, it reuses it, if not it creates a new control. +3. If none of the above, Fabulous will select the first element that returns `canReuseView = true` among the eligible remaining previous elements. + +4. If no previous element can be reused, a new one is created + +Note that old keyed elements that didn't had a matching key in the new list will be destroyed instead of being reused by new unkeyed elements to help developers avoid undesired animations, such as fade-in/fade-out on Button Text changes on iOS ([#308](https://github.com/fsprojects/Fabulous/issues/308)) or ripple effects on Android Button. In the end, controls that weren't reused are destroyed.