.NET MAUI: iOS ListView disappearing cells

In this article, we will delve into a persistent .NET MAUI issue affecting ListViews on the iOS platform in .NET 7 builds. While the .NET 8 preview has addressed this issue, developers seeking a solution can employ the BindableLayout as a temporary workaround. We’ll also explore the concept of BindableLayout and touch on the CachingStrategy within ListViews.

The Problem: Disappearing Cells in ListViews on iOS

The issue at hand revolves around the behavior of ListViews on the iOS platform in .NET 7 builds. As users scroll through the list elements, the ListView cells mysteriously disappear, causing a jarring experience. While the .NET 8 preview has resolved this vexing problem, the official release is still pending, leaving developers seeking immediate solutions.

The Workaround: BindableLayout within ScrollView

A viable workaround to mitigate the disappearing cell issue involves utilizing the BindableLayout within a ScrollView. The BindableLayout.ItemSource property can be harnessed to mimic the ListView’s behavior. However, it’s crucial to acknowledge that this solution might not deliver the same performance as a native ListView.

Example Code

Here’s how you can implement the BindableLayout workaround:

<ScrollView>
    <StackLayout>
        <BindableLayout.ItemsSource>
            <x:Array Type="{x:Type local:ItemModel}">
                <local:ItemModel Name="Item 1" />
                <local:ItemModel Name="Item 2" />
                <local:ItemModel Name="Item 3" />
                <!-- Add more items here -->
            </x:Array>
        </BindableLayout.ItemsSource>
        <BindableLayout.ItemTemplate>
            <DataTemplate>
                <Label Text="{Binding Name}" />
            </DataTemplate>
        </BindableLayout.ItemTemplate>
    </StackLayout>
</ScrollView>

BindableLayout: A Glimpse

BindableLayout is a versatile feature within the .NET MAUI framework that allows developers to easily bind collections to layout controls. It’s an excellent alternative when dealing with scenarios where a native ListView isn’t performing optimally or in cases like the aforementioned issue. BindableLayout empowers developers to achieve the desired UI layout while maintaining data synchronization.

Use BindableLayout when you want more control over the layout of your items, need to create a dynamic UI with varying layouts, and don’t necessarily require built-in performance optimizations.

Understanding CachingStrategy within ListViews

ListViews in .NET MAUI come with a property known as CachingStrategy. This property determines how the ListView should cache its visual elements, significantly influencing performance. There are three options:

  1. RecycleElement: This strategy reuses existing cells, enhancing memory usage and rendering speed. However, it might pose issues when complex cell layouts are used.
  2. RetainElement: This strategy preserves cells for the duration of the ListView’s existence. While memory consumption can be higher, it can be useful for more intricate cell layouts.
  3. None: In this strategy, no caching is employed, causing cells to be created and destroyed frequently. While it minimizes memory usage, it can have an adverse impact on performance.

Conclusion

While the .NET MAUI framework continues to evolve, issues like the disappearing ListView cells on iOS in .NET 7 builds are inevitable. Developers eagerly anticipate the benefits that .NET 8 will bring, including the resolution of this particular problem. In the interim, the BindableLayout within a ScrollView offers a workaround that replicates the ListView’s behavior, albeit with potential performance differences. By understanding concepts like BindableLayout and the CachingStrategy within ListViews, developers can navigate these challenges while continuing to create engaging and efficient cross-platform applications. Stay tuned for the official .NET 8 release and more innovations that will undoubtedly enhance the .NET MAUI experience.

https://github.com/dotnet/maui/issues/11640

This content has 9 months. Some of the information in this post may be out of date or no longer work. Please, read this page keeping its age in your mind.

Xamarin Forms: Infinite scrolling ListView

To get a dynamic “paged” loading for your list view, follow the steps below:

Make a new usercontrol in your forms project. Derive from ContentView instead from ListView. By deriving from listview, you simply can not Count the items binded to the ItemsSource property, which is the base of the Fetching logic.

public class InfiniteScrollListView : ContentView

Make a countable ItemsSource bindable property for your new usercontrol.

		public static readonly BindableProperty ItemsSourceProperty = BindableProperty.Create(
														propertyName: "ItemsSource",
														returnType: typeof(IList),
														declaringType: typeof(View),
														defaultValue: null,
														defaultBindingMode: BindingMode.TwoWay,
														propertyChanged: null);

		public IList ItemsSource
		{
			get { return (IList)GetValue(ItemsSourceProperty); }
			set { SetValue(ItemsSourceProperty, value); }
		}

We need to get the usercontrol to display a listview for us. Make a new instance of a listview. You can specify a caching strategy for the listview, which can be useful on lists have a lot of items. Set the contenview’s content to the new listview instance.

		public InfiniteScrollListView()
		{
			_listViewInstance = new ListView(cachingStrategy: ListViewCachingStrategy.RecycleElement);
			_listViewInstance.HorizontalOptions = LayoutOptions.Fill;
			_listViewInstance.VerticalOptions = LayoutOptions.Fill;
			_listViewInstance.ItemAppearing += InfiniteScrollListView_ItemAppearing;
			_listViewInstance.SetBinding(ListView.ItemsSourceProperty, new Binding(nameof(ItemsSource), BindingMode.TwoWay, source: this));
			Content = _listViewInstance;
		}

		~InfiniteScrollListView()
		{
			if (_listViewInstance != null)
				_listViewInstance.ItemAppearing -= InfiniteScrollListView_ItemAppearing;
		}

The ItemAppearing event is the most important event in our Infinite scrolling listview instance. It’s called, when an item is displayed “phisically” on the screen. Let’s make an eventhandler for it. We are using a new Bindable Property for fetching data command, which will be called, when the last item of the ItemsSource has been displayed, and an another Bindable property, to turn off the fetching if the Data access layer could not provide more elements for the query. The _lastElementIndex will be the e.ItemIndex.

if (_lastItemDisplayedIndex == (ItemsSource?.Count - 1 ?? int.MinValue))
			{
				if (HasMoreItems)
				{
					if (FetchDataCommand?.CanExecute(null) == true)
					{
						FetchDataCommand?.Execute(null);
					}
				}
			}

This content has 4 years. Some of the information in this post may be out of date or no longer work. Please, read this page keeping its age in your mind.

Xamarin Forms: Auto magasságú ListView

Formsban, ha egy ListView HorizontalOptions, VerticalOptions tulajdonságait Fill-re állítjuk, és a ListView egy Grid sorában van mondjuk, amelynek a Height-je Auto, a lista le fogja foglalni a Grid által kínált összes helyet. Ez azért van, mert a ListView measure-nél még nem tudja hogy milyen listaelemek milyen listaelem formátummal fognak megjelenni benne. Így elveszítjük a lehetőséget a dinamikus megjelenítésre, mert a felületünk szét fog csúszni.

Ennek a problémakörnek megoldásaképpen készítettem egy olyan ListView-t, amely képes a listaelemek tényleges lemért magassága után újraméretezni a lista magasságát. Ez a lista, mindig a benne található elemek magassága méretűre fog nyúlni. Meg lehet még fűszerezni annyival, hogy a Separatorok magasságát is beleszámolja, illetve hogy legyen egy maximálisan engedett magassága is, én ezt nem tettem meg, de jó kiindulási alap.

 public class AutoHeightListView : ListView
    {
        public AutoHeightListView()
        {
        }

        protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            base.OnPropertyChanged(propertyName);

            // ItemsSource változásakor 
            if (propertyName == nameof(ListView.ItemsSource))
            {
                if(ItemsSource != null)
                {
                    // Kikérjük a ListView feltemapltezett celljeit.
                    IEnumerable<PropertyInfo> pInfos = (this as ItemsView<Cell>).GetType().GetRuntimeProperties();
                    var templatedItems = pInfos.FirstOrDefault(info => info.Name == "TemplatedItems");

                    if (templatedItems != null)
                    {
                        var cells = templatedItems.GetValue(this);
                        if(cells is Xamarin.Forms.ITemplatedItemsList<Xamarin.Forms.Cell> cellsCasted)
                        {
                            if(cellsCasted.Count != 0)
                            {
                                ((ViewCell)cellsCasted.Last()).View.SizeChanged += FirstElementHeightChanged;
                            }
                        }
                    }
                }
            }
        }

        double prevHeight = 0;

        private void FirstElementHeightChanged(object sender, EventArgs e)
        {
            SetHeightRequestByCellsHeight();
        }

        /// <summary>
        /// Megméri minden sor Viewcell magasságát, és az egész listview magasságát ezeknek az összegére állítja be.
        /// </summary>
        private void SetHeightRequestByCellsHeight()
        {
            double calculatedHeight = 0;

            // Kikérjük a ListView feltemapltezett celljeit.
            IEnumerable<PropertyInfo> pInfos = (this as ItemsView<Cell>).GetType().GetRuntimeProperties();
            var templatedItems = pInfos.FirstOrDefault(info => info.Name == "TemplatedItems");

            if (templatedItems != null)
            {
                var cells = templatedItems.GetValue(this);
                foreach (ViewCell cell in cells as Xamarin.Forms.ITemplatedItemsList<Xamarin.Forms.Cell>)
                {
                    calculatedHeight += cell.View.Height;
                }
            }

            if (calculatedHeight == prevHeight)
                return;

            prevHeight = calculatedHeight;
            HeightRequest = calculatedHeight;
        }
    }
This content has 4 years. Some of the information in this post may be out of date or no longer work. Please, read this page keeping its age in your mind.