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
Step 2 - Add MVVM folders and Classes
Open Solution Explorer >> ListViewWithSubListView ( .NetStandard or shared) project and add the following folders :
- Models
- Views
- ViewModels
Step 3 - Add Models classes
Create a new Class named “Hotel”
CS code
- public class Hotel {
- public string Name {
- get;
- set;
- }
- public List < Room > Rooms {
- get;
- set;
- }
- public bool IsVisible {
- get;
- set;
- } = false;
- public Hotel() {}
- public Hotel(string name, List < Room > rooms) {
- Name = name;
- Rooms = rooms;
- }
Create a new Class named “Room”
CS code
- public class Room {
- public string RoomName {
- get;
- set;
- }
- public int TypeID {
- get;
- set;
- }
- public Room() {}
- public Room(string name, int typeID) {
- RoomName = name;
- TypeID = typeID;
- }
- }
Step 4 - Add ViewModels Classes
Create a new Class named “RoomViewModel”
Cs Code
- public class RoomViewModel {
- private Room _room;
- public RoomViewModel(Room room) {
- _room = room;
- }
- public string RoomName {
- get {
- return _room.RoomName;
- }
- }
- public int TypeID {
- get {
- return _room.TypeID;
- }
- }
- public Room Room {
- get => _room;
- }
- }
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,
- public class HotelViewModel: ObservableRangeCollection < RoomViewModel > , INotifyPropertyChanged {
-
- private ObservableRangeCollection < RoomViewModel > hotelRooms = new ObservableRangeCollection < RoomViewModel > ();
- public HotelViewModel(Hotel hotel, bool expanded = false) {
- Hotel = hotel;
- _expanded = expanded;
- foreach(Room room in hotel.Rooms) {
- Add(new RoomViewModel(room));
- }
- if (expanded) AddRange(hotelRooms);
- }
- public HotelViewModel() {}
- 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) {
- AddRange(hotelRooms);
- } else {
- Clear();
- }
- }
- }
- }
- public string StateIcon {
- get {
- if (Expanded) {
- return "arrow_a.png";
- } else {
- return "arrow_b.png";
- }
- }
- }
- public string Name {
- get {
- return Hotel.Name;
- }
- }
- public Hotel Hotel {
- get;
- set;
- }
- }
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
- public class HotelsGroupViewModel: BaseViewModel {
- private HotelViewModel _oldHotel;
- private ObservableCollection < HotelViewModel > items;
- public ObservableCollection < HotelViewModel > Items {
- get => items;
- set => SetProperty(ref items, value);
- }
- public Command LoadHotelsCommand {
- get;
- set;
- }
- public Command < HotelViewModel > RefreshItemsCommand {
- get;
- set;
- }
- public HotelsGroupViewModel() {
- items = new ObservableCollection < HotelViewModel > ();
- Items = new ObservableCollection < HotelViewModel > ();
- LoadHotelsCommand = new Command(async () => await ExecuteLoadItemsCommandAsync());
- RefreshItemsCommand = new Command < HotelViewModel > ((item) => ExecuteRefreshItemsCommand(item));
- }
- public bool isExpanded = false;
- private void ExecuteRefreshItemsCommand(HotelViewModel item) {
- if (_oldHotel == item) {
-
- Expanded = !item.Expanded;
- } else {
- if (_oldHotel != null) {
-
- Expanded = false;
- }
-
- Expanded = true;
- }
- _oldHotel = item;
- }
- async System.Threading.Tasks.Task ExecuteLoadItemsCommandAsync() {
- try {
- if (IsBusy) return;
- IsBusy = true;
- Clear();
- List < Room > Hotel1rooms = new List < Room > () {
- new Room("Jasmine", 1), new Room("Flower Suite", 2), new Room("narcissus", 1)
- };
- List < Room > Hotel2rooms = new List < Room > () {
- new Room("Princess", 1), new Room("Royale", 1), new Room("Queen", 1)
- };
- List < Room > Hotel3rooms = new List < Room > () {
- new Room("Marhaba", 1), new Room("Marhaba Salem", 1), new Room("Salem Royal", 1), new Room("Wedding Roome", 1), new Room("Wedding Suite", 2)
- };
- List < Hotel > items = new List < Hotel > () {
- new Hotel("Yasmine Hammamet", Hotel1rooms), new Hotel("El Mouradi Hammamet", Hotel2rooms), new Hotel("Marhaba Royal Salem", Hotel3rooms)
- };
- if (items != null && items.Count > 0) {
- foreach(var hotel in items)
- Add(new HotelViewModel(hotel));
- } else {
- IsEmpty = true;
- }
- } catch (Exception ex) {
- IsBusy = false;
- WriteLine(ex);
- } finally {
- IsBusy = false;
- }
- }
- }
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
- <?xml version="1.0" encoding="utf-8" ?>
- <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">
- <ContentPage.Content>
- <Grid>
- <StackLayout x:Name="hotelStack" Padding="1,0,1,0">
- <ListView x:Name="HotelsList" BackgroundColor="White" IsGroupingEnabled="True" IsPullToRefreshEnabled="true" IsRefreshing="{Binding IsBusy, Mode=OneWay}" ItemsSource="{Binding Items}" RefreshCommand="{Binding LoadHotelsCommand}">
- <ListView.ItemTemplate>
- <DataTemplate>
- <ViewCell>
- <StackLayout Orientation="Horizontal" VerticalOptions="Center">
- <Label VerticalOptions="Center" FontAttributes="Bold" FontSize="Medium" Text="{Binding .RoomName}" TextColor="Black" VerticalTextAlignment="Center" /> </StackLayout>
- </ViewCell>
- </DataTemplate>
- </ListView.ItemTemplate>
- <ListView.GroupHeaderTemplate>
- <DataTemplate>
- <ViewCell>
- <Grid>
- <Label FontAttributes="Bold" FontSize="Small" Text="{Binding Name}" TextColor="Gray" VerticalTextAlignment="Center" />
- <Image x:Name="ImgA" Source="{Binding StateIcon}" Margin="0,0,5,0" HeightRequest="20" WidthRequest="20" HorizontalOptions="End" />
- <Grid.GestureRecognizers>
- <TapGestureRecognizer Command="{Binding Source={x:Reference currentPage}, Path=BindingContext.RefreshItemsCommand}" NumberOfTapsRequired="1" CommandParameter="{Binding .}" />
- </Grid.GestureRecognizers>
- </Grid>
- </ViewCell>
- </DataTemplate>
- </ListView.GroupHeaderTemplate>
- </ListView>
- </StackLayout>
- </Grid>
- </ContentPage.Content>
- </ContentPage>
Cs Code
Step 6 - Run The App
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.
You can download the code from my Github. or Visual Studio Market place.
Advantages
- 100% cross platform Expandable ListView
- Avoid problem of nested ListView in Android platform