Reputation: 1345
I just made simple CustomAdapter
for ListView
with clickable button.
This is my adapter which on button click will toast
an item position:
public class ViewHolder : Java.Lang.Object
{
public TextView txtName { get; set; }
public TextView txtAge { get; set; }
public TextView txtEmail { get; set; }
public Button button1 { get; set; }
}
public class CustomAdapter : BaseAdapter
{
private Activity activity;
private List<Person> persons;
public CustomAdapter(Activity activity,List<Person> persons)
{
this.activity = activity;
this.persons = persons;
}
public override int Count
{
get
{
return persons.Count;
}
}
public override Java.Lang.Object GetItem(int position)
{
return null;
}
public override long GetItemId(int position)
{
return persons[position].Id;
}
public override View GetView(int position, View convertView, ViewGroup parent)
{
var view = convertView;
if (view == null)
{
view = LayoutInflater.From(parent.Context).Inflate(Resource.Layout.list_view_dataTemplate, parent, false);
var txtName = view.FindViewById<TextView>(Resource.Id.textView1);
var txtAge = view.FindViewById<TextView>(Resource.Id.textView2);
var txtEmail = view.FindViewById<TextView>(Resource.Id.textView3);
var button1 = view.FindViewById<Button>(Resource.Id.button1);
view.Tag = new ViewHolder() { txtName = txtName, txtAge = txtAge, txtEmail = txtEmail, button1 = button1 };
}
var holder = (ViewHolder)view.Tag;
holder.txtName.Text = "name " + position;
holder.txtAge.Text = "age " + position;
holder.txtEmail.Text = "email " + position;
holder.button1.Click += delegate
{
Toast.MakeText(Application.Context, "pos: " + position.ToString(), ToastLength.Short).Show();
};
return view;
}
My problem is that, when I click on this button once, I Get few Toasts
one by one. Like "pos: 1" => "pos: 2" => "pos: 3" => "pos: 0"
For diffrent rows, the Toast
messages pop in different way.
I am struggling with this problem couple of hours and I cant find a way to solve this problem.
I have made an Github repo with whole "test project" so you can see exacly what I am talking about.
Any help would be much appreciated :)
Upvotes: 0
Views: 643
Reputation: 39102
The problem is caused by the view recyclation feature in Android.
You are checking if the convertView
is not null, which is correct, but when it is not null, you are setting its properties including the Click
event handler.
The problem is, that when you use +=
, a new event handler is added to those already existing, so when the view is recycled several times, the button may have several event handlers attached and that causes several toast messages to appear.
To fix this, you should add the event handler within the initial view
creation only:
public override View GetView(int position, View convertView, ViewGroup parent)
{
var view = convertView;
if (view == null)
{
view = LayoutInflater.From(parent.Context).Inflate(Resource.Layout.list_view_dataTemplate, parent, false);
var txtName = view.FindViewById<TextView>(Resource.Id.textView1);
var txtAge = view.FindViewById<TextView>(Resource.Id.textView2);
var txtEmail = view.FindViewById<TextView>(Resource.Id.textView3);
var button1 = view.FindViewById<Button>(Resource.Id.button1);
view.Tag = new ViewHolder() { txtName = txtName, txtAge = txtAge, txtEmail = txtEmail, button1 = button1 };
button1.Tag = position;
button1.Click += (sender, args) => {
Toast.MakeText(Application.Context, "pos: " + ((Button)sender).Tag.ToString(), ToastLength.Short).Show();
};
}
var holder = (ViewHolder)view.Tag;
holder.txtName.Text = "name " + position;
holder.txtAge.Text = "age " + position;
holder.txtEmail.Text = "email " + position;
holder.button1.Tag = position;
return view;
}
Note that I am setting the position
as the tag of the button
. I originally used the position
variable in the event handler directly, but unfortunately this doesn't work, as when the variable is used inside the handler, it is hoisted and it would always refer to the original position
when the view was created. Using sender
's Tag
you can always retrieve the current value as expected.
As a simplification as suggested by @pskink, you could add a position
property and ShowToast
method to your ViewHolder
. Then you could use the ViewHolder
directly in the Click
handler and the code would be much simpler:
Updated ViewHolder
:
public class ViewHolder : Java.Lang.Object
{
public TextView txtName { get; set; }
public TextView txtAge { get; set; }
public TextView txtEmail { get; set; }
public Button button1 { get; set; }
public int position { get; set; }
public void ShowToast()
{
Toast.MakeText(Application.Context, "pos: " + position.ToString(), ToastLength.Short).Show();
}
}
And updated GetView
:
public override View GetView(int position, View convertView, ViewGroup parent)
{
var view = convertView;
if (view == null)
{
view = LayoutInflater.From(parent.Context).Inflate(Resource.Layout.list_view_dataTemplate, parent, false);
var txtName = view.FindViewById<TextView>(Resource.Id.textView1);
var txtAge = view.FindViewById<TextView>(Resource.Id.textView2);
var txtEmail = view.FindViewById<TextView>(Resource.Id.textView3);
var button1 = view.FindViewById<Button>(Resource.Id.button1);
var viewHolder = new ViewHolder() { txtName = txtName, txtAge = txtAge, txtEmail = txtEmail, button1 = button1 };
view.Tag = viewHolder;
button1.Click += (sender, args) => {
viewHolder.ShowToast();
};
}
var holder = (ViewHolder)view.Tag;
holder.txtName.Text = "name " + position;
holder.txtAge.Text = "age " + position;
holder.txtEmail.Text = "email " + position;
holder.position = position;
return view;
}
Upvotes: 2