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. or Visual Studio Market place.

Advantages