Monodroid - Handling Click events inside ListAdapter rows
Asked Answered
C

3

9

I have a ListView with an ArrayAdapter set to it, and each row in the adapter contains four buttons that need to receive and handle click events. Normally in Android I would call SetOnClickListener on each button when I create the cell, but Mono gives the ability to set event handlers for the Click event instead. It looks like there's some weirdness to this in Mono though, because I run into one of two issues depending on where I set the event handler.

ArrayAdapter GetView Example 1:

View tweetCell = convertView;
if (tweetCell == null) {
    tweetCell = ((LayoutInflater)Context.GetSystemService (Context.LayoutInflaterService)).Inflate (Resource.Layout.TweetCell, null);

    tweetCell.FindViewById (Resource.Id.btn_moveTweet).Click += (object sender, EventArgs e) => MoveTweet (GetItem(position));

    tweetCell.FindViewById (Resource.Id.btn_unfavoriteTweet).Click += (object sender, EventArgs e) => UnfavoriteTweet (GetItem(position));

    tweetCell.FindViewById (Resource.Id.btn_hideTweet).Click += (object sender, EventArgs e) => HideTweet (GetItem(position));

    tweetCell.FindViewById (Resource.Id.btn_shareTweet).Click += (object sender, EventArgs e) => ShareTweet (GetItem(position));
}

Here, my event handler only gets set once per button (good!), but the position value is wrong most of the time. I'm wondering if the conversion from Mono to Android code is causing GetItem(position) to use the same value for position every time (the value that position is set to when the cell is first created). This code would work totally fine in normal Android.

ArrayAdapter GetView Example 2:

View tweetCell = convertView;
if (tweetCell == null) {
    tweetCell = ((LayoutInflater)Context.GetSystemService (Context.LayoutInflaterService)).Inflate (Resource.Layout.TweetCell, null);
}
tweetCell.FindViewById (Resource.Id.btn_moveTweet).Click += (object sender, EventArgs e) => MoveTweet (GetItem(position));

tweetCell.FindViewById (Resource.Id.btn_unfavoriteTweet).Click += (object sender, EventArgs e) => UnfavoriteTweet (GetItem(position));

tweetCell.FindViewById (Resource.Id.btn_hideTweet).Click += (object sender, EventArgs e) => HideTweet (GetItem(position));

tweetCell.FindViewById (Resource.Id.btn_shareTweet).Click += (object sender, EventArgs e) => ShareTweet (GetItem(position));

This method does cause the click event to be fired for the correct position, but it sets a new event handler every time the row is recycled. This causes click events for lots of rows at the same time. A workaround for this method would seem to be to keep references to the event handlers and remove them before setting them again inside GetView, but this seems extremely inelegant.

Is there a better method for handling click events inside ListView items in Monodroid?

Celtuce answered 25/4, 2013 at 13:11 Comment(1)
I ran into the same issue. The way I solved it was using the ViewHolder pattern. You can always shove the position in the Tag for the convertView and pull it out in the click event handler.Dyanna
S
1

i know its an old thread, but it has a lot of votes and is still marked as unanswered

It is only logical that the scenarios that you described happens! It has nothing to do with mono whatsoever. It has to do with the convertview. This is what the documentation says on convertview:

convertView - The old view to reuse, if possible. Note: You should check that this view is non-null and of an appropriate type before using. If it is not possible to convert this view to display the correct data, this method can create a new view.

Example 1: In your first example the convertView will be null the first time and the events will be set. Then the next time the GetView method is called the convertview may or may not be null. If it is not null it will use the old view with the events still attached! So this means that the event with the position parameter are still from a previous view!

Example 2: This example will work as expected but it is not very efficient like you mentioned. It has to locate the controls every time the FindViewById method is called.

Solution: The solution for this performance problem is to implement the viewholder pattern.

First you create a class that will hold your views:

private class MyViewHolder : Java.Lang.Object 
{
  public Button MoveTweet { get; set; }
  public Button ShareTweet { get; set; }
  public Button UnfavoriteTweet { get; set; }
  public Button HideTweet { get; set; }
}

Now you can use this viewholder in your code

public override View GetView (int position, View convertView, ViewGroup parent)
{
  MyViewHolder holder;
  var view = convertView;

  if(view != null) 
    holder = view.Tag as MyViewHolder;


  if (holder == null) {
    holder = new MyViewHolder ();
    view = activity.LayoutInflater.Inflate (Resource.Layout.OptimizedItem, null);
    holder.MoveTweet = view.FindViewById<Button> (Resource.Id. btn_moveTweet);
    holder.ShareTweet = view.FindViewById<Button> (Resource.Id. btn_shareTweet);
    holder.UnfavoriteTweet = view.FindViewById<Button> (Resource.Id. btn_unfavoriteTweetTweet);
    holder.HideTweet = view.FindViewById<Button> (Resource.Id. btn_hideTweet);
    view.Tag = holder;
  } 


  holder.MoveTweet.Click += (object sender, EventArgs e) => MoveTweet (GetItem(position));
  holder.UnfavoriteTweet.Click += (object sender, EventArgs e) => UnfavoriteTweet (GetItem(position))
  holder.HideTweet.Click += (object sender, EventArgs e) => HideTweet (GetItem(position));
  holder.FavoriteTweet.Click += (object sender, EventArgs e) => ShareTweet (GetItem(position));

  return view;
}

For more information on this check out: https://blog.xamarin.com/creating-highly-performant-smooth-scrolling-android-listviews/

Stepaniestepbrother answered 5/4, 2016 at 7:12 Comment(0)
B
0

I had similar problems too. Apparently, I will avoid solution 2. Moreover, I observe the problem in solution 1 only happened on Android 2.3.

This is how I fixed the problem.

I keep a reference to the ListView in the adapter, say _listView. Then, in your GetItem() method (Don't miss the position variable. Its value is a mystery), call _listView.GetPositionForView((View)sender) to get the correct position.

Blackheart answered 25/4, 2013 at 19:1 Comment(2)
The option 1 scenario is happening for me in all versions. I haven't tried your solution yet, but that just seems so backwards... not knocking your solution at all, I just wish that it worked properly in Mono so that the ListView and adapter didn't need to be so tightly coupled.Celtuce
I know, but this is the solution to the problem. I do really hope this works properly in Mono for Android as well. Also, I don't think keeping a reference of ListView in the adapter is that tightly coupled.Blackheart
C
0

Try with this code:

    void OnListItemClick(object sender, AdapterView.ItemClickEventArgs e)
    {
        var listView = sender as ListView;
        var item = items[e.Position];

        //init your data...

        tweetCell.FindViewById (Resource.Id.btn_moveTweet).Click += (object sender, EventArgs e) => MoveTweet (item);
    }
Conquer answered 2/10, 2014 at 9:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.