Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / Android

Sectioned ListView for Android Using Mono C#

4.33/5 (8 votes)
14 Mar 2013CPOL6 min read 40.2K   1.2K  
Develop a Preferences style sectioned list adapter using Android Mono C#.

Introduction

Are you trying to figure out how to place multiple ListView controls into a single layout, but get them to behave properly when displaying multiple list items? Are you having problems with them scrolling properly? This example will show you how to combine the separate ListView controls into one single ListView, and split it into subsections, each using its own ListAdapter. I should clarify, we won't actually have nested ListView controls, we will be using subsections within one ListView, and populating each list item dynamically. 

Background  

This example assumes that you are already familiar with Android, and Mono C# coding. 

I based this approach off of an example in a Wrox book, Professional Android Programming with Mono for Android and .NET/C#. This example is modified a bit from the book example. This approach will allow the scrolling behavior to work properly. The best practice is not to have more than one ListView in a single layout. Doing so causes each ListView to default to displaying one list item each, forcing separate scrolling for each ListView. This is very annoying behavior, and the desired behavior would be for each ListView to display all of its list items, and allow the parent layout to handle the scrolling. This method will allow you to achieve this behavior. I've also expanded the book example to show you how to handle the ListView.ItemClicked event to properly handle the right item type, since our example will combine multiple list item types, each sprouting from their own adapter. 

Using the code

This example will use a data model concerning types of food. Our layout will show a ListView, sectioned for each different type of food we define. First, lets define our data model:  

C#
public class MeatType
{
   private double _pricePerPound; 
   public MeatType(String name, String description, double pricePerPound)
   { 
      _name = name;
      _description = description;
      _pricePerPound = pricePerPound;
   }
   public String Name
   {
      get { return _name; } set { _name = value; } 
   }
   public String Description
   {
      get { return _description; } set { _description = value; }
   }
   public double PricePerPound
   {
      get { return _pricePerPound; } set { _pricePerPound = value; }
   }
}  

For brevity sake, we also have a VegetableType and FruitType, each with the same structure as the MeatType, but I won't list them here since their structure is the same. 

Next, we need a template to describe the layout for our food type list items. Although we will be coding a separate ListAdapter for each food type, the adapters can all use the same list item template in this example, therefore, we only need one, FoodTypeListItem.xml. This template will be a LinearLayout, with horizontal orientation, and three TextView controls to hold our three property values. 

XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
          android:layout_width="fill_parent"
          android:layout_height="wrap_content"
          android:orientation="horizontal"
          android:id="@+id/rootLayout">
      <TextView
           android:id="@+id/nameLabel"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_marginLeft="10px"
           android:width="100px"
           android:textAppearance="?android:attr/textAppearanceSmall" />
      <TextView
           android:id="@+id/descriptionLabel"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:width="150px"
           android:textAppearance="?android:attr/textAppearanceSmall" />
      <TextView
           android:id="@+id/priceLabel"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:width="50px"
           android:textAppearance="?android:attr/textAppearanceSmall" />
</LinearLayout>

This template will be used to inflate our list items. Each adapter can call this template, and populate it differently, but the look will be the same, allowing consistency in the interface presentation.

Next we need to code our list adapters, which will each extend BaseAdapter<T>. Again, for brevity, I will only show the MeatTypeListAdapter, the VegetableTypeListAdapter and FruitTypeListAdapter will both be nearly identical except for the <Type> casts.

C#
using Android.Widget;
using Android.App;
 
public class MeatTypeListAdapter : BaseAdapter<MeatType>
{
   private Activity _context;  
   private List<MeatTypes> _items;
   private int _templateResourceId;
   public MeatTypeListAdapter(Activity context, 
     int templateResourceId, List<MeatType> items) : base()
   {
       _context = context;
       _templateResourceId = templateResourceId;
       _items = items;
   }
   public override int Count { get { return _items.Count; } }
   public override MeatType this[int index] { get { return _items[index]; } }
   public override long GetItemId(int position) { return position; }
   public override View GetView(int position, View convertView, ViewGroup parent)
   {
       MeatType item = this[position];
       View view = convertView;
       if(view == null || !(view is LinearLayout))
       {
          view = _context.LayoutInflater.Inflate(_templateResourceId, parent, false);
       }
       TextView nameLabel = view.FindViewById<TextView>(Resource.Id.nameLabel);
       nameLabel.Text = item.Name;
       TextView descriptionLabel = view.FindViewById<TextView>(Resource.Id.descriptionLabel);
       descriptionLabel.Text = item.Description;
       TextView priceLabel = view.FindViewById<TextView>(Resource.Id.priceLabel);
       priceLabel.Text = item.PricePerPound.ToString("F2");
       return view;
   }
} 
The bones of the adapter is the GetView method. It takes the item at the given position, and creates a View of the type defined by the template, and populates its controls with the items property values. You have to code this method to properly handle the template you plan to use, and the item type your data is coming from. In our case, our View type is a LinearLayout, but if you use a different root layout type, or even just a base control type, then your code should reflect this type by changing the !(view is LinearLayout) to whatever your control type is, so that you provide the proper View type. This if clause allows for recycling of code at runtime, so if a LinearLayout is already passed, i.e. the same View type for consecutive calls to GetView, then inflation is not necessary. 

Next, we need to create a SectionedListAdapter to handle the multiple lists we will want to contain within our ListView control. Before we can code this adapter though, we need a ListSection class to describe the separate list subsections. The ListSection class will hold the text caption for the section, the column header names for the section, and the ListAdapter that goes with the section.

C#
using Android.Widget;
 
public class ListSection
{
   private String _caption, _columnHeader1, _columnHeader2, _columnHeader3;
   private BaseAdapter _adapter;
   public ListSection(String caption, String columnHeader1, String columnHeader2,
                      String columnHeader3, BaseAdapter adapter)
   {
      _caption = caption;
      _columnHeader1 = columnHeader1;
      _columnHeader2 = columnHeader2;
      _columnHeader3 = columnHeader3;
      _adapter = adapter;
   }
   public String Caption { get { return _caption; } set { _caption = value; } }
   public String ColumnHeader1 { get { return _columnHeader1; } set { _columnHeader1 = value; } }
   public String ColumnHeader2 { get { return _columnHeader2; } set { _columnHeader2 = value; } }
   public String ColumnHeader3 { get { return _columnHeader3; } set { _columnHeader3 = value; } }
   public BaseAdapter Adapter { get { return _adapter; } set { _adapter = value; } }
} 

Also, before we create our sectioned adapter, we need an xml template to describe our section header, or separator. You could simply use a TextView to achieve this. Setting the separator View style tag to "?android:attr/listSeparatorTextViewStyle" will place a separator border line on the bottom border of the separator View. For this example, I want the separator to also include the column headers, so the template will be a bit more complex than a simple TextView. The ListSeparator.xml looks like: 

XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
      android:id="@+id/rootLayout"
      android:layout_width="fill_parent"
      android:layout_height="wrap_content"
      android:orientation="vertical">
      <TextView
         android:id="@+id/caption"
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
         android:layout_marginTop="10px"
         android:textAppearance="?android:attr/textAppearanceSmall" />
      <LinearLayout
         android:id="@+id/columnHeaderLayout"
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"
         android:orientation="horizontal"
         style="?android:attr/listSeparatorTextViewStyle">
         <TextView
               android:id="@+id/columnHeader1"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:marginLeft="10px"
               android:width="100px"
               android:textAppearance="?android:attr/textAppearanceSmall" />
         <TextView
               android:id="@+id/columnHeader2"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:width="150px"
               android:textAppearance="?android:attr/textAppearanceSmall" />
         <TextView
               android:id="@+id/columnHeader3"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:width="50px"
               android:textAppearance="?android:attr/textAppearanceSmall" />
      </LinearLayout>
</LinearLayout> 

 You can see, we've set the inner LinearLayout, which holds our column headers, as the listSeperatorTextViewStyle, so that the entire LinearLayout gets the border line beneath it. Now we are ready to create our sectioned adapter, the SectionedListAdapter

C#
using Android.App;
using Android.Widget;
using Android.Views;
 
public class SectionedListAdapter
{
   private const int TYPE_SECTION_HEADER = 0;
   private Context _context;
   private LayoutInflater _inflater;
   private List<ListSection> _sections; 
   public SectionedListAdapter(Context context)
   {
      _context = context;
      _inflater = LayoutInflater.From(_context);
      _sections = new List<ListSection>();
   }
   public List<ListSection> Sections { get { return _sections; } set { _sections = value; } }
   
   // Each section has x list items + 1 list item for the caption. This is the reason for the +1 in the tally
   public override int Count
   {
      get
      {
         int count = 0;
         foreach (ListSection s in _sections) count += s.Adapter.Count + 1;
         return count;
      }
   }
   
   // We know there will be at least 1 type, the seperator, plus each
   // type for each section, that is why we start with 1
   public override int ViewTypeCount
   {
      get
      {
         int viewTypeCount = 1;
         foreach (ListSection s in _sections) viewTypeCount += s.Adapter.ViewTypeCount;
         return viewTypeCount;
      }
   } 
   public override ListSection this[int index] { get { return _sections[index]; } }
   // Since we dont want the captions selectable or clickable returning a hard false here achieves this
   public override bool AreAllItemsEnabled() { return false; } 
   public override int GetItemViewType(int position)
   {
      int typeOffset = TYPE_SECTION_HEADER + 1;
      foreach (ListSection s in _sections)
      {
         if (position == 0) return TYPE_SECTION_HEADER;
         int size = s.Adapter.Count + 1;
         if (position < size) return (typeOffset + s.Adapter.GetItemViewType(position - 1)); 
         position -+ size;
         typeOffset += s.Adapter.ViewTypeCount;
      }
      return -1;
   }
   public override long GetItemId(int position) { return position; }
   public void AddSection(String caption, String columnHeader1, String columnHeader2,
                                       String columnHeader3, BaseAdapter adapter)
   {
      _sections.Add(new ListSection(caption, columnHeader1, columnHeader2, columnHeader3, adapter));
   }
   public override View GetView(int position, View convertView, ViewGroup parent)
   {
      View view = convertView;
       foreach (ListSection s in _sections)
      {
         // postion == 0 means we have to inflate the section separator
         if (position == 0) 
         {
            if (view == null || !(view is LinearLayout))
            {
               view = _inflater.Inflate(Resource.Layout.ListSeparator, parent, false);
            }
            TextView caption = view.FindViewById<TextView>(Resource.Id.caption);
            caption.Text = s.Caption;
            TextView columnHeader1 = view.FindViewById<TextView>(Resource.Id.columnHeader1);
            columnHeader1.Text = s.ColumnHeader1;
            TextView columnHeader2 = view.FindViewById<TextView>(Resource.Id.columnHeader2);
            columnHeader2.Text = s.ColumnHeader2;
            TextView columnHeader3 = view.FindViewById<TextView>(Resource.Id.columnHeader3); 
            columnHeader3.Text = s.ColumnHeader3;
         }
         int size = s.Adapter.Count + 1;
         // postion < size means we are at an item, so we just pass through its View from its adapter
         if (position < size) return s.Adapter.GetView(position - 1, convertView, parent);
         position -= size;
      }
      return null;
   }

   public override Java.Lang.Object GetItem(int position)
   {
      foreach (ListSection s in _sections)
      {
         if (position == 0) return null; // this is a separator item, dont want it instantiated
         int size = s.Adapter.Count + 1;
         if (position < size) return s.Adapter.GetItem(position);
         position -= size;
      }
      return null;
   }
}

As you see, we needed to override GetItem in this case. Normally you would not need to, as by default it returns this[int]. However, for our sectioned adapter, this[int] returns a ListSection object, which does no good when trying to retrieve a list item from the ListView. Overriding this method as above forces the method to dig into the proper sublist and return the appropriate object. 

Now all that is needed is to populate the ListView that needs to house all this information. See the snippet below from within the OnCreate method of our App. 

C#
// Fist lets create and populate the List<> instances that hold our food items
List<MeatType> meats = new List<MeatType>();
meats.Add(new MeatType("Hamburger", "Ground chuck beef", 2.76));
meats.Add(new MeatType("Sirloin", "Sliced sirloin steaks", 4.56));
List<VegetableType> veggies = new List<VegetableType)();
veggies.Add(new VegetableType("Brocolli", "Cut brocolli floretes", 1.76));
veggies.Add(new VegetableType("Carrots", "Cut peeled baby carrots", 2.18));
List<FruitType> fruits = new List<FruitType>();
fruits.Add(new FruitType("Apple", "Granny smith apples", 0.87));
fruits.Add(new FruitType("Peach", "South Carolina peaches", 1.12));
// Now we create our adapters for the item types
MeatTypeListAdapter madptr = new MeatTypeListAdapter(this, Resource.Layout.FoodTypeListItem, meats); 
VegetableTypeListAdapter vadptr = new VegetableTypeListAdapter(this, Resource.Layout.FoodTypeListItem, veggies);
FruitTypeListAdapter fadptr = new FruitTypeListAdapter(this, Resource.Layout.FoodTypeListItem, fruits);
// Now we create our sectioned adapter and add its sections
SectionedListAdapter sadptr = new SectionedListAdapter(this);
sadptr.AddSection("Available Meats", "Name", "Description", "Price (lb.)", madptr);
sadptr.AddSection("Available Vegetables", "Name", "Description", "Price (lb.)", vadptr);
sadptr.AddSection("Available Fruits", "Name", "Description", "Price (lb.)", fadptr);
// Now fetch the ListView and set its adapter to the sectioned adapter
ListView foodList = FindViewById<ListView>(Resource.Id.foodList);
foodList.SetAdapter(sadptr);  
foodList.ItemClick += new EventHandler<AdapterView.ItemClickEventHandler>(foodList_ItemClick); 

Now for the final step, properly handling the ItemClick event. We have to be sure that the proper sub list is queried when the ItemClick event fires. In this example, we are showing a new layout with the details of the item clicked. Here is the guts of the ItemClick event handler. There may be a better way to do this, and I am certainly open to any suggestions, but I don't see a clear way to cast a Java.Lang.Object into a .NET Object. For this reason, I've compared the output of both ToString methods. If by chance your data object has a custom ToString method, you may need to tweak this a bit. 

C#
private void foodList_ItemClick(object sender, AdapterView.ListItemClickEventArgs e)
{
   SectionedListAdapter adptr = (sender as ListView).Adapter as SectionedListAdapter;
   if (adptr.GetItem(e.Position != null)
   {
      if (adptr.GetItem(e.Position).ToString() == typeof(MeatType).ToString())
      {
         // Handle your code however you like here for when a meat is clicked
      }
      else if (adptr.GetItem(e.Position).ToString() == typeof(VegetableType).ToString())
      {
         // Handle your code however you like here for when a vegetable is clicked
      }
      else if (adptr.GetItem(e.Position).ToString() == typeof(FruitType).ToString())
      {
        // Handler your code however you like here for when a fruit is clicked
      }
   }
}  

Update - I realized this method is really only useful if you just want to react a certain generic way to the click. If you need to actually handle the object clicked, then things get a bit more tricky, not too bad though. To do this, you first need to create a wrapper class to handle passage of a Java.Lang.Object instance from the Adapter, which is passed when you override the GetItem(int) method of the Adapter. First, the wrapper class, JavaObjectHandler

C#
using Java.Lang;
using System; 
public class JavaObjectHandler : Java.Lang.Object
{
   private System.Object _instance;
   public JavaObjectHandler(System.Object instance) { _instance = instance; }
   public System.Object Instance { get { return _instance; } }
}  

Now, when we override the GetItem(int) method in the adapter, we will pass a JavaObjectHandler instance rather than the default Java.Lang.Object instance, and our JavaObjectHandler will contain the data object lying hidden in the Adapter. Go back to any of your BaseAdapter<T> classes and add the following method

C#
public override Java.Lang.Object GetItem(int position)
{
   if (position < _items.Count)
   {
      return new JavaObjectHandler(_items[position]);
   }
   return null;
}  

Since we have already overridden this method in our SectionedListAdapter, it will now kindly either pass null if a separator is clicked, or the underlying JavaObjectHandler containing our data object. Now you can modify your ItemClick event to use this new JavaObjectHandler instance, for example

C#
private void foodList_ItemClick(object sender, AdapterView.ItemClickEventArgs e)
{
   SectionedListAdapter adapter = (sender as ListView).Adapter as SectionedListAdapter;
   JavaObjectHandler item = adapter.GetItem(e.Position);
   if (item != null)
   {
      if (item.Instance is MeatType)
      {
         Toast.MakeText(this, "You clicked a meat: " + 
           (item.Instance as MeatType).Name, ToastLength.Short).Show();
      }
      else if (item.Instance is VegetableType)
      {
         Toast.MakeText(this, "You clicked a vegetable: " + 
           (item.Instance as VegetableType).Name, ToastLength.Short).Show();
      }
      else if (item.Instance is FruitType)
      {
         Toast.MakeText(this, "You clicked a fruit: " + 
           (item.Instance as FruitType).Name, ToastLenght.Short).Show();
      }
   }
}  

This sectioned adapter can be used for ListView or for ListActivity alike.  

History 

  • 1.0 - 12/7/2012 - General How-To posted. Source and example project to follow shortly.
  • 1.1 - 12/17/2012 - Updated to reflect JavaObjectHandler usage for more advanced ItemClick event handling. Added sample project to article. Sample is built for Android 4.0.3.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)