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

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