Grzegorz G.
Grzegorz G.

Reputation: 1345

xamarin - CustomAdapter with buttons in ListView

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

Answers (1)

Martin Zikmund
Martin Zikmund

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.

Update

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

Related Questions