fabulous-dev / Fabulous.MauiControls

Declarative UIs for .NET MAUI Controls with F# and MVU, using Fabulous
https://docs.fabulous.dev/maui
Apache License 2.0
88 stars 6 forks source link

ListView: when adding/removing items, app immediately crashes on Windows, raise 'ItemTemplate count has exceeded the limit of 23' Exception on Android #41

Open heldap opened 1 year ago

heldap commented 1 year ago

Code:

namespace DebugView

open Fabulous
open Fabulous.Maui

open type Fabulous.Maui.View

module App =
    let mutable id = 0
    type Item =
        {
            Name: string
            Detail: string
        }
        static member create () = 
            let i = {Name= $"Dummy{id}"; Detail="Dummy Dum"}
            id <- id + 1
            i

    type Model = { Items: Item list }

    type Msg = 
    | Add 
    | RemoveLast 

    let init () = { Items = [Item.create()] }, Cmd.none

    let removeLast = 
        function
        | [] -> []
        | l -> List.removeAt (l.Length-1) l

    let update msg model =
        match msg with
        | Add -> {model with Items = List.append model.Items [Item.create()]}, Cmd.none
        | RemoveLast -> {model with Items = removeLast model.Items}, Cmd.none

    let view model =
        Application(
            ContentPage(
                (VStack(){
                    ListView model.Items (fun i -> TextCell(i.Name).detailText(i.Detail))
                    HStack(){
                        Button("Add", Add)
                        Button("Remove Last", RemoveLast)
                    }
                })
            )
        )

    let program = Program.statefulWithCmd init update view

On Windows Machine target, whether I click Add or Remove, the app crash: it seems that any attempt to modify ListView's items results in a crash (Win32 exception)

Here is the log:

Debug: Unhandled exception: System.ArgumentException: Value is an invalid value for ItemTemplate (Parameter 'value')
   at Microsoft.Maui.Controls.BindableObject.SetValueCore(BindableProperty property, Object value, SetValueFlags attributes, SetValuePrivateFlags privateAttributes)
   at Microsoft.Maui.Controls.BindableObject.SetValue(BindableProperty property, Object value, Boolean fromStyle, Boolean checkAccess)
   at Microsoft.Maui.Controls.BindableObject.SetValue(BindableProperty property, Object value)
   at Fabulous.Maui.ItemsViewOfCell.ItemsSource@14-16.Invoke(FSharpValueOption`1 _arg1, FSharpValueOption`1 newValueOpt, IViewNode node)
   at Fabulous.ScalarAttributeDefinitions.CreateAttributeData@73-1.Invoke(FSharpValueOption`1 oldValueOpt, FSharpValueOption`1 newValueOpt, IViewNode node)
   at Fabulous.ViewNode.Fabulous.IViewNode.ApplyDiff(WidgetDiff& diff)
   at Fabulous.Maui.LayoutOfView.Children@12-8.Invoke(a _arg1, WidgetCollectionItemChanges diffs, IViewNode node)
   at Fabulous.ViewNode.Fabulous.IViewNode.ApplyDiff(WidgetDiff& diff)
   at Fabulous.Maui.ContentPage.Content@31.Invoke(WidgetDiff diff, IViewNode node)
   at Fabulous.ViewNode.Fabulous.IViewNode.ApplyDiff(WidgetDiff& diff)
   at Fabulous.Maui.Application.MainPage@45.Invoke(WidgetDiff diff, IViewNode node)
   at Fabulous.ViewNode.Fabulous.IViewNode.ApplyDiff(WidgetDiff& diff)
   at Fabulous.Reconciler.update(FSharpFunc`2 canReuseView, FSharpValueOption`1 prevOpt, Widget next, IViewNode node)
   at Fabulous.ViewAdapters.OnStateChanged@209.Invoke(Unit unitVar0)
Debug: Unhandled exception: System.ArgumentException: Value is an invalid value for ItemTemplate (Parameter 'value')
   at Microsoft.Maui.Controls.BindableObject.SetValueCore(BindableProperty property, Object value, SetValueFlags attributes, SetValuePrivateFlags privateAttributes)
   at Microsoft.Maui.Controls.BindableObject.SetValue(BindableProperty property, Object value, Boolean fromStyle, Boolean checkAccess)
   at Microsoft.Maui.Controls.BindableObject.SetValue(BindableProperty property, Object value)
   at Fabulous.Maui.ItemsViewOfCell.ItemsSource@14-16.Invoke(FSharpValueOption`1 _arg1, FSharpValueOption`1 newValueOpt, IViewNode node)
   at Fabulous.ScalarAttributeDefinitions.CreateAttributeData@73-1.Invoke(FSharpValueOption`1 oldValueOpt, FSharpValueOption`1 newValueOpt, IViewNode node)
   at Fabulous.ViewNode.Fabulous.IViewNode.ApplyDiff(WidgetDiff& diff)
   at Fabulous.Maui.LayoutOfView.Children@12-8.Invoke(a _arg1, WidgetCollectionItemChanges diffs, IViewNode node)
   at Fabulous.ViewNode.Fabulous.IViewNode.ApplyDiff(WidgetDiff& diff)
   at Fabulous.Maui.ContentPage.Content@31.Invoke(WidgetDiff diff, IViewNode node)
   at Fabulous.ViewNode.Fabulous.IViewNode.ApplyDiff(WidgetDiff& diff)
   at Fabulous.Maui.Application.MainPage@45.Invoke(WidgetDiff diff, IViewNode node)
   at Fabulous.ViewNode.Fabulous.IViewNode.ApplyDiff(WidgetDiff& diff)
   at Fabulous.Reconciler.update(FSharpFunc`2 canReuseView, FSharpValueOption`1 prevOpt, Widget next, IViewNode node)
   at Fabulous.ViewAdapters.OnStateChanged@209.Invoke(Unit unitVar0)
   at Fabulous.Maui.Program.statefulWithCmd@121-4.Invoke()
   at Microsoft.Maui.ApplicationModel.MainThread.BeginInvokeOnMainThread(Action action)
   at Fabulous.Maui.Program.statefulWithCmd@121-3.Invoke(FSharpFunc`2 arg00)
   at Fabulous.ViewAdapters.ViewAdapter`3.OnStateChanged(StateChangedEventArgs args)
'DebugView.exe' (CoreCLR: clrhost): Loaded 'D:\Projects\_Apps\DebugView\DebugView\bin\Debug\net7.0-windows10.0.19041.0\win10-x64\AppX\System.IO.Compression.dll'. Skipped loading symbols. Module is optimized and the debugger option 'Just My Code' is enabled.
Debug: Unhandled exception: System.ArgumentException: Value is an invalid value for ItemTemplate (Parameter 'value')
   at Microsoft.Maui.Controls.BindableObject.SetValueCore(BindableProperty property, Object value, SetValueFlags attributes, SetValuePrivateFlags privateAttributes)
   at Microsoft.Maui.Controls.BindableObject.SetValue(BindableProperty property, Object value, Boolean fromStyle, Boolean checkAccess)
   at Microsoft.Maui.Controls.BindableObject.SetValue(BindableProperty property, Object value)
   at Fabulous.Maui.ItemsViewOfCell.ItemsSource@14-16.Invoke(FSharpValueOption`1 _arg1, FSharpValueOption`1 newValueOpt, IViewNode node)
   at Fabulous.ScalarAttributeDefinitions.CreateAttributeData@73-1.Invoke(FSharpValueOption`1 oldValueOpt, FSharpValueOption`1 newValueOpt, IViewNode node)
   at Fabulous.ViewNode.Fabulous.IViewNode.ApplyDiff(WidgetDiff& diff)
   at Fabulous.Maui.LayoutOfView.Children@12-8.Invoke(a _arg1, WidgetCollectionItemChanges diffs, IViewNode node)
   at Fabulous.ViewNode.Fabulous.IViewNode.ApplyDiff(WidgetDiff& diff)
   at Fabulous.Maui.ContentPage.Content@31.Invoke(WidgetDiff diff, IViewNode node)
   at Fabulous.ViewNode.Fabulous.IViewNode.ApplyDiff(WidgetDiff& diff)
   at Fabulous.Maui.Application.MainPage@45.Invoke(WidgetDiff diff, IViewNode node)
   at Fabulous.ViewNode.Fabulous.IViewNode.ApplyDiff(WidgetDiff& diff)
   at Fabulous.Reconciler.update(FSharpFunc`2 canReuseView, FSharpValueOption`1 prevOpt, Widget next, IViewNode node)
   at Fabulous.ViewAdapters.OnStateChanged@209.Invoke(Unit unitVar0)
   at Fabulous.Maui.Program.statefulWithCmd@121-4.Invoke()
   at Microsoft.Maui.ApplicationModel.MainThread.BeginInvokeOnMainThread(Action action)
   at Fabulous.Maui.Program.statefulWithCmd@121-3.Invoke(FSharpFunc`2 arg00)
   at Fabulous.ViewAdapters.ViewAdapter`3.OnStateChanged(StateChangedEventArgs args)
   at Fabulous.ViewAdapters.-ctor@165.Invoke(StateChangedEventArgs arg00)
   at Microsoft.FSharp.Control.CommonExtensions.SubscribeToObservable@2281.System.IObserver<'T>.OnNext(T args) in D:\a\_work\1\s\src\FSharp.Core\async.fs:line 2283
   at Microsoft.FSharp.Control.FSharpEvent`1.Trigger(T arg) in D:\a\_work\1\s\src\FSharp.Core\event.fs:line 175
   at Fabulous.Runners.Runner`4.dispatch(msg msg)
The program '[11072] DebugView.exe' has exited with code 3221226107 (0xc000027b).

On Android, with both emulator (Android 10.0, API 29) and my phone (Android 6.0 API 23); after a while adding and removing items, I get a System.Exception:'ItemTemplate count has exceeded the limit of 23 Please make sure to reuse DataTemplate objects

TimLariviere commented 1 year ago

Thanks for the report! I managed to reproduce with the example given 👍

What's happening in your case is that Fabulous expects your items to be a stable reference value (aka mutable list) instead of a non-mutable one (like F# list type where a new list instance is created with each mutation).

If the reference changes, Fabulous will consider the whole list needs to be reloaded and will lose the DataTemplates that were created previously -- causing the crash on Android after reaching the limit of .NET MAUI.

A workaround would be to change the Model type:

type Model = { Items: ObservableCollection<Item> }

Note: I'm using ObservableCollection instead of any System.Collections.IList type, because .NET MAUI expects the collection to raise events when adding/removing items which ObservableCollection do

Then, you can still add and remove items like usual.

let init () =
    let items = ObservableCollection<Item>()
    items.Add(Item.create())
    { Items = items }, Cmd.none

let update msg model =
    match msg with
    | Add ->
        model.Items.Add(Item.create())
        model, Cmd.none
    | RemoveLast ->
        if model.Items.Count > 0 then
            model.Items.RemoveAt(model.Items.Count - 1)

        model, Cmd.none

This approach will work, though it has some drawbacks because of the mutated collection (especially if you want clean separation between states).

Virtualized collection widgets like ListView are very tricky to work with in Fabulous, and can easily crash. I'm not really satisfied with our implementation.

heldap commented 1 year ago

Thanks you for the quick reply :) This workaround is fine for my needs.

May I close this issue ? It could be interesting to keep it opened though, as someone else might encounter this issue (since using F# list is instinctive): keeping it opened makes the workaround visible.