ListView Performance & Caching Strategy

__savoskin____savoskin__ UAMember ✭✭

Hello, everybody!

I have a question about ListView performance. Is there any way to display a large list of rows with different templates? I had tried a lot of recipes including DataTemplateSelector, custom ViewCell, etc but list still very slow while scrolling. At last I implemented this idea with ListViewCachingStrategy.RecycleElement and got quite fast list, but all the rows are shuffled. But if I'm using the same template for each row everything works as expected (rows are ordered). So, is there any way to achieve this? Thanks beforehand!

The ViewCell which I have used with ListViewCachingStrategy.RecycleElement:

public class ChatItemViewCell : ViewCell
    {
        private readonly ChatItemQuestionView questionView = null;
        private readonly ChatItemAnswerView answerView = null;
        private ChatItem context = null;

        public ChatItemViewCell()
        {
            questionView = new ChatItemQuestionView();
            answerView = new ChatItemAnswerView();
        }

        protected override void OnBindingContextChanged()
        {
                base.OnBindingContextChanged();
                context = BindingContext as ChatItem;

                if (context != null)
                {
                    if (context.IsBot)
                    {
                        questionView.UpdateContext(context);
                        View = questionView;
                    }
                    else
                    {
                        answerView.UpdateContext(context);
                        View = answerView;
                    }
                }
        }
    }

Best Answer

Answers

  • __savoskin____savoskin__ UAMember ✭✭

    Simple example is attached

  • AdamPAdamP AUUniversity ✭✭✭✭✭

    savoskin - Cell Recycling uses the previous cell layout but just changes the data, so it doesn't have to load all the cells at once.

    However if you have a cell that is different each time, this strategy won't work.

    Is there anyway you can make it the same layout but then you will have the ability to change colors, text and images as needed. Even hide or make visible certain elements as needed.

  • __savoskin____savoskin__ UAMember ✭✭

    Hi, Adam! Thanks for reply. Do you mean I shouldn't implement different templates, but I can use one and change visibility of certain items in depending of BindingContext at that very moment in the OnBindingContextChanged method?

  • JohnHardmanJohnHardman GBUniversity mod

    @savoskin - DataTemplateSelector should work. I use it to switch between eight different templates for different types of data, in conjunction with ListViewCachingStrategy.RecycleElement and some code in OnBindingContextChanged. Without this, the alternative of using one template that contains the views required for every possibility, making them visible or invisible as required, would be horribly inefficient.

    I haven't seen any issues so far, and have been testing on Android, iOS, WinPhone 8.1 RT and UWP.

    What do you mean by "shuffled"?

  • __savoskin____savoskin__ UAMember ✭✭

    Hi, John! > What do you mean by "shuffled"? - I mean that items appears on display in no particular order while we are scrolling the list (if we are using ListViewCachingStrategy.RecycleElement and different templates in ViewCell). You can see what I mean in the sample attached to my post.

    John, how many items do you have in that list? Is it scrolling smooth? In my case in the list which contains 100 items on my tab Sony SGP311 with CPU 1.5 Ghz I still can't achieve smooth scrolling although I'm using Adam's advice and defined both of templates in the ViewCell. Yes, in this way I solved my problem with item's order but I still can't improve the performance.

    Could you please show me small example how I can implement this feature? How can I done the long list with different item templates (in my case there are two of them). Thank you!

  • JohnHardmanJohnHardman GBUniversity mod

    @savoskin - I've bumped up the number of items in my list to 124 and put an index in the displayed text so that I can check the order. No shuffling happening on any platform.

    In terms of performance, I do most of my testing on low-end devices and am currently using a debug build. Using XF 2.2.0.45, performance is good on everything other than my low-end Windows 10 phone, but even on that it is acceptable. Prior to using DataTemplateSelector and RecycleElement, the Android performance was bad, but using DataTemplateSelector and RecycleElement has resolved that.

    I'm in the midst of re-factoring, so my code's a bit messy at the moment, but here are some relevant bits (apologies if there are any emojis in this - the editor kept sticking them in as I was typing):

    (1) The selector itself

    public class MyAppMenuItemTemplateSelector : DataTemplateSelector
    {
        protected override DataTemplate OnSelectTemplate(object item, BindableObject container)
        {
            MyAppBaseMenuItemViewModel bmi = item as MyAppBaseMenuItemViewModel;
            if ((bmi != null) && (bmi.DataTemplate != null))
            {
                bmi.ParentNonSelectableListView = container as ViewsUsingXamarinForms.MyAppNonSelectableListView;
                return bmi.DataTemplate; // each of my view classes has its own template already available
            }
            else
            {
                DataTemplate template = new DataTemplate(() =>
                {
                    View view = new Label
                    {
                        Text = string.Format("*** ERROR - Unrecognised data type : {0} ***", item.GetType()),
                        FontSize = Device.GetNamedSize(NamedSize.Default, typeof(Label))
                    };
    
                    ViewCell vc = new ViewCell
                    {
                        View = view
                    };
    
                    return vc;
                });
    
                return template;
            }
        }
    }
    

    (2) I subclass ListView (actually I subclass another subclass that I use to prevent cell selection):

    public class MyAppMenuAsNonSelectableListView : MyAppNonSelectableListView
    {
        public MyAppMenuAsNonSelectableListView(
            IMyAppViewState MyAppViewState,
            MyAppMenuViewModel menuViewModel,
            ObservableCollection<MyAppBaseDataModel> menuItemsDataModel,
            ObservableCollection<MyAppBaseMenuItemViewModel> menuItemsViewModel,
            BaseContentPage<BaseViewModel> thisPage,
            IDoubleTapBlocker doubleTapBlocker) : base(ListViewCachingStrategy.RecycleElement)
        {
            MyAppMenuSectionViewModel.DataTemplateSubclass 
                = new MyAppMenuSectionTemplatedView();
    
            // Use the above pattern for all of the other data template subclasses here...
    
    
            // then set the listview properties
    
            BackgroundColor = Color.Aqua; // temporary to make it obvious when things are slow
            BindingContext = menuViewModel;
            ClassId = "MyApp_ClassId_StackLayout";
            DataModel = menuItemsDataModel;
            ViewModel =
                new ObservableCollection<MyAppBaseMenuItemViewModel>(
                    menuViewModel.ItemsSource as IEnumerable<MyAppBaseMenuItemViewModel>);
            HasUnevenRows = true;
            HorizontalOptions = LayoutOptions.Fill;
            IsPullToRefreshEnabled = false;
            ItemTemplate = new MyAppMenuItemTemplateSelector(),
            SeparatorVisibility = SeparatorVisibility.None; // Note that I include my own separators within my ViewCells
            VerticalOptions = LayoutOptions.StartAndExpand;
            Footer = menuViewModel;
            FooterTemplate = new MyAppMenuFooterTemplate();
            Header = menuViewModel;
            HeaderTemplate = new MyAppMenuHeaderTemplate();
            menuTitle = menuViewModel.Title;
            ItemsSource = ViewModel;
            ItemTapped += OnItemTapped;
            menuItemsDataModel.CollectionChanged += (sender, e) =>
            {
                switch (e.Action)
    
                // more code here
    

    (3) Each of my view models subclasses a common base class, and provides access to the appropriate data template subclass

    public class MyAppMenuSectionViewModel : MyAppBaseMenuItemViewModel
    {
        public static DataTemplate DataTemplateSubclass { get; set; }
    
        public override DataTemplate DataTemplate
        {
            get { return DataTemplateSubclass; }
        }
    
        // More code follows
    

    (4) A simple view class looks like this

    public class MyAppMenuSectionTemplatedView : DataTemplate
    {
        public MyAppMenuSectionTemplatedView() :
            base(() =>
            {
                MyAppLabel label = new MyAppLabel
                {
                    HorizontalTextAlignment = TextAlignment.Start,
                    LineBreakMode = LineBreakMode.WordWrap,
                    VerticalTextAlignment = DeviceWrapper.OnPlatform(TextAlignment.End, TextAlignment.Center, TextAlignment.Center, TextAlignment.End)
                };
    
                label.SetBinding(MyAppLabel.AutomationIdProperty,
                    new Binding(
                        nameof(MyAppMenuSectionViewModel.AutomationId),
                        BindingMode.OneWay,
                        MyAppAppendTextConverter.Instance,
                        "_Label",
                        null));
    
                label.SetBinding(VisualElement.BackgroundColorProperty,
                    nameof(MyAppMenuSectionViewModel.BackgroundColor),
                    BindingMode.OneWay);
    
                label.SetBinding(Label.FontAttributesProperty,
                    nameof(MyAppMenuSectionViewModel.FontAttributes),
                    BindingMode.OneWay);
    
                label.SetBinding(Label.FontFamilyProperty,
                    nameof(MyAppMenuSectionViewModel.FontFamily),
                    BindingMode.OneWay);
    
                label.SetBinding(Label.FontSizeProperty,
                    nameof(MyAppMenuSectionViewModel.FontSize),
                        BindingMode.OneWay);
    
                label.SetBinding(Label.TextProperty,
                    nameof(MyAppMenuSectionViewModel.Text),
                    BindingMode.OneWay,
                    MyAppTextToUpperCaseConverter.Instance);
    
                label.SetBinding(Label.TextColorProperty,
                    nameof(MyAppMenuSectionViewModel.TextColor),
                    BindingMode.OneWay);
    
                MyAppStackLayout tc = new MyAppStackLayout
                {
                    HorizontalOptions = LayoutOptions.Fill,
                    Padding = DeviceWrapper.OnPlatform(
                        new Thickness(10, 30, 10, 4),
                        new Thickness(10, 25, 10, 4),
                        new Thickness(10, 25, 10, 4),
                        new Thickness(10, 25, 10, 4)),
                    VerticalOptions = LayoutOptions.CenterAndExpand,
                    Children =
                    {
                        label
                    }
                };
    
                MyAppTemplateHelpers.ApplyBindingsToStackLayout(tc);
    
                MyAppStackLayout tc2 = new MyAppStackLayout
                {
                    HorizontalOptions = LayoutOptions.FillAndExpand,
                    Margin = 0,
                    Padding = 0,
                    Spacing = 0,
                    VerticalOptions = LayoutOptions.StartAndExpand,
                    Children =
                    {
                        tc,
                        MyAppTemplateHelpers.CreateSeparator(false)
                    }
                };
                MyAppTemplateHelpers.ApplyBindingsToStackLayout(tc2, "2");
    
                ViewCell vc = new ViewCell
                {
                    View = tc2
                };
    
                return vc;
            })
        {
            // no-op
        }
    
    } // public class MyAppMenuSectionTemplatedView : DataTemplate
    

    Hope that gives you enough to point you in the right direction (well, the way that works for me anyway).

  • __savoskin____savoskin__ UAMember ✭✭

    Thank you very much, John for your answer! But, I suppose, your row's templates have the same number of children. For example: question-template has an image and answer-template has an image, so when caching strategy in use the app can re-use one cell for different items. But what happens if the first one have an image and the second - a label in their body (I mean the model/binding context of that cell)). When caching mechanism will try to re-use the cell which doesn't have an image in its template for item with image - we will get the broken view. I can see that now, because one of template has an image and a label and second - only label, so when I'm scrolling fast I see how caching mechanism is trying to re-use wrong cell for a unappropriate item and allocate, for example, place for image for item without image source. So I'm totally confused!.. Anyway, thank you, with your help it's working faster but the problem still exists.

  • JohnHardmanJohnHardman GBUniversity mod

    @savoskin - No, the templates vary a lot. They contain varying numbers of images, varying numbers of strings, expanders/contractors, hyperlinks etc. In an attempt to reduce maintenance and testing, my app uses the same chunk of code for everything from its menus, to static information pages (e.g. terms & conditions), to settings pages, to dynamic & complex data views. As part of my testing, I have a single list that mixes all of those. It was that list that I bumped up to 124 items to see if I observe the same problems that you are seeing. It all worked fine.

    There would be very little point to DataTemplateSelectors if they couldn't handle variation.

  • __savoskin____savoskin__ UAMember ✭✭

    It's odd, because I see following - cell context in one case can has an Image and a label and only label in another. Then, in the OnBindingContextChanged method I check the variable and hide image if it doesn't exists, BUT when I'm scrolling I see the allocated place for the image in that case when it should be hidden. I think it worth to say that size of that image is assigned manually. Any idea?

  • JohnHardmanJohnHardman GBUniversity mod

    @savoskin - I only make use of OnBindingContextChanged when I cannot think of any other sensible way of implementing something, so most of my image-related stuff is done using bindings as part of the main template code, rather than being done in OnBindingContextChanged. From your description, you may be seeing (particularly when scrolling quickly) your cells populated with stuff from the template and bindings, with them subsequently being updated by your OnBindingContextChanged implementation or by async image population. That's one reason I want to try to get as much as possible working just from templates and bindings, rather than splitting off some code into OnBindingContextChanged.

    Even the stuff I do currently in OnBindingContextChanged, I plan to get rid of soon by adding a couple more templates (to cope with expansion and contraction of view cells, which I currently do in OnBindingContextChanged). I do find OnBindingContextChanged useful for debugging though. IMHO, it's worth putting some diagnostics in there to confirm what the BindingContext is, but that's just for debugging.

    Note that there is an issue on UWP where images are not drawn correctly ( https://bugzilla.xamarin.com/show_bug.cgi?id=36097 ), and I hit a funny with value converters on UWP and WinRT that I use for toggling between two images ( https://bugzilla.xamarin.com/show_bug.cgi?id=41266 ), so there are definitely some things to watch out for.

  • __savoskin____savoskin__ UAMember ✭✭

    From your description, you may be seeing (particularly when scrolling quickly) your cells populated with stuff from the template and bindings, with them subsequently being updated by your OnBindingContextChanged implementation or by async image population.

    • No, I don't think so. When I use OnBindingContextChanged I don't use bindings (for the same reason). I understand that it's my mistake and it's somewhere close, but I can't guess, so this forum is my last chance. I think it will be better to show you what is the cause of my headache. I've made for you a very small and simple example that can illustrate what I meant when I say that the caching mechanism trying to use wrong template or, maybe, it just don't recalculate the size of controls. Run, please this app and you'll see. Hope, you can find where I go wrong! Please.
  • JohnHardmanJohnHardman GBUniversity mod

    @savoskin - it doesn't build for me. I get the following errors:

    Severity Code Description Project File Line Suppression State
    Error CS0103 The name 'InitializeComponent' does not exist in the current context Example G:\Example\Example\Example\Controls\AnswerCell.xaml.cs 9 Active
    Error CS0103 The name 'InitializeComponent' does not exist in the current context Example G:\Example\Example\Example\App.xaml.cs 9 Active
    Error CS0103 The name 'InitializeComponent' does not exist in the current context Example G:\Example\Example\Example\ExamplePage.xaml.cs 9 Active
    Error CS0103 The name 'list' does not exist in the current context Example G:\Example\Example\Example\ExamplePage.xaml.cs 10 Active
    Error CS0103 The name 'InitializeComponent' does not exist in the current context Example G:\Example\Example\Example\Controls\QuestionCell.xaml.cs 9 Active

    I have other commitments tomorrow, but if you can sort out whatever is causing those errors, I'll try to take another look later in the week.

  • __savoskin____savoskin__ UAMember ✭✭

    It seems you are trying to build it in VS. But as far as I know VS doesn't load Xamarin Studio projects correctly. So, I've rebuild it for you in VS2015, see attachment.

  • JohnHardmanJohnHardman GBUniversity mod

    @savoskin - Nice sample :-)

    Unfortunately, the documentation for OnBindingContextChanged is not particularly helpful. All it says is "Invoked whenever the binding context of the View changes. Override this method to add class handling for this event". As a result, without ploughing through the Xamarin.Forms source code (which is also short on comments), it's hard to be definitive about what is supposed to happen.

    What does happen is that:
    QuestionCell.OnBindingContextChanged may be called with the BindingContext changing to either another Question or to an Answer
    AnswerCell.OnBindingContextChanged may be called with the BindingContext changing to either another Answer or to a Question.

    Within my app's code, I check the type of the new BindingContext before making use of it. I haven't encountered any issues doing this, but as mentioned previously, I do intend to get rid of even that code by adding more DataTemplates. Using OnBindingContextChanged was a bit of a last resort, but one that I now know how to avoid.

    It would still be useful to get a clearer explanation of how OnBindingContextChanged is expected to be used. This is probably a question for @TheRealJasonSmith

    I did add a pile of diagnostics to your code. It didn't show anything that I wouldn't expect.

    I've included a couple of snapshots of diagnostics below, although I added more after this too. Running with these bits in place, anything labelled "My Answer" or "Changed to My Answer" appears where I would expect, and anything labelled "My Question" (never saw one labelled "Changed to My Question", but I would expect to if carried on testing long enough) appears where I would expect it.

    I changed the QuestionCell.OnBindingContextChanged() to:

        protected override void OnBindingContextChanged()
        {
            base.OnBindingContextChanged();
    
            if (this.BindingContext == null)
            {
                System.Diagnostics.Debug.WriteLine("QuestionCell BindingContext = {null}");
            }
    
            System.Diagnostics.Debug.WriteLine("QuestionCell BindingContext = {0}", this.BindingContext.GetType());
    
            if (!(this.BindingContext is Example.ListRowModel))
                throw new InvalidDataException("this.BindingContext for QuestionCell is NOT Example.ListRowModel");
            else
            {
                ListRowModel lrm = this.BindingContext as ListRowModel;
                if (!lrm.IsBot)
                    lrm.Message = "Changed to My Answer";
                else
                {
                    lrm.Message = "My Question";
                }
            }
        }
    

    And changed the AnswerCell.OnBindingContextChanged() to:

        protected override void OnBindingContextChanged()
        {
            base.OnBindingContextChanged();
    
            if (this.BindingContext == null)
            {
                System.Diagnostics.Debug.WriteLine("AnswerCell BindingContext = {null}");
            }
    
            System.Diagnostics.Debug.WriteLine("AnswerCell BindingContext = {0}", this.BindingContext.GetType());
    
            if (!(this.BindingContext is Example.ListRowModel))
                throw new InvalidDataException("this.BindingContext for AnswerCell is NOT Example.ListRowModel");
            else
            {
                ListRowModel lrm = this.BindingContext as ListRowModel;
                if (lrm.IsBot)
                    lrm.Message = "Changed to My Question";
                else
                {
                    lrm.Message = "My Answer";
                }
            }
        }
    

    Scrolling back and forth through the list shows anything with "My Question" (or hopefully "Changed to My Question") on the left, and anything with "My Answer" or "Changed to My Answer" on the right.

  • __savoskin____savoskin__ UAMember ✭✭

    John, thank you very much for your time and help. You was absolutely right - there is no problem with context or DataTemplateSelector. I've found that the cause of that strange behavior was the StackLayout in the question's template which, for some reason, doesn't recalculate its size when context change. The problem was solved when I changed it to the Grid. So, thank you very much for your help!

    I see, you are well experienced in Xamarin Forms, so can I ask one more question? :) - Why some of images which situated on the server are not showing although they are available through the browser? The cause is lying in their size (XF can't scale them)? And do you use some kind of cache or "Object Pool Pattern" for your images when use ListViewCachingStrategy.RecycleElement or your load them each time from the server?

  • __savoskin____savoskin__ UAMember ✭✭

    It seems that I need to write a custom control which will hold the image itself and progress bar (will be displayed till image download). And, in additional, I need to write my custom cache based on weak references to hold image source. It's pity that there is no existing solution for this purpose and I forced to do a lot of routine work. Thank you very much, John! You've saved me!

  • JohnHardmanJohnHardman GBUniversity mod

    savoskin - Do take a look at https://github.com/luberda-molinet/FFImageLoading in case it does some of what you need

  • __savoskin____savoskin__ UAMember ✭✭

    oh, thanks, I will definitely look on it!

  • hashmuralihashmurali Member
    edited March 4

    i have a similar problem when adding a grid inside a viewcell in a listview. The problem is on scrolling the listview sometimes i can see duplicate items with truncation. Issue present in both Android and iOS.

Sign In or Register to comment.