Expandable ListView

Xamarin.Forms Expandable ListView

This demo explain how to create a expandable ListView with MVVM pattern using Xamarin.Forms. The ideal is from this blog. If you prefer first to know how to create a expandable ListView without MVVM pattern, please read the blog, and then go back to read this one.
The following gif shows the result of this demo.

Model

An easy example, a continent include many countries, so here we create two models, Continent and Country. Continent has a Name and a list with several countries. Country has three property, Name,Code and Flag.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Continent  
{
public string Name { get; set; }
public List<Country> Countries { get; set; } = new List<Country>();
}


public class Country
{
public string Name { get; set; }
public string Code { get; set; }
public string Flag { get; set; }
}

View

The view is the page, layout, UI controls of what a user sees on the screen. In this example is the ExamplePage.

ViewModel

For exposing public properties of a model and some other properties that are useful for presenting , we need a ViewModel for each Model. Country model corresponds to a CountryViewModel and Continent corresponds to a ContinentViewModel.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class CountryViewModel
{
private Country _country;

public CountryViewModel(Country country)
{
this._country = country;
}

public string Name { get { return _country.Name; } }
public string Code { get { return _country.Code; } }
public string Flag { get { return _country.Flag; } }
}

ContinentViewModel has a Expanded property which keeps track on whether the continent is expanded or collapsed. StateIcon will be an expaned icon when the list is expanded and collapsed when it’s not.

ContinentViewModel implements INotifyPropertyChanged interfacce so that any changes on properties will be reflected in our list view. For example, when property Expanded changes, the StateIcon should be changed too, so we need to notify our ListView that the both properties are changed by calling OnPropertyChanged methods.

1
2
OnPropertyChanged(new PropertyChangedEventArgs("Expanded"));
OnPropertyChanged(new PropertyChangedEventArgs("StateIcon"));

Notice that ContinentViewModel must be subclass of ObservableCollection with generic type of CountryViewModel. Items of the ObservableCollection will be shown on the ListView. Here I use a light-weight MVVM helper classes named MVVMHelpers of James, it has a awesome ObservableRangeCollection which is a subclass of ObservableCollection and can add, remove or replace a range of object. More detail on this video.

Constuctor of ContinentViewModel takes a Continent object which contains a country list and set Expanded by default to be true. In the constuctor method, a backup variable for storing CountryViewModel obejcts is populated with the countries of Continent. This backup variable is added to the ContinentViewModel if Expanded property is true.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class ContinentViewModel : ObservableRangeCollection<CountryViewModel>, INotifyPropertyChanged  
{
private Continent _continent;

// It's a backup variable for storing CountryViewModel objects
private ObservableRangeCollection<CountryViewModel> Countries
= new ObservableRangeCollection<CountryViewModel>();

public ContinentViewModel(Continent continent, bool expanded = true)
{
this._continent = continent;
this._expanded = expanded;
// Continent has many countries. Once we get it, init CountryViewModel and store it in a backup variable
foreach (Country c in continent.Countries) {
Countries.Add(new CountryViewModel(c));
}
// ContinentViewModel add a range with CountryViewModel
if(expanded)
this.AddRange(Countries);
}

public string Name { get { return _continent.Name; } }

private bool _expanded;
public bool Expanded
{
get { return _expanded; }
set
{
if (_expanded != value)
{
_expanded = value;
OnPropertyChanged(new PropertyChangedEventArgs("Expanded"));
OnPropertyChanged(new PropertyChangedEventArgs("StateIcon"));
if (_expanded)
{
this.AddRange(Countries);
}
else {
this.Clear();
}
}
}
}

public string StateIcon
{
get { return Expanded ? "up" : "down"; }
}
}

Beside the above two ViewModels, another ViewModel is also required—ExamplePageViewModel corresponds to ExamplePage. Most of logic to ExamplePage should be handled in ExamplePageViewModel but not in ExamplePage.xaml.cs. ExamplePage sets the BindingContext to the ExamplePageViewModel.
Once the ExamplePage is appearing, it calls OnAppearing methods, this is the apporiate place to load data from server. This job belongs to our ExamplePageViewModel, and it simply execute its command to load data. There is another method is used for handling property changed event. If the StateIcon has changed and new icon is inflated, it performs fade animation. Now nothing more code should be here, and logic handling should be in ExamplePageViewModel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public partial class ExamplePage : ContentPage
{
private ExamplePageViewModel ViewModel {
get { return (ExamplePageViewModel)BindingContext; }
set { BindingContext = value; }
}

public ExamplePage(ExamplePageViewModel viewModel)
{
InitializeComponent();
this.ViewModel = viewModel;
}

protected override void OnAppearing()
{
base.OnAppearing();
ViewModel.LoadDataCommand.Execute(null);
}

private void StateImage_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName.Equals("Source")) {
var image = sender as Image;
image.Opacity = 0;
image.FadeTo(1, 1000);
}
}
}

ExamplePageViewModel has a List of type ObservableRangeCollection and two commands. List is the data source for ListView. LoadDataCommand is for loading data from server and HeaderClickCommand is for Continent click event handling.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ExamplePageViewModel
{
public ObservableRangeCollection<ContinentViewModel> List { get; private set; }
= new ObservableRangeCollection<ContinentViewModel>();

public ICommand LoadDataCommand { get; private set; }
public ICommand HeaderClickCommand { get; private set; }

public ExamplePageViewModel()
{
this.LoadDataCommand = new Command(async () => await ExecuteLoadDataCommand());
this.HeaderClickCommand = new Command<ContinentViewModel>((item) => ExecuteHeaderClickCommand(item));
}

private async Task ExecuteLoadDataCommand()
{
...
...
}

private void ExecuteHeaderClickCommand(ContinentViewModel item) {
item.Expanded = !item.Expanded;
}
}

Data Binding

ExamplePage contains only a ListView displayed with customized group header and item. Group header template is a view of RelativeLayout, it shows a Lable and an Image. The TapGestureRecognizer of the RelativeLayout allows to click the header and expand or collapse the ListView. Once user click the group header, HeaderClickCommand will be executed and the Expanded property of ContinentViewModel will be updated. Since we have written the Set method of property Expanded, it changes the content of ContinentViewModel—add a range or clear itself.

The bindable property ItemSource of ListView is bound to List which is a public property of ExamplePageViewModel.
The GroupHeaderTemplate of ListView is bound to each item of List object, which is a ContinentViewModel object.
The ItemTemplate of ListView is bound to each item of the ContinentViewModel object, which is a CountryViewModel object.


Source Code is here

package used in this example:

Reference : http://www.compliancestudio.io/blog/xamarin-forms-expandable-listview