Xamarin.Forms - Expandable ListView With A Sub ListView MVVM Pattern

In mobile applications we often have lists to display, this is easy only if the list contains simple and not complex structure. This article demonstrates how we create a complex Expandable ListView with a sub ListView in Xamarin.Forms.. This Expandable List allows user to Expand and Collapse items via a simple click. We have a List of Hotels each hotel contains a list of rooms. One click on a hotel will display its rooms while a double click will collapse it.

Introduction

In mobile applications we often have lists to display, this is easy only if the list contains a simple and not complex structure.  This article demonstrates how we create a complex Expandable ListView with a sub ListView in  Xamarin.Forms.. This Expandable List allows user to Expand and Collapse items via a simple click. We have a list of hotels and each hotel contains a list of rooms. One click on a hotel will display its rooms while a double click will collapse it.

To do that follow the steps below,

Step 1

Create a new Xamarin Forms project with a .NetStandard or shared project.

Project Name - ListViewWithSubListView

 

Xamarin

 

Step 2 - Add MVVM folders and Classes

Open Solution Explorer >> ListViewWithSubListView ( .NetStandard or shared) project  and add the following folders :

  1. Models
  2. Views
  3. ViewModels

Step 3 - Add Models classes

Create a new Class named “Hotel”

CS code

 

  1. public class Hotel {  
  2.     public string Name {  
  3.         get;  
  4.         set;  
  5.     }  
  6.     public List < Room > Rooms {  
  7.         get;  
  8.         set;  
  9.     }  
  10.     public bool IsVisible {  
  11.         get;  
  12.         set;  
  13.     } = false;  
  14.     public Hotel() {}  
  15.     public Hotel(string name, List < Room > rooms) {  
  16.         Name = name;  
  17.         Rooms = rooms;  
  18.     }  

 

Create a new Class named “Room”

CS code

 

  1. public class Room {  
  2.     public string RoomName {  
  3.         get;  
  4.         set;  
  5.     }  
  6.     public int TypeID {  
  7.         get;  
  8.         set;  
  9.     }  
  10.     public Room() {}  
  11.     public Room(string name, int typeID) {  
  12.         RoomName = name;  
  13.         TypeID = typeID;  
  14.     }  
  15. }  

Step 4 - Add ViewModels Classes

Create a new Class named “RoomViewModel”

Cs Code

 

  1. public class RoomViewModel {  
  2.     private Room _room;  
  3.     public RoomViewModel(Room room) {  
  4.         _room = room;  
  5.     }  
  6.     public string RoomName {  
  7.         get {  
  8.             return _room.RoomName;  
  9.         }  
  10.     }  
  11.     public int TypeID {  
  12.         get {  
  13.             return _room.TypeID;  
  14.         }  
  15.     }  
  16.     public Room Room {  
  17.         get => _room;  
  18.     }  
  19. }  

 

Create a new Class named “HotelViewModel”

The hotel view Model is an observable collection of RoomViewModel. I used  the MVVMHelpers plugin to inherit from ObservableRangeCollection,

  1. public class HotelViewModel: ObservableRangeCollection < RoomViewModel > , INotifyPropertyChanged {  
  2.     // It's a backup variable for storing CountryViewModel objects  
  3.     private ObservableRangeCollection < RoomViewModel > hotelRooms = new ObservableRangeCollection < RoomViewModel > ();  
  4.     public HotelViewModel(Hotel hotel, bool expanded = false) {  
  5.         Hotel = hotel;  
  6.         _expanded = expanded;  
  7.         foreach(Room room in hotel.Rooms) {  
  8.             Add(new RoomViewModel(room));  
  9.         }  
  10.         if (expanded) AddRange(hotelRooms);  
  11.     }  
  12.     public HotelViewModel() {}  
  13.     private bool _expanded;  
  14.     public bool Expanded {  
  15.         get {  
  16.             return _expanded;  
  17.         }  
  18.         set {  
  19.             if (_expanded != value) {  
  20.                 _expanded = value;  
  21.                 OnPropertyChanged(new PropertyChangedEventArgs("Expanded"));  
  22.                 OnPropertyChanged(new PropertyChangedEventArgs("StateIcon"));  
  23.                 if (_expanded) {  
  24.                     AddRange(hotelRooms);  
  25.                 } else {  
  26.                     Clear();  
  27.                 }  
  28.             }  
  29.         }  
  30.     }  
  31.     public string StateIcon {  
  32.         get {  
  33.             if (Expanded) {  
  34.                 return "arrow_a.png";  
  35.             } else {  
  36.                 return "arrow_b.png";  
  37.             }  
  38.         }  
  39.     }  
  40.     public string Name {  
  41.         get {  
  42.             return Hotel.Name;  
  43.         }  
  44.     }  
  45.     public Hotel Hotel {  
  46.         get;  
  47.         set;  
  48.     }  
  49. }  

 

HotelViewModel contains a StateIcon and Expanded properties used to expand or Collapse a hotel via INotifyPropertieChanged.

To represent the list of hotels we will add a new view Model Named HotelsGroupViewModel

CS code

 

  1. public class HotelsGroupViewModel: BaseViewModel {  
  2.     private HotelViewModel _oldHotel;  
  3.     private ObservableCollection < HotelViewModel > items;  
  4.     public ObservableCollection < HotelViewModel > Items {  
  5.         get => items;  
  6.         set => SetProperty(ref items, value);  
  7.     }  
  8.     public Command LoadHotelsCommand {  
  9.         get;  
  10.         set;  
  11.     }  
  12.     public Command < HotelViewModel > RefreshItemsCommand {  
  13.         get;  
  14.         set;  
  15.     }  
  16.     public HotelsGroupViewModel() {  
  17.         items = new ObservableCollection < HotelViewModel > ();  
  18.         Items = new ObservableCollection < HotelViewModel > ();  
  19.         LoadHotelsCommand = new Command(async () => await ExecuteLoadItemsCommandAsync());  
  20.         RefreshItemsCommand = new Command < HotelViewModel > ((item) => ExecuteRefreshItemsCommand(item));  
  21.     }  
  22.     public bool isExpanded = false;  
  23.     private void ExecuteRefreshItemsCommand(HotelViewModel item) {  
  24.         if (_oldHotel == item) {  
  25.             // click twice on the same item will hide it  
  26.             Expanded = !item.Expanded;  
  27.         } else {  
  28.             if (_oldHotel != null) {  
  29.                 // hide previous selected item  
  30.                 Expanded = false;  
  31.             }  
  32.             // show selected item  
  33.             Expanded = true;  
  34.         }  
  35.         _oldHotel = item;  
  36.     }  
  37.     async System.Threading.Tasks.Task ExecuteLoadItemsCommandAsync() {  
  38.         try {  
  39.             if (IsBusy) return;  
  40.             IsBusy = true;  
  41.             Clear();  
  42.             List < Room > Hotel1rooms = new List < Room > () {  
  43.                 new Room("Jasmine", 1), new Room("Flower Suite", 2), new Room("narcissus", 1)  
  44.             };  
  45.             List < Room > Hotel2rooms = new List < Room > () {  
  46.                 new Room("Princess", 1), new Room("Royale", 1), new Room("Queen", 1)  
  47.             };  
  48.             List < Room > Hotel3rooms = new List < Room > () {  
  49.                 new Room("Marhaba", 1), new Room("Marhaba Salem", 1), new Room("Salem Royal", 1), new Room("Wedding Roome", 1), new Room("Wedding Suite", 2)  
  50.             };  
  51.             List < Hotel > items = new List < Hotel > () {  
  52.                 new Hotel("Yasmine Hammamet", Hotel1rooms), new Hotel("El Mouradi Hammamet", Hotel2rooms), new Hotel("Marhaba Royal Salem", Hotel3rooms)  
  53.             };  
  54.             if (items != null && items.Count > 0) {  
  55.                 foreach(var hotel in items)  
  56.                 Add(new HotelViewModel(hotel));  
  57.             } else {  
  58.                 IsEmpty = true;  
  59.             }  
  60.         } catch (Exception ex) {  
  61.             IsBusy = false;  
  62.             WriteLine(ex);  
  63.         } finally {  
  64.             IsBusy = false;  
  65.         }  
  66.     }  
  67. }  

 

This view model is used by the view and it contains commands to load and refresh the list of hotels.

Step 5 - Add Views Classes

Add a content View named “Hotel”:

The list of hotels is represented by a grouped list view; the headers are the hotels and the items are the rooms.

You shouldn’t forget to set IsGroupingEnabled="True" in the ListView declaration.

To detect the user click event I added a TapGestureRecogniser to the Grid.

XAML code

 

  1. <?xml version="1.0" encoding="utf-8" ?>  
  2. <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Name="currentPage" xmlns:local="clr-namespace:ListViewWithSubListView.Views" x:Class="ListViewWithSubListView.Views.Hotels">  
  3.     <ContentPage.Content>  
  4.         <Grid>  
  5.             <StackLayout x:Name="hotelStack" Padding="1,0,1,0">  
  6.                 <ListView x:Name="HotelsList" BackgroundColor="White" IsGroupingEnabled="True" IsPullToRefreshEnabled="true" IsRefreshing="{Binding IsBusy, Mode=OneWay}" ItemsSource="{Binding Items}" RefreshCommand="{Binding LoadHotelsCommand}">  
  7.                     <ListView.ItemTemplate>  
  8.                         <DataTemplate>  
  9.                             <ViewCell>  
  10.                                 <StackLayout Orientation="Horizontal" VerticalOptions="Center">  
  11.                                     <Label VerticalOptions="Center" FontAttributes="Bold" FontSize="Medium" Text="{Binding .RoomName}" TextColor="Black" VerticalTextAlignment="Center" /> </StackLayout>  
  12.                             </ViewCell>  
  13.                         </DataTemplate>  
  14.                     </ListView.ItemTemplate>  
  15.                     <ListView.GroupHeaderTemplate>  
  16.                         <DataTemplate>  
  17.                             <ViewCell>  
  18.                                 <Grid>  
  19.                                     <Label FontAttributes="Bold" FontSize="Small" Text="{Binding Name}" TextColor="Gray" VerticalTextAlignment="Center" />  
  20.                                     <Image x:Name="ImgA" Source="{Binding StateIcon}" Margin="0,0,5,0" HeightRequest="20" WidthRequest="20" HorizontalOptions="End" />  
  21.                                     <Grid.GestureRecognizers>  
  22.                                         <TapGestureRecognizer Command="{Binding Source={x:Reference currentPage}, Path=BindingContext.RefreshItemsCommand}" NumberOfTapsRequired="1" CommandParameter="{Binding .}" />  
  23.                                     </Grid.GestureRecognizers>  
  24.                                 </Grid>  
  25.                             </ViewCell>  
  26.                         </DataTemplate>  
  27.                     </ListView.GroupHeaderTemplate>  
  28.                 </ListView>  
  29.             </StackLayout>  
  30.         </Grid>  
  31.     </ContentPage.Content>  
  32. </ContentPage>  

 

Cs Code

Xamarin

Step 6 - Run The App

Code

 

In App.Xaml.cs Change MainPage by Hotel .   Click "F5" or "Build " to "Run" your application.Running your project, you will have the result like below.

Xamarin

 

You can download the code from my Github.

Advantages

  • 100% cross platform Expandable ListView
  • Avoid problem of nested ListView in Android platform