Xamarin.forms: ForceUpdateSize slows UI

Created on 16 May 2018  ·  19Comments  ·  Source: xamarin/Xamarin.Forms

Description

ForceUpdateSize slows UI on iOS

Steps to Reproduce

  1. Create page with a ListView
  2. Change Cell height on "Tapped" event, and call ForceUpdateSize afterwards
  3. After 5-6 taps on the same cell the UI starts to slow down, eventually almost freezes

Expected Behavior

UI should be fluent

Actual Behavior

UI is clonky and non responsive

Basic Information

This issue seems to happen only on iOS.
The issue has been brought up a few times before, but seem to be closed because of a "dead" PR.. but that really doesn't solve the issue.
Xamarin Forum: https://forums.xamarin.com/discussion/123437/forceupdatesize-slows-ui#latest
Github Issue: https://github.com/xamarin/Xamarin.Forms/issues/2039
PR: https://github.com/xamarin/Xamarin.Forms/pull/2040

  • Version with issue: 3.0.0.482510
  • Last known good version: Pretty sure it never worked
  • IDE: Visual Studio for Mac
  • Platform Target Frameworks: iOS

    • iOS: 11.3

Reproduction Link

ForceUpdateSize.zip

listview performance 6 high impact iOS 🍎 bug

Most helpful comment

Here's another work around but less code then @Bastelbaer :

public class EnhancedListView : ListView
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="EnhancedListView"/> class.
        /// </summary>
        /// <param name="strategy">The strategy.</param>
        public EnhancedListView(ListViewCachingStrategy strategy) : base(strategy)
        {
        }

        public void ForceNativeTableUpdate()
        {
            ViewCellSizeChangedEvent?.Invoke();
        }

        public event Action ViewCellSizeChangedEvent;
    }

iOS renderer:

[assembly: ExportRenderer(typeof(EnhancedListView), typeof(EnhancedListViewRenderer))]
namespace MyApp.iOS.Renderers
{
    public class EnhancedListViewRenderer : ListViewRenderer
    {
        protected override void OnElementChanged(ElementChangedEventArgs<ListView> e)
        {
            base.OnElementChanged(e);

            if (e.NewElement is EnhancedListView enhancedListView)
            {
                enhancedListView.ViewCellSizeChangedEvent += UpdateTableView;
            }
        }

        private void UpdateTableView()
        {
            if (!(Control is UITableView tv)) return;
            tv.BeginUpdates();
            tv.EndUpdates();
        }
    }
}

Where ever you need to update the viewcell without calling the ForceUpdateSize:

YourListView.ForceNativeTableUpdate();

All 19 comments

Reproduction functions as described

Any updates on this one?

Ping :)

same here

+1

I have the same problem. If I use the ForceUpdateSize-method the UI relod the full ListView and not only the Cell. To avoid the update of the ListView I was using following workaround:

```C#
CustomListView.cs
namespace MainProject
{
public delegate void CellHeightChangedDelegate(int row, int section, double height);

public class CustomListView : ListView
{
    public CellHeightChangedDelegate CellHeightChangedDelegate;
}

}

```xaml
CustomViewCell.xaml
<?xml version="1.0" encoding="UTF-8" ?>
<ViewCell
    x:Class="MainProject.CustomViewCell"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
    <ViewCell.View>
        <StackLayout
            x:Name="ContentStack"
            BackgroundColor="Transparent"
            HorizontalOptions="FillAndExpand"
            VerticalOptions="FillAndExpand" />
    </ViewCell.View>
</ViewCell>

```C#
CustomViewCell.cs
namespace MainProject
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class CustomViewCell: ViewCell
{
public static readonly BindableProperty SectionProperty = BindableProperty.Create(nameof(Section), typeof(int), typeof(CustomViewCell), 0);

    public int Section
    {
        get { return (int)GetValue(SectionProperty); }
        set { SetValue(SectionProperty, value); }
    }

    public static readonly BindableProperty RowProperty = BindableProperty.Create(nameof(Row), typeof(int), typeof(CustomViewCell), 0);

    public int Row
    {
        get { return (int)GetValue(RowProperty); }
        set { SetValue(RowProperty, value); }
    }

    public static readonly BindableProperty CellHeightProperty = BindableProperty.Create(nameof(CellHeight), typeof(double), typeof(CustomViewCell), 40d, propertyChanged: CellHeightCahnged);

    private static void CellHeightCahnged(BindableObject bindable, object oldValue, object newValue)
    {
        double newHeight = (double)newValue;
        CustomViewCell cell = (CustomViewCell)bindable;
        cell.Height = newHeight;

        if (cell.Parent != null)
        {
            ((CustomListView)cell.Parent).CellHeightChangedDelegate(cell.Index, cell.Section, newHeight);
        }
    }

    public double CellHeight
    {
        get { return (double)GetValue(CellHeightProperty); }
        set { SetValue(CellHeightProperty, value); }
    }

    public static readonly BindableProperty ContentProperty = BindableProperty.Create("Content", typeof(StackLayout), typeof(CustomViewCell), new StackLayout(), propertyChanged: ContentPropertyCahnged);

    private static void ContentPropertyCahnged(BindableObject bindable, object oldValue, object newValue)
    {
        CustomViewCell cell = (CustomViewCell)bindable;
        StackLayout content = (StackLayout)newValue;
        cell.ContentStack.Children.Add(content);
    }

    public StackLayout Content
    {
        get { return (StackLayout)GetValue(ContentProperty); }
        set { SetValue(ContentProperty, value); }
    }

    public CustomViewCell()
    {
    InitializeComponent();
}
}

}


```C#
CustomListViewItem.cs
namespace MainProject
{
    public class CustomListViewItem : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        public double CellHeight { get => Content.Height; }
        public int Row { get; private set; }
        public int Section { get; private set; }
        public StackLayout Content { get; private set; }

        public CustomListViewItem(int row, int section, StackLayout content)
        {
            Row = row;
            Section = section;
            Content = content;
            Content.SizeChanged += ContentSizeChanged;
        }

        private void ContentSizeChanged(object sender, EventArgs e)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("CellHeight"));
        }
    }
}
<?xml version="1.0" encoding="UTF-8" ?>
<ContentPage
    x:Class="MainProject.MainPage"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:root="clr-namespace:MainProject"
    HorizontalOptions="FillAndExpand"
    VerticalOptions="FillAndExpand">
    <ContentPage.Content>
        <root:CustomListView
            HasUnevenRows="True"
            HorizontalOptions="FillAndExpand"
            SelectionMode="None"
            SeparatorVisibility="None"
            ItemsSource=" *ObservableCollection of Type CustomListViewItem* " 
            VerticalOptions="FillAndExpand">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <root:CustomViewCell
                        CellHeight="{Binding CellHeight}"
                        Content="{Binding Content}"
                        Row="{Binding Row}" 
                        Section="{Binding Section}"/>
                </DataTemplate>
            </ListView.ItemTemplate>
        </root:CustomListView>
    </ContentPage.Content>
</ContentPage>

```C#
CustomListViewRenderer.cs
[assembly: ExportRenderer(typeof(CustomListView), typeof(MainProject.iOS.CustomListViewRenderer))]
namespace MainProject.iOS
{
public class CustomListViewRenderer : ListViewRenderer
{
public new static void Init() { }

    protected override void OnElementChanged(ElementChangedEventArgs<ListView> e)
    {
        if (e.NewElement != null)
        {
            ((CustomListView)e.NewElement).CellHeightChangedDelegate = UpdateCellHeight;
        }
        base.OnElementChanged(e);

        if (Control != null)
        {
            Control.AllowsSelection = false;
        }
    }

    private void UpdateCellHeight(int row, int section, double height)
    {
        if (Control != null)
        {
            UITableViewCell cell = Control.CellAt(NSIndexPath.FromRowSection(row, section));
            if (cell != null)
            {
                CGRect frame = cell.Frame;
                Control.ContentSize = new CGSize(Control.ContentSize.Width, Control.ContentSize.Height + (height - frame.Height));
                frame.Height = (nfloat)height;
                UIView.Animate(0.3d, () => { cell.Frame = frame; });
            }
        }
    }
}

}
```

Hi, its interesting to be here
Tombs up.

On Wed, Dec 12, 2018, 9:58 AM Patrick van de Wal notifications@github.com
wrote:

Any updates on this one?


You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/xamarin/Xamarin.Forms/issues/2735#issuecomment-446511421,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AqWg_jJlj7qb1GJx33iRHySkzFt2BHU_ks5u4MUvgaJpZM4UAyE_
.

I'm also seeing this, and I noticed something that might give a clue as to what is happening.

My ListView ItemsSource is bound to an IQueryable, which in turn provides real-time data from Realm.

If I call ViewCell.ForceUpdateSize(), the contents of the cell seem to be recreated every time, and to make it worse, the previous content is not being released.

I've confirmed this with manual reference counting, and it almost seems like the ViewCell or its contents are being recreated with every ForceUpdateSize() call, and the previous cell isn't being disposed of.

Just so I'm clear on this: the bound data is being "created" (lazy loaded in my case) over and over when the size updates, and naturally the reference count gets progressively larger, ultimately causing the slowdown.

any update?

Unfortunately, ForceUpdateSize never truly worked on iOS. I'm hoping CollectionView will resize cells better when it's complete.

The issue seems to be due to the fact that force update size event handlers are being subscribed at an exponential rate. WireUpForceUpdateSizeRequested in CellRenderer wires up the same event handler every time GetCell is called (and this can happen when items are first shown, the user scrolls through the list, and the user taps on a cell and issues a forced size update).

The unsubscribing of the event handler here is useless because by the time that line is hit, a new instance of the renderer is created, and we've lost a named reference to the previous handler, which brings me to my next question. Is the renderer supposed to be re-created frequently or is it supposed to be retained and re-used? If new renderers are newed up frequently, what happens to the old renderers? I think there might be a memory leak somewhere that keeps existing event handlers.

One can null out the ForceUpdateSizeRequested event before calling ForceUpdateSize(), and you might need to call ForceUpdateSize() twice for redrawing to finish, but I doubt this is a complete solution.

Having said that, I spent almost all day trying to fix this, but the code is a mess, and after all these years, I'm still not sure how Xamarin implemented cell recyling. I think the team needs to shift most of their focus on finishing up CollectionView and give us the opportunity to migrate away from ListView. The fact that ViewCell is not part of CollectionView is a huge improvement, in my opinion.

4012

Just adding +1 to this. The issue with ForceUpdateSize precludes the ability to animate the height of a cell on iOS. The more times ForceUpdateSize is called the longer the hang is.

Here's another work around but less code then @Bastelbaer :

public class EnhancedListView : ListView
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="EnhancedListView"/> class.
        /// </summary>
        /// <param name="strategy">The strategy.</param>
        public EnhancedListView(ListViewCachingStrategy strategy) : base(strategy)
        {
        }

        public void ForceNativeTableUpdate()
        {
            ViewCellSizeChangedEvent?.Invoke();
        }

        public event Action ViewCellSizeChangedEvent;
    }

iOS renderer:

[assembly: ExportRenderer(typeof(EnhancedListView), typeof(EnhancedListViewRenderer))]
namespace MyApp.iOS.Renderers
{
    public class EnhancedListViewRenderer : ListViewRenderer
    {
        protected override void OnElementChanged(ElementChangedEventArgs<ListView> e)
        {
            base.OnElementChanged(e);

            if (e.NewElement is EnhancedListView enhancedListView)
            {
                enhancedListView.ViewCellSizeChangedEvent += UpdateTableView;
            }
        }

        private void UpdateTableView()
        {
            if (!(Control is UITableView tv)) return;
            tv.BeginUpdates();
            tv.EndUpdates();
        }
    }
}

Where ever you need to update the viewcell without calling the ForceUpdateSize:

YourListView.ForceNativeTableUpdate();

Same problem for the TableView! If you update the ViewCell with ForceUpdateSize, your application slows and in the end it freeze. Problem as well only for iOS. Android works perfectly.

This issue is now older than 12 months!
Great work Xamarin Team..

Here's another work around but less code then @Bastelbaer :

public class EnhancedListView : ListView
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="EnhancedListView"/> class.
        /// </summary>
        /// <param name="strategy">The strategy.</param>
        public EnhancedListView(ListViewCachingStrategy strategy) : base(strategy)
        {
        }

        public void ForceNativeTableUpdate()
        {
            ViewCellSizeChangedEvent?.Invoke();
        }

        public event Action ViewCellSizeChangedEvent;
    }

iOS renderer:

[assembly: ExportRenderer(typeof(EnhancedListView), typeof(EnhancedListViewRenderer))]
namespace MyApp.iOS.Renderers
{
    public class EnhancedListViewRenderer : ListViewRenderer
    {
        protected override void OnElementChanged(ElementChangedEventArgs<ListView> e)
        {
            base.OnElementChanged(e);

            if (e.NewElement is EnhancedListView enhancedListView)
            {
                enhancedListView.ViewCellSizeChangedEvent += UpdateTableView;
            }
        }

        private void UpdateTableView()
        {
            if (!(Control is UITableView tv)) return;
            tv.BeginUpdates();
            tv.EndUpdates();
        }
    }
}

Where ever you need to update the viewcell without calling the ForceUpdateSize:

YourListView.ForceNativeTableUpdate();

I've added a delay before updating the table. Else the cell wont update.

DispatchQueue.MainQueue.DispatchAfter(new DispatchTime(DispatchTime.Now, TimeSpan.FromSeconds(0.2)), () => { if (!(Control is UITableView tv)) return; tv.BeginUpdates(); tv.EndUpdates(); });

Here's another work around but less code then @Bastelbaer :

public class EnhancedListView : ListView
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="EnhancedListView"/> class.
        /// </summary>
        /// <param name="strategy">The strategy.</param>
        public EnhancedListView(ListViewCachingStrategy strategy) : base(strategy)
        {
        }

        public void ForceNativeTableUpdate()
        {
            ViewCellSizeChangedEvent?.Invoke();
        }

        public event Action ViewCellSizeChangedEvent;
    }

iOS renderer:

[assembly: ExportRenderer(typeof(EnhancedListView), typeof(EnhancedListViewRenderer))]
namespace MyApp.iOS.Renderers
{
    public class EnhancedListViewRenderer : ListViewRenderer
    {
        protected override void OnElementChanged(ElementChangedEventArgs<ListView> e)
        {
            base.OnElementChanged(e);

            if (e.NewElement is EnhancedListView enhancedListView)
            {
                enhancedListView.ViewCellSizeChangedEvent += UpdateTableView;
            }
        }

        private void UpdateTableView()
        {
            if (!(Control is UITableView tv)) return;
            tv.BeginUpdates();
            tv.EndUpdates();
        }
    }
}

Where ever you need to update the viewcell without calling the ForceUpdateSize:

YourListView.ForceNativeTableUpdate();

Hi, I don't know why but I get this error

Type 'EnhancedListView' is not usable as an object element because it is not public or does not define a public parameterless constructor or a type converter.

Visual Studio Professional 2019 for Mac Version 8.3.3 (build 8)
Xamarin.iOS Version: 13.4.0.2
Xamarin.Android Version: 10.0.3.0

@qhu91it your listview is expecting a parameter that was defined in the EnhancedListView class, you need to pass the ListViewCachingStrategy in xaml to it.

<x:Arguments>
  <ListViewCachingStrategy>RetainElement</ListViewCachingStrategy>
</x:Arguments>

@qhu91it your listview is expecting a parameter that was defined in the EnhancedListView class, you need to pass the ListViewCachingStrategy in xaml to it.

<x:Arguments>
  <ListViewCachingStrategy>RetainElement</ListViewCachingStrategy>
</x:Arguments>

Thank you, I try but still get same error, don't know why :(

<controls:EnhancedListView>
    <x:Arguments>
        <ListViewCachingStrategy>RecycleElement</ListViewCachingStrategy>
    </x:Arguments>

</controls:EnhancedListView>

@qhu91it your listview is expecting a parameter that was defined in the EnhancedListView class, you need to pass the ListViewCachingStrategy in xaml to it.

RetainElement

Thank you, I try but still get same error, don't know why :(


RecycleElement

You just have to hit rebuild to get rid of the cached error message. Same thing happened to me. The workaround seems fine, by the way.

Was this page helpful?
0 / 5 - 0 ratings