maui icon indicating copy to clipboard operation
maui copied to clipboard

[iOS] ListView with HasUnevenRows="true" doesn't resize dynamically

Open stefanpirkl opened this issue 1 year ago • 16 comments

Description

On iOS, when using a ListView with HasUnevenRows="true", the ViewCell's size doesn't change dynamically if the enclosed content resizes.

I'm using a simple ContentView inside the ViewCell to resize the items.

Android - accordion in ViewCell open: Android open Android - accordion in ViewCell closed: Android closed

iOS - accordion in ViewCell open: iOS open iOS - accordion in ViewCell closed - ViewCell takes up the same space as before: iOS closed

Apologies for the rather complicated example code.

(I've liberated some code for the sample from https://github.com/dotnet/maui/issues/7217 to reproduce the issue)

Steps to Reproduce

  1. Create a new .Net MAUI Solution called "iOSContentViewBug"
  2. Replace content of MainPage.xaml with
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
			 xmlns:c="clr-namespace:iOSContentViewBug"
             x:Class="iOSContentViewBug.MainPage">

	<ListView ItemsSource="{Binding AccordionValues}" HasUnevenRows="True">
		<ListView.ItemTemplate>
			<DataTemplate>
				<ViewCell>
					<c:AccordionView AccordionContent="blablabla">
					</c:AccordionView>
				</ViewCell>
			</DataTemplate>
		</ListView.ItemTemplate>
	</ListView>

</ContentPage>

  1. Replace content of MainPage.xaml.cs with this:
namespace iOSContentViewBug;

public partial class MainPage : ContentPage
{
    private List<string> _accordionValues = new List<string>
    {
        "Content",
        "Content",
        "Content",
        "Content",
        "Content",
        "Content",
        "Content",
        "Content",
        "Content",
        "Content",
        "Content"
    };


    public MainPage()
    {
        InitializeComponent();

        BindingContext = this;
    }


    public List<string> AccordionValues
    {
        get { return _accordionValues; }
        set { _accordionValues = value; OnPropertyChanged(nameof(AccordionValues)); }
    }
}

  1. Create a new ContentView "AccordionView.xaml"
  2. Replace AccordionView.xaml with this:
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="iOSContentViewBug.AccordionView"
			 x:Name="_accordionView">
	<StackLayout>
		<Frame Margin="10" BackgroundColor="Red">
			<Grid RowDefinitions="Auto,Auto" VerticalOptions="Center">
				<Frame Grid.Row="0">
					<Grid ColumnDefinitions="*,Auto">
						<Label Grid.Column="0" Text="Header" />
						<Label
                            x:Name="label"
                            Grid.Column="1"
                            HorizontalOptions="End"
                            Text="Hide">
							<Label.GestureRecognizers>
								<TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped" />
							</Label.GestureRecognizers>
						</Label>
					</Grid>
				</Frame>
				<Frame Grid.Row="1" x:Name="frame">
					<Label Text="{Binding Source={x:Reference _accordionView}, Path=AccordionContent}"/>
				</Frame>
				<!--<ContentView x:Name="_accContent" Grid.Row="1"
                         Content="{Binding Source={x:Reference _accordionView}, Path=AccordionContentView}">
				</ContentView>-->
			</Grid>
		</Frame>
	</StackLayout>
</ContentView>

  1. Replace AccordionView.xaml.cs with this:
namespace iOSContentViewBug;

public partial class AccordionView : ContentView
{
    public AccordionView()
    {
        InitializeComponent();
    }

    public static readonly BindableProperty AccordionContentProperty = BindableProperty.Create(nameof(AccordionContent), typeof(string), typeof(AccordionView), default(string));
    public string AccordionContent
    {
        get => (string)GetValue(AccordionContentProperty);
        set => SetValue(AccordionContentProperty, value);
    }

    private void TapGestureRecognizer_Tapped(object sender, EventArgs e)
    {
        frame.IsVisible = !frame.IsVisible;
        label.Text = frame.IsVisible ? "Show" : "Hide";
    }
}
  1. Run on iOS - the ViewCell should keep the same size, regardless of state of the Content View (Show/Hide)

Link to public reproduction project repository

https://github.com/stefanpirkl/bugreport-maui-ios-listview-uneven

Version with bug

7.0 (current)

Last version that worked well

Unknown/Other

Affected platforms

iOS

Affected platform versions

iOS 16.2

Did you find any workaround?

no

Relevant log output

No response

stefanpirkl avatar May 12 '23 12:05 stefanpirkl

We've added this issue to our backlog, and we will work to address it as time and resources allow. If you have any additional information or questions about this issue, please leave a comment. For additional info about issue management, please read our Triage Process.

ghost avatar May 12 '23 14:05 ghost

Does the same thing happen if you use a CollectionVIew?

mattleibow avatar May 12 '23 14:05 mattleibow

Hi, thanks for the quick response!

CollectionView behaves exactly the same, I'm afraid.

stefanpirkl avatar May 15 '23 06:05 stefanpirkl

Does the same thing happen if you use a CollectionVIew?

CollectionView has the same problem on iOS, it works well on Android.

Codespilot avatar May 18 '23 09:05 Codespilot

Also happens to me. Have you found any workaround?

v0idzz avatar May 21 '23 13:05 v0idzz

No. AFAIK, there is no workaround.

stefanpirkl avatar May 22 '23 08:05 stefanpirkl

Verified this on Visual Studio Enterprise 17.7.0 Preview 2.0. Repro on iOS 16.4, not repro on Android 13.0-API33 with below Project: iOSContentViewBug.zip

XamlTest avatar Jul 10 '23 06:07 XamlTest

Any news on this issue?

stefanpirkl avatar Oct 11 '23 07:10 stefanpirkl

After digging a bit, these seem to be the same issues: #8239 #7967 Still no workaround. This is very frustrating.

stefanpirkl avatar Oct 17 '23 16:10 stefanpirkl

We've finally found a workaround!

Instead of ListView/CollectionView, use a (Vertical/Horizontal)StackLayout with BindableLayout: ` <ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:c="clr-namespace:iOSContentViewBug" x:Class="iOSContentViewBug.MainPage">

<VerticalStackLayout BindableLayout.ItemsSource="{Binding AccordionValues}">
	<BindableLayout.ItemTemplate>
		<DataTemplate>
		  <c:AccordionView AccordionContent="blablabla">
		  </c:AccordionView>
		</DataTemplate>
	</BindableLayout.ItemTemplate>
</VerticalStackLayout>
`

If you need scrolling, wrap the StackLayout in an additional ScrollView.

stefanpirkl avatar Oct 26 '23 16:10 stefanpirkl

We've finally found a workaround!

Instead of ListView/CollectionView, use a (Vertical/Horizontal)StackLayout with BindableLayout: `

<VerticalStackLayout BindableLayout.ItemsSource="{Binding AccordionValues}">
	<BindableLayout.ItemTemplate>
		<DataTemplate>
		  <c:AccordionView AccordionContent="blablabla">
		  </c:AccordionView>
		</DataTemplate>
	</BindableLayout.ItemTemplate>
</VerticalStackLayout>

` If you need scrolling, wrap the StackLayout in an additional ScrollView.

Although this does work, bindable layouts do not have virtualization like listview and collectionview do, so performance can suffer as itemssource count increases. Could really do with a proper fix for this!

ieuangriffiths97 avatar Nov 10 '23 00:11 ieuangriffiths97

Although this does work, bindable layouts do not have virtualization like listview and collectionview do, so performance can suffer as itemssource count increases. Could really do with a proper for this!

Sorry :( I know it's not a proper fix, just thought I'd let people know there's a workaround, since we've really struggled with this issue. Maybe the workaround was too obvious and our dev team is just stupid? Should I delete it?

stefanpirkl avatar Nov 10 '23 06:11 stefanpirkl

Although this does work, bindable layouts do not have virtualization like listview and collectionview do, so performance can suffer as itemssource count increases. Could really do with a proper for this!

Sorry :( I know it's not a proper fix, just thought I'd let people know there's a workaround, since we've really struggled with this issue. Maybe the workaround was too obvious and our dev team is just stupid? Should I delete it?

No, it's not stupid! This will be a perfectly fine solution for some use cases, so it's useful to let people know about it! Was just dropping my two cents/leaving a reply on the thread so I can track it.

ieuangriffiths97 avatar Nov 10 '23 09:11 ieuangriffiths97

Is there any solution until now? This workaround is useful in simple scenarios, but using BindableLayout can be very costly, Especially when using TemplateSelectors, loading page can get very slow.

jevonsflash avatar Jan 23 '24 08:01 jevonsflash

is there any other solution for binding items more fastly using BindableLayout?

karimullla avatar Feb 05 '24 09:02 karimullla

I guess it's still not fixed, and there is still no workaround?!

andreas-spindler-mw avatar Feb 20 '24 10:02 andreas-spindler-mw

The workaround in this case is to call ForceUpdateSize() on the ViewCell after you make a change you expect to cause the ListView item size to require an update.

So, in the repro, I added an event to AccordionView: public event EventHandler AccordionVisibilityChanged; and in the Tap gesture callback where the frame visibility is changed, I invoke that event.

On the MainPage in the repro, I have registered to the event and have this in the MainPage.xaml.cs code behind:

private void AccordionView_OnAccordionVisibilityChanged(object sender, EventArgs e)
    {
        if (sender is AccordionView accordionView && accordionView.Parent is ViewCell viewCell)
        {
              viewCell.ForceUpdateSize();
        }
    }

https://github.com/dotnet/maui/assets/271950/c0df0f66-ea60-4924-b3ac-aaa2cf76aed2

Redth avatar May 29 '24 15:05 Redth

This problem seems to be fixed if you set VerticalOptions to Start on the ListView

DevelopmentDan avatar Jul 26 '24 09:07 DevelopmentDan

I had a similar issue in a ListView and came up with the following workaround which is placed in the ViewModel and called when an item in the ListView gets updated (in my instance this was called via a messenger passing updatedObject).

// get index of object in list
int index = YourItems.IndexOf(updatedObject);

// assign updated object to item in list
YourItems[index] = updatedObject;

// hack to re-highlight the row
YourSelectedItem = null;
YourSelectedItem = YourItems[index];

As long as YourSelectedItem is set to the SelectedItem in the ListView and YourItems is set to the ItemsSource, this should work fine.

Hope it helps.

kevinjohnobrien avatar Jul 31 '24 19:07 kevinjohnobrien