Forum Xamarin Xamarin.iOS

Best Practices for Replacing ForceUpdateSize() on a Custom ViewCell?

stefanhkstefanhk Member ✭✭

I've created an expandable listview using this example. It works, but on iOS, there's a slight delay in the viewcell expansion animation. Upon further research, it seems that ForceUpdateSize is an expensive method, and there's lots of conversation about how/why it isn't a good solution for iOS because of performance hits.

Now even after reading through the forum thread above, as well as a similar stackoverflow post, I'm still not sure what the simplest path forward is. Ideally, I'd like to create a custom renderer for the custom viewcell I've created, so that it overrides ForceUpdateSize only on iOS. But I haven't found any code examples that would do this (all of the examples I've found offer more elaborate solutions, such as creating an entire custom listview). Is it possible to create only a custom renderer for the viewcell? If so, does anyone have advice/examples on how to achieve this?

For context, here is what I'm working with:
XAML:

                <ListView ItemsSource="{Binding Items}"
                          IsPullToRefreshEnabled="True"
                          RefreshCommand="{Binding LoadItemsCommand}"
                          HasUnevenRows="True"
                          IsRefreshing="{Binding IsBusy, Mode=OneWay}"
                          CachingStrategy="RecycleElement"
                          HorizontalOptions="FillAndExpand"
                          BackgroundColor="Transparent"
                          ItemTapped="ExpandDetails">
                    <d:ListView.ItemsSource>
                        <x:Array Type="{x:Type x:String}">
                            <x:String>TD ...</x:String>
                            <x:String>TD ...</x:String>
                            <x:String>TD ...</x:String>
                            <x:String>TD ...</x:String>
                            <x:String>TD ...</x:String>
                            <x:String>TD ...</x:String>
                            <x:String>TD ...</x:String>
                            <x:String>TD ...</x:String>
                            <x:String>TD ...</x:String>
                            <x:String>TD ...</x:String>
                        </x:Array>
                    </d:ListView.ItemsSource>
                    <ListView.ItemTemplate>
                        <DataTemplate>
                            <utilities:ExpandingViewCell>
                                <Grid Padding="0,10">
                                    <Grid.RowDefinitions>
                                        <RowDefinition Height="Auto"/>
                                        <RowDefinition Height="*"/>
                                    </Grid.RowDefinitions>
                                    <Grid.ColumnDefinitions>
                                        <ColumnDefinition Width="100"/>
                                        <ColumnDefinition Width="100"/>
                                        <ColumnDefinition Width="*"/>
                                    </Grid.ColumnDefinitions>
                                    <StackLayout Grid.Row="0" Grid.Column="0" >
                                        <Label
                                           Text="{Binding Item.ItemNumber}"
                                           LineBreakMode="NoWrap"
                                           Style="{DynamicResource ListItemTextStyle}"
                                           FontSize="16"
                                           BackgroundColor="Transparent"/>
                                    </StackLayout>
                                    <StackLayout Grid.Row="0" Grid.Column="1">
                                        <Label 
                                           Text="{Binding Item.DateCollected}"
                                           LineBreakMode="CharacterWrap"
                                           Style="{DynamicResource ListItemTextStyle}"
                                           FontSize="16"
                                           BackgroundColor="Transparent"/>
                                    </StackLayout>
                                    <StackLayout Grid.Row="0" Grid.Column="2">
                                        <Label Text="{Binding Item.Property}"
                                           LineBreakMode="WordWrap"
                                           Style="{DynamicResource ListItemTextStyle}"
                                           FontSize="16"
                                           BackgroundColor="Transparent"/>
                                    </StackLayout>
                                    <StackLayout Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" IsVisible="{Binding AreDetailsVisible}">
                                        <Label Text="{Binding Item.Notes, StringFormat='Collection Notes: {0}'}"
                                               LineBreakMode="WordWrap"
                                               Style="{DynamicResource ListItemTextStyle}"
                                               FontSize="16"
                                               BackgroundColor="Transparent"/>
                                        <Image IsVisible="{Binding HasImage}"
                                               Source="{Binding Item.ImagePath}"/>
                                    </StackLayout>
                                </Grid>
                            </utilities:ExpandingViewCell>
                        </DataTemplate>
                    </ListView.ItemTemplate>
                </ListView>

ItemTapped Event (in XAML code behind):

        private void ExpandDetails(object sender, ItemTappedEventArgs e)
        {
            var sample = e.Item as Item;

            viewModel.ToggleItemDetails(item);
        }

ViewModel:

public class ItemViewModel: ReadOnlyDataStoreViewModel<ItemViewDto>
    {
        private ObservableCollection<RecentItem> _recentItems { get; set; }
        public ObservableCollection<RecentIteme> RecentItems
        {
            get => _recentItems;
            private set
            {
                _recentItems= value;
                OnPropertyChanged();
            }
        }
        public ICommand LoadItemsCommand { get; set; }
        private RecentWasteSample _lastSelectedItem { get; set; }
        private RecentWasteSample _currentlySelectedItem { get; set; }
        public RecentWasteSample CurrentlySelectedItem
        {
            get
            {
                return _currentlySelectedItem;
            }
            set
            {
                _currentlySelectedItem = value;
                if (value != null)
                {
                    ToggleItemDetails(value);
                    CurrentlySelectedItem = null;
                }
            }
        }

        public RecentItemsViewModel(IReadOnlyDataStore<ItemViewDto> dataStore) : base(dataStore)
        {
            RecentItems = new ObservableCollection<RecentItem>();
            LoadItemsCommand = new Command(async () => await ExecuteLoadItemsCommand()); 
        }

        public async Task ExecuteLoadItemsCommand()
        {
            if (IsBusy)
                return;

            try
            {
                IsBusy = true;
                RecentItems.Clear();
                var items = await _dataStore.GetItemsAsync(true);
                foreach (var item in items)
                {
                    var sample = new RecentItem
                    {
                        Item= item,
                        AreDetailsVisible = false
                    };
                    RecentItems.Add(sample);
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex);
            }
            finally
            {
                IsBusy = false;
            }
        }

        public void ToggleItemDetails(RecentItem item)
        {
                if (_lastSelectedWasteSample == item)
                {
                    item.AreDetailsVisible = !item.AreDetailsVisible;
                    OnPropertyChanged(nameof(RecentItem));
                }
                else
                {
                    if (_lastSelectedItem != null)
                    {
                        _lastSelectedItem.AreDetailsVisible = false;
                    }
                   item.AreDetailsVisible = true;
                    OnPropertyChanged(nameof(RecentItem));
                }

                _lastSelectedItem = item;
        }
    }

Custom viewcell class:

    public class ExpandingViewCell : ViewCell
    {
        protected override void OnTapped()
        {
            base.OnTapped();
            ForceUpdateSize();
        }
    }

Any advice is appreciated. Thank you!

Answers

  • Dinesh_OfficialDinesh_Official Member ✭✭✭

    @stefanhk , you can achieve the requirement by using SizeThatFits override of parent UIView of ViewCell. Can you share me a simple sample. I'll try to share you a solution.

  • ColeXColeX Member, Xamarin Team Xamurai

    First , check the definition of Cell.ForceUpdateSize : Immediately updates the cell's size.

    So the solution in iOS project : create custom renderer for listview , call tableView.ReloadRows to reload specific cell in RowSelected methond , then it would trigger the method GetHeightForRow .

    Sample code

        [assembly: ExportRenderer(typeof(ListView), typeof(MyLVRenderer))]
        public class MyLVRenderer : ListViewRenderer
            {
                //UITableViewSource originalSource;
    
                protected override void OnElementChanged(ElementChangedEventArgs<ListView> e)
                {
                    base.OnElementChanged(e);
    
                    UITableViewSource originalSource = (UIKit.UITableViewSource)Control.Source;
                    Control.Source = new MyLVSource(originalSource);
    
                }
            }
    
            public class MyLVSource : UITableViewSource
            {
                UITableViewSource originalSource;
    
                public MyLVSource(UITableViewSource origSource)
                {
                    originalSource = origSource;
                }
    
                public override nint RowsInSection(UITableView tableview, nint section)
                {
                    return originalSource.RowsInSection(tableview, section);
                }
    
    
                 public void RowSelected(UITableView tableView, NSIndexPath indexPath);
                {
                       tableView.ReloadRows(indexPath, UITableViewRowAnimation.Automatic);
                }
    
                public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
                {
                    return originalSource.GetCell(tableView, indexPath);
                }
    
                public override nfloat GetHeightForFooter(UITableView tableView, nint section)
                {
                    return originalSource.GetHeightForFooter(tableView, section);
                }
    
                public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath)
                {
                    nfloat origHeight = originalSource.GetHeightForRow(tableView, indexPath);
    
                    // calculate your own row height here
                    return xxx;
                }                
            }
        }
    
  • stefanhkstefanhk Member ✭✭

    @ColeX , thank you for the advice, it is very helpful. Two followup questions:

    1: tableView,ReloadRows takes an array of type NSIndexPath, not just one path. Would something like the following snippet work for calling ReloadRows in RowSelected?

            public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
            {
                NSIndexPath[] paths = {indexPath};
                tableView.ReloadRows(paths, UITableViewRowAnimation.Automatic);
            }
    

    2: I'm having some trouble calculating the appropriate height in GetHeightForRow. I've attempted the following, but it returns an exception on load because it returns -2.

            public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath)
            {
                var originalHeight = _originalSource.GetHeightForRow(tableView, indexPath);
                return originalHeight + tableView.RowHeight;
            }
    

    I don't know if I just have to add extra logic to make sure that originalHeight and tableView.RowHeight exist before returning, or if I'm going about this completely the wrong way. Advice appreciated. Thanks!

  • ColeXColeX Member, Xamarin Team Xamurai

    @stefanhk

    1. You're right , it should be tableView.ReloadRows(new NSIndexPath[] { indexPath }, UITableViewRowAnimation.Automatic); .

    2. Remove the line var originalHeight = _originalSource.GetHeightForRow(tableView, indexPath); , add your own logic in the method .

Sign In or Register to comment.