In today’s post we will take a look at some binding examples to ListView and how improperly handled model data in view-model can cause performance penalties and effect the UX experience. Especially, if you are dealing grouped collections and sections on the view-model, it becomes even more difficult to handle the collection change events.
Let us start with a simple example where we retrieve the model data from a remote service and display the enumerable in a list view.
MVVM and Service Data
Simple service implementation retrieves an enumerable data:
public interface IPlayerService { Task<IEnumerable<Person>> GetPlayersAsync(); }
The the update command/method pair used to update the exposed property in the View-Model:
public AllContactsViewModel() { _editCommand = new Command(() => UpdatePlayer()); _updateCommand = new Command(async () => await UpdatePlayers()); UpdatePlayers().ConfigureAwait(false); } private async Task UpdatePlayers() { IEnumerable<Person> serviceResult = Enumerable.Empty<Person>(); try { serviceResult = await _service.GetPlayersAsync(); } catch(Exception ex) { // TODO: } if(serviceResult?.Any()??false) { AllContacts = new List<Person>(serviceResult); } }
And finally the AllContacts list is bound to our list view:
<ListView x:Name="allContacts" ItemsSource="{Binding AllContacts, Mode=TwoWay}" HasUnevenRows="true" SeparatorVisibility="None">
Notice that, every time the UpdateCommand is called a service call is going to be executed and the list is going to be reassigned.
Now, let us take a look at the instruments run with Time Profile using this implementation:
In this run, after the initial application load, we used the UpdateCommand 3 times. These peeks in the CPU usage are clearly shown in the graph. Another important thing to notice is that instruments identified the costliest execution tree to be the one that contains layoutSubViews method call on the UITableView instance. And we can only assume that this layout request is triggered by a data reload on the table view instance:
As explained in the apple guidelines, this method should not be called if our intention is not actually to refresh the whole list. If we follow the code from the ListViewRenderer implementation perspective, you will notice that Reload is called when the collection changed is called with NotifyCollectionChangedAction.Reset.
MVVM and ObservableCollection
So what can be done in this case. Let us assume, our data items are immutable (used more semantically: we will not have data updates in the list, only inserts), and we are just responsible for adding the new items to the list. In this scenario, we can use ObservableCollection to add the new service data into the list (after changing the AllContacts type to ObservableCollection:
if(serviceResult?.Any()??false) { var newItems = serviceResult.Where(item => !AllContacts.Any(contact => item.Name == contact.Name)); foreach(var item in newItems) { AllContacts.Add(item); } }
The service results are analyzed for new contacts (i.e comparison is made on basis of Name), and new items are added to observable collection.
Now looking at the instrument run, the most costly execution tree is shown as the one that handles the tap event on the update button.
Moreover, if you look at the graph carefully, you will notice that, instead of reload data, end updates is triggered on the UITableView. This means the ListViewRenderer.CollectionChanged actually is triggered possibly with NotifyCollectionChangedAction.Add.
But what if we expect the data to be changing on the list view items. If there are no special template (with a template selector) you are using to display the list view items (in other words if you don’t require a redraw on the cell level), updating the properties of the items in the observable collection should be enough to trigger a redraw on the data control level (e.g. only the label/entry to be updated).
So modifying the update method we would have something similar to:
if(serviceResult?.Any()??false) { var newItems = serviceResult.Where(item => !AllContacts.Any(contact => item.Name == contact.Name)); var updatedItems = serviceResult.Where(item => AllContacts.Any(contact => item.Name == contact.Name)); foreach (var updateItem in updatedItems) { var existingItem = AllContacts.FirstOrDefault(contact => contact.Name == updateItem.Name); existingItem.LoadData(updateItem); } foreach(var item in newItems) { AllContacts.Add(item); } }
And the resultant view looks similar to:
NOTE:In the example above, we have a custom effect implemented which will set the background to a different color according to the rendering time. |
Notice that, only the newly added items are actually drawn with a different background color, while the updated data on the items didn’t cause a full refresh.
Of course, it is even nicer if we can consolidate this implementation into a custom ObservableCollection so the View-Models responsibility would be to simply add the new range of items. A simple implementation of this can be found at ObservableRangeCollection
In the next part of the implementation we will use the ObservableListCollection implementation:
/// <summary> /// ObservableCollection implementation for updating and adding items /// </summary> /// <typeparam name="T">Entity type</typeparam> public class ObservableListCollection<T> : ObservableCollection<T> { private IEqualityComparer<T> _equivalenceComparer; private Func<T, T, bool> _updateFunction; /// <summary> /// Initializes a new instance of the <see cref="ObservableListCollection{T}"/> class. /// </summary> public ObservableListCollection() : base() { } /// <summary> /// Initializes a new instance of the <see cref="ObservableListCollection{T}"/> class. /// </summary> /// <param name="collection">collection: The collection from which the elements are copied.</param> /// <exception cref="System.ArgumentNullException">The collection parameter cannot be null.</exception> public ObservableListCollection(IEnumerable<T> collection, IEqualityComparer<T> equivalence = null, Func<T,T,bool> updateCallback = null) : base(collection) { _equivalenceComparer = equivalence ?? EqualityComparer<T>.Default; _updateFunction = updateCallback; } /// <summary> /// Updates or adds the elements of the specified collection to the end of the ObservableCollection(Of T). /// </summary> /// <param name="collection"> /// The collection to be added. /// </param> public void UpdateRange(IEnumerable<T> collection) { if (collection == null) { throw new ArgumentNullException(nameof(collection)); } CheckReentrancy(); int startIndex = Count; var updatedItems = collection.Where(item => Items.Any(contact => _equivalenceComparer.Equals(contact, item))).ToList(); bool anyItemUpdated = false; foreach (var updateItem in updatedItems) { var existingItem = Items.FirstOrDefault(contact => _equivalenceComparer.Equals(contact, updateItem)); // TODO: We can fire NotifyCollectionChanged.Update if needed depending on anyItemUpdated. anyItemUpdated = anyItemUpdated | _updateFunction?.Invoke(existingItem, updateItem) ?? false; } var newItems = collection.Where(item => !Items.Any(contact => _equivalenceComparer.Equals(contact, item))).ToList(); if(!newItems.Any()) { return; } foreach (var item in newItems) { Items.Add(item); } OnPropertyChanged(new PropertyChangedEventArgs("Count")); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newItems, startIndex)); } }
In this implementation while a custom implementation of IEqualityComparer can be used to identify which item to update (e.g. in our example we were using “Name” property as the primary key for the data items), updateCallback function is used to inject the update method from the view model.
MVVM and Grouped Data
Now let us imagine that we want to have sections in our list view and the service data returned is a flat enumerable. In this case, every time we load the data, we would need to group the items according to specifications (i.e. a selector function) and assign our grouped collection back to our view model.
For this purpose, let us first define our view model outlet for the grouped collection:
public IEnumerable<IGrouping<string, Person>> GroupedContacts { get { return _groupedContacts; } set { SetProperty(ref _groupedContacts, value); } }
Then we set up the load method to group the items once they are retrieved from the service:
private async Task UpdatePlayers() { IEnumerable<Person> serviceResult = Enumerable.Empty<Person>(); try { serviceResult = await _service.GetPlayersAsync(); } catch(Exception ex) { // TODO: } if(serviceResult?.Any()??false) { _allContacts.UpdateRange(serviceResult); GroupedContacts = serviceResult.GroupBy(item => item.Position); } }
Once the view model is set to display a grouped list, we can update our ListView with the grouped binding:
<ListView x:Name="allContacts" ItemsSource="{Binding GroupedContacts, Mode=TwoWay}" IsGroupingEnabled="true" GroupDisplayBinding="{Binding Key}" HasUnevenRows="true" SeparatorVisibility="None">
However, just like the initial solution, every time the service call returns the flat list of data, the whole list is going to be updated instead of localized updated even though no data item has actually changed.
So let us first organize our implementation to use the ObservableCollectionList to represent a group.
public class ObservableListGroup<TKey, TItem> : ObservableListCollection<TItem> { public TKey Key { get; } public ObservableListGroup(TKey key, IEnumerable<TItem> items, IEqualityComparer<TItem> equivalence = null, Func<TItem, TItem, bool> updateCallback = null) : base(Enumerable.Empty<TItem>(), equivalence, updateCallback) { Key = key; UpdateRange(items); } }
So with this implementation, we can be sure that the list updates on the group level will be handled “properly” by the ObservableListCollection.UpdateRange method.
In order to bind the grouped data, we will need an implementation of an observable set of these list groups.
public class ObservableListGroupCollection<TKey, TItem> : ObservableListCollection<ObservableListGroup<TKey, TItem>> { private Func<TItem, TKey> _groupKeySelector; private IEqualityComparer<TItem> _equivalenceComparer; private Func<TItem, TItem, bool> _updateFunction; public ObservableListGroupCollection( IEnumerable<TItem> collection, Func<TItem,TKey> keySelector, IEqualityComparer<TItem> equivalence = null, Func<TItem, TItem, bool> updateCallback = null) { _groupKeySelector = keySelector; _equivalenceComparer = equivalence; _updateFunction = updateCallback; UpdateItems(collection); } public void UpdateItems(IEnumerable<TItem> items) { // ... } }
Notice that in this implementation, we are getting the collection related implementations such as the equivalence function and update callback. We will be using these values when we are creating the ObservableListGroups to track the collection changes in the section level. Additionally, we are using a function to identify which group does an item need to go to.
Now that we have all the attributes to be able to identify the sections and create the collection of lists, we can implement the UpdateItems method. As opposed to UpdateRange method which in this case would add or update group of items, UpdateItems takes a list of items and process the grouping.
public void UpdateItems(IEnumerable<TItem> items) { // First we group the items coming down the pipeline var grouppedItems = items.GroupBy(_groupKeySelector); var startIndex = Count; var newItems = new List<ObservableListGroup<TKey, TItem>>(); foreach(var group in grouppedItems) { var section = Items.FirstOrDefault(item => item.Key.Equals(group.Key)); if(section == null) { // Create the observable group if it does not exists var newGroup = new ObservableListGroup<TKey, TItem>(group.Key, group, _equivalenceComparer, _updateFunction); newItems.Add(newGroup); Items.Add(newGroup); } else { // Update the range of the existing group section.UpdateRange(group); } } if (newItems.Any()) { // Raise list change events if only new sections were added. OnPropertyChanged(new PropertyChangedEventArgs("Count")); OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, newItems, startIndex)); } }
There we have it, now whenever a new set of data comes, we never push a full layout update:
- Update (NotificationCollectionChangedAction.Add) the groups list when a new section is added
- Update (NotificationCollectionChangedAction.Add) the list inside a group when new items are added
- We don’t invoke a collection changed event for items that updated by the incoming data
This implementation does not consider items changing their group (e.g. in this example if the position attribute of a player changes, he should be moved to another group), nor it considers the items that are deleted (e.g. if the data item does not exist in the service response but exists in the old list of items).
The resulting list looks similar to:
On the you can see the initially loaded data (i.e. 2 groups and 3 player information), and on the right you can see the result after 3 data updates received from the server (i.e. each service call in this emulated scenario returning one additional data item). You can see that first two updates only added 2 new items to the existing section of “Defender”, and the third update returned the additional that cause the new group creation. During this 3 update cycles, the initial 2 groups and 3 players were not re-rendered (see the color code).
Hope you enjoyed this implementation. Please be aware that this implementation neglects lots of details about deleted items and data item updates. It is also important to mention that while the behavior will be similar on android, this post was written only with iOS in mind.
Happy coding everyone,
Really great write up, thank you for this!
Any chance of you sharing that custom effect for changing the background color based on rendering time?
LikeLike
ah the background color was just an addendum to the code from my previous post https://canbilgin.wordpress.com/2018/02/11/add-list-view-drop-shadow-on-xamarin-forms/ that adds the background color to the container based on mod10 of DateTime.Now.Second:
var color = GetColor();
Container.Layer.BackgroundColor = new CGColor((nfloat)color.R, (nfloat)color.G, (nfloat)color.B, (nfloat)0.2);
LikeLike
Cool thanks
LikeLike
Pingback: Dew Drop - February 26, 2018 (#2672) - Morning Dew
Pingback: Re-Order ListView Items with Drag & Drop – I | Can Bilgin
Why don’t you share the code ?
LikeLike