Aidal Aidal - 1 month ago 6
Android Question

Xamarin Android ListView with ViewHolder odd behavior

I'm having some problems with a ListView that appears to be working correctly visually, but when an item is clicked it becomes obvious that something is wrong.
I'm pretty new to Xamarin and native app development in general, so this could just be a simple rookie mistake at work.

I'm unsure whether it is because I'm doing something wrong in general in regard to how ListViews + ViewHolders are supposed to be used or if I'm just forgetting something.

Maybe my mistake is a general pitfall that us Xamarin noobs fall into, so maybe some of you more experienced people can tell me right away what is causing my problem.

Here is my scenario:

I have a List of objects called ProjectTask. Such object can hold a similar list itself, though only 1 level deep. So TaskObj.Tasks is possible, but TaskObj.Tasks[0].Tasks is not.

So I want my first listview to display all the parent tasks and when an item is clicked, I switch to a second listview displaying the sub tasks of that task.

This appears to be working until I have scrolled the first listview. Once that is done, the listview still "looks" right, but when I click an item, it is not the correct item being selected.

Like if a task has an Name and a Description attribute which both show on the listview item, then when I click the item I can see that it is an item with another Name that is actually being sent to the activity that handles the second listview.

Does anyone have an idea from this description as to what is going on?

Actually I didn't wanna post a bunch of code right away, but I'm gonna do it anyway since I bet I will be asked to sooner then later - Here is a simplified version of the classes in use, only insignificant stuff has been removed.

ViewHolder class

public class ViewHolderProjectTaskExtended : Java.Lang.Object
{
public Button btnStop { get; set; }
public Button btnStart { get; set; }
public TextView tvName { get; set; }
public CheckBox is_started { get; set; }
public TextView task_id { get; set; }

public ViewHolderProjectTaskExtended()
{
}
}


ListAdapter class

public class ProjectTaskExtendedListAdapter : BaseAdapter<ProjectTask>
{
List<ProjectTask> _items;
Activity _context;

public ProjectTaskExtendedListAdapter(Activity context, List<ProjectTask> tasks)
{
_items = tasks;
_context = context;
}

public override ProjectTask this[int position]
{
get { return _items[position]; }
}

public override int Count
{
get { return _items.Count; }
}

public override long GetItemId(int position)
{
return position;
}

public override View GetView(int position, View convertView, ViewGroup parent)
{
var item = _items[position];
ViewHolderProjectTaskExtended viewHolder = null;
View view = convertView;

if (view != null)
{
viewHolder = view.Tag as ViewHolderProjectTaskExtended;
}

#region viewHolder doesn't exist
if (viewHolder == null)
{
view = this._context.LayoutInflater.Inflate(Resource.Layout.ListItem_SalesOrderExtended, null);

viewHolder = new ViewHolderSalesOrderExtended();

viewHolder.tvName = view.FindViewById<TextView>(Resource.Id.tv_salesorder_text);
viewHolder.btnStop = view.FindViewById<Button>(Resource.Id.btn_stop_session);
viewHolder.btnStart = view.FindViewById<Button>(Resource.Id.btn_start_session);

viewHolder.is_started = view.FindViewById<CheckBox>(Resource.Id.chb_is_started);
viewHolder.task_id = view.FindViewById<TextView>(Resource.Id.tv_task_id);

view.Tag = viewHolder;

viewHolder.tvName.Text = "(" + item.name + ")" + Environment.NewLine + item.description;

viewHolder.task_id.Text = item.id;

viewHolder.btnStart.Tag = item.id;
viewHolder.btnStop.Tag = item.id;

if (item.tasks.Count > 0) // has sub tasks
{
viewHolder.has_children.Checked = true;
viewHolder.btnStop.Visibility = ViewStates.Gone;
viewHolder.btnStart.Visibility = ViewStates.Invisible;

viewHolder.tvName.Click += (sender, e) =>
{
Toast.MakeText(_context, "Select sub task for " + item.name + "", ToastLength.Short).Show();

var ident_select_sub_task = new Intent(_context, typeof(SelectSubTaskActivity));

ident_select_sub_task.PutExtra("pt_parent", JsonConvert.SerializeObject(item));

_context.StartActivity(ident_select_sub_task);
};
}
else // has no sub tasks
{
if (viewHolder.is_started.Checked == false)
{
viewHolder.btnStart.Visibility = ViewStates.Visible;
viewHolder.btnStop.Visibility = ViewStates.Gone;
}
else
{
viewHolder.btnStart.Visibility = ViewStates.Gone;
viewHolder.btnStop.Visibility = ViewStates.Visible;
}

viewHolder.btnStart.Click += (sender, e) =>
{
Toast.MakeText(_context, "Task " + item.name + " is starting", ToastLength.Short).Show();

// code dealing with starting a task
};

viewHolder.btnStop.Click += (sender, e) =>
{
Toast.MakeText(_context, "Task " + item.name + " is stopping", ToastLength.Short).Show();

// code dealing with stopping a task
};
}
}
#endregion

#region viewHolder exists (reuse)
else
{
viewHolder.tvName.Text = "(" + item.name + ")" + Environment.NewLine + item.description;

viewHolder.task_id.Text = item.id;

viewHolder.btnStart.Tag = item.id;
viewHolder.btnStop.Tag = item.id;

if (item.tasks.Count > 0) // has sub tasks
{
viewHolder.btnStart.Visibility = ViewStates.Invisible;
viewHolder.btnStop.Visibility = ViewStates.Gone;
}
else // has no sub tasks
{
if (viewHolder.is_started.Checked == false)
{
viewHolder.btnStart.Visibility = ViewStates.Visible;
viewHolder.btnStop.Visibility = ViewStates.Gone;
}
else
{
viewHolder.btnStart.Visibility = ViewStates.Gone;
viewHolder.btnStop.Visibility = ViewStates.Visible;
}
}
}
#endregion

return view;
}
}


EDIT

Ok, I have tried to change my ListAdapter now to follow your example InitLipton and it seems to work when doing it this way.
I just don't get why it fails if I pass the actual item in the tag opposed to passing an index to an item and the retrieving that item by index - what are the mechanics that makes this go wrong when the listview has been scrolled?

Updated ListViewAdapter class (take 2) again, nonessential stuff has been removed or better readability.

public class ProjectTaskExtendedListAdapter : BaseAdapter<ProjectTask>
{
List<ProjectTask> _items;
Activity _context;

public ProjectTaskExtendedListAdapter(Activity context, List<ProjectTask> tasks)
{
_items = tasks;
_context = context;
}

public override ProjectTask this[int position]
{
get { return _items[position]; }
}

public override int Count
{
get { return _items.Count; }
}

public override long GetItemId(int position)
{
return position;
}

public override View GetView(int position, View convertView, ViewGroup parent)
{
ViewHolderProjectTaskExtended viewHolder = null;
View view = convertView;

if (viewHolder == null)
{
view = this._context.LayoutInflater.Inflate(Resource.Layout.ListItem_SalesOrderExtended, null);

viewHolder = new ViewHolderSalesOrderExtended();

viewHolder.tvName = view.FindViewById<TextView>(Resource.Id.tv_salesorder_text);
viewHolder.btnStop = view.FindViewById<Button>(Resource.Id.btn_stop_session);
viewHolder.btnStart = view.FindViewById<Button>(Resource.Id.btn_start_session);

viewHolder.is_started = view.FindViewById<CheckBox>(Resource.Id.chb_is_started);
viewHolder.has_children = view.FindViewById<CheckBox>(Resource.Id.chb_has_children);
viewHolder.task_id = view.FindViewById<TextView>(Resource.Id.tv_task_id);

viewHolder.tvName.Click += (sender, e) => itemClicked(viewHolder.tvName);
}
else
{
viewHolder = (ViewHolderProjectTaskExtended)view.Tag;
}

var item = _items[position];

viewHolder.tvName.Text = "(" + item.name + ")" + Environment.NewLine + item.description;
viewHolder.tvName.Tag = position;

if (item.tasks.Count > 0)
{
viewHolder.btnStart.Visibility = ViewStates.Invisible;
viewHolder.btnStop.Visibility = ViewStates.Gone;
}
else
{
if (viewHolder.is_started.Checked == false)
{
viewHolder.btnStart.Visibility = ViewStates.Visible;
viewHolder.btnStop.Visibility = ViewStates.Gone;
}
else
{
viewHolder.btnStart.Visibility = ViewStates.Gone;
viewHolder.btnStop.Visibility = ViewStates.Visible;
}
}

return view;
}

private void itemClicked(object sender)
{
var tv = sender as TextView;

var position = (int)tv.Tag;
var _item = _items[position];

Toast.MakeText(_context, "Select sub task for " + _item.name + "", ToastLength.Short).Show();

var ident_select_sub_task = new Intent(_context, typeof(SelectSubTaskActivity));

ident_select_sub_task.PutExtra("pt_parent", JsonConvert.SerializeObject(_item));

_context.StartActivity(ident_select_sub_task);
}
}

Answer

For the tag of your Textview that you are creating use the position of the Item. Then you will be able to use that as the index back into the list of items

This is an adapter i did before, but look at the CheckBox. When that goes into SetChecked as the obj, i can parse it back to a checkbox, i then have the Tag inv which is the position of the item in the list.

public override View GetView(int position, View convertView, ViewGroup parent)
    {
        ViewHolder holder;

        if (convertView == null)
        {
            convertView = _activity.LayoutInflater.Inflate(Resource.Layout.CarItem, parent, false);

            holder = new ViewHolder
            {
                CheckBox = convertView.FindViewById<CheckBox>(Resource.Id.CheckBoxActiveItem),
                Title = convertView.FindViewById<TextView>(Resource.Id.Title),
            };

            convertView.Tag = holder;
            convertView.SetTag(Resource.Id.CheckBoxActiveItem, holder.CheckBox);
            convertView.SetTag(Resource.Id.Title, holder.Title);
        }
        else
        {
            holder = (ViewHolder)convertView.Tag;
        }


        var item = _items[position];
        holder.Title.Text = item .DisplayName;
        holder.CheckBox.Checked = item .IsDefault;
        holder.CheckBox.Click += (sender, args) => SetChecked(holder.CheckBox.Checked, sender);
        holder.CheckBox.Tag = position;


        return convertView;
    }


     private void SetChecked(bool isChecked, object sender)
    {

        var box = sender as CheckBox;

        //Now you have the Item that has been selected, regardless of the scroll
        var position = (int)box.Tag;
        var ccItem = _items[position];
    }
Comments