Tuesday, May 14, 2013

Defining ContextMenus for ListView Items in Xamarin Android

While working on my Android app I had need to display a list of items with position-dependent context menus.  It took me awhile to develop a clean solution so I'd like to share my work with anyone who would like dynamic menus available on their ListViews that display over items using a long-press.

Inside of my Activity class I have declared three important variables:

private ListView itemList;
private int selectedItemNumber;
private string selectedItemText;

What each variable tracks should be self explanatory from the declaration, but please comment if you have questions on anything in my posts.  The reason it's important to declare these within scope of the Activity class and not within a method is because we lose focus of what ListView item is being interacted with in-between the step of creating a menu and when that menu is used.  There might be a way to extract that information from the second step, but none was obvious to me and I found the approach of using private variables an easy solution.

In addition to those three variables I've also created constant values to track the int-based menu options:

private const int EDIT_ITEM = 0;
private const int VIEW_ITEM = 1;
private const int DELETE_ITEM = 2;

These allow me to refer to actions in a clear way in the code.  In the OnCreate method of my Activity there are two important steps needed to use context menus:

protected override void OnCreate (Bundle bundle)
{
 base.OnCreate (bundle);
 SetContentView (Resource.Layout.SendTickets);

 //Two important steps to for my context menus
 itemList = (ListView) FindViewById(Resource.Id.myListViewName);
 RegisterForContextMenu(itemList);

}

Now that I've associated my ListView with a private variable and have registered it for ContextMenus I only have to define two additional methods for everything to work.  The first is the method that is triggered when an item is selected because of the RegisterForContextMenu call made in OnCreate, and it creates the menu that is displayed:

public override void OnCreateContextMenu(IContextMenu menu, View v, IContextMenuContextMenuInfo info)
{
 AdapterViewAdapterContextMenuInfo menuInfo = (AdapterView.AdapterContextMenuInfo) info;
 
 selectedItemText = ((TextView) menuInfo.TargetView).Text;
 selectedItemId = menuInfo.Id;

 //My listview is set to have a "No Data" item on the first row if it's empty
 if (!selectedTicketText.Equals("No Data"))
 {
  menu.SetHeaderTitle("My Menu Header");
  //All items can be viewed
  menu.Add(0, VIEW_ITEM, 0, "View Item");
  //But only the first item in the list can be edited
  if (selectedItemId== 0)
   menu.Add(0, EDIT_ITEM, 0, "Edit Item");

  if (!selectedTicketText.Contains ("Words that mean it can be deleted"))
   menu.Add (0, DELETE_ITEM, 0, "Delete Item");
 }
 else  //This won't create any menu at all
 {
  menu.SetHeaderTitle("No Options");
 }
}

Okay, so now my menu will show up and it will look different based on what text the item in the ListView contains, and what item number it is assigned, which is the 0-based index in the listview.  You can see in my menu code above that I have added the ability to View to any-and-all items, but only the first item in the list can be edited, and only items containing my special text can be deleted.

The last step is to specify what actions occur when the menu is used.  We do this in the OnContextItemSelected method:

public override bool OnContextItemSelected(IMenuItem item)
{
 if (item.ItemId.Equals (VIEW_ITEM)) 
 {
  MethodThatViewsMyItem(selectedItemText);
 } 
 else if (item.ItemId.Equals(EDIT_ITEM))
 {
  MethodThatEditsMyItem(selectedItemNumber);
 }
 else if (item.ItemId.Equals(DELETE_ITEM))
 {
  MethodThatDeletesMyItem(selectedItemNumber);
 }

 return base.OnOptionsItemSelected(item);
}

This is where the information captured as selectedItemText and selectedItemNumber from OnCreateContextMenu comes in handy.  Since this method only has access to the item that was clicked, and that IMenuItem contains no information about the ListView item we have to refer to our Activity-scope variables selectedItemText and selectedItemNumber.

As always, feel free to post questions or comments, even if it's to suggest a better way of doing this!