【问题标题】:Xamarin Android ListView with ViewHolder odd behaviorXamarin Android ListView 与 ViewHolder 奇怪的行为
【发布时间】:2026-01-24 05:10:02
【问题描述】:

我在使用 ListView 时遇到了一些问题,它在视觉上似乎可以正常工作,但是当单击某个项目时,很明显出现了问题。 总的来说,我对 Xamarin 和本机应用程序开发还很陌生,所以这可能只是一个简单的新手错误。

我不确定这是否是因为我在应该如何使用 ListViews + ViewHolders 方面做错了什么,或者我只是忘记了一些东西。

也许我的错误是我们 Xamarin 新手陷入的普遍陷阱,所以也许你们中的一些更有经验的人可以立即告诉我是什么导致了我的问题。

这是我的场景:

我有一个名为 ProjectTask 的对象列表。这样的对象本身可以包含一个类似的列表,尽管只有 1 级深。所以 TaskObj.Tasks 是可能的,但 TaskObj.Tasks[0].Tasks 不是。

所以我希望我的第一个列表视图显示所有父任务,当单击一个项目时,我切换到显示该任务的子任务的第二个列表视图。

这似乎一直在工作,直到我滚动了第一个列表视图。完成后,列表视图仍然“看起来”正确,但是当我单击一个项目时,它不是被选择的正确项目。

如果一个任务有一个 Name 和一个 Description 属性,它们都显示在 listview 项目上,那么当我单击该项目时,我可以看到它是一个项目另一个 Name 实际上被发送到处理第二个列表视图的活动。

有人从这个描述中知道发生了什么吗?

实际上我不想立即发布一堆代码,但我还是会这样做,因为我敢打赌我迟早会被要求 - 这是正在使用的类的简化版本,只是微不足道的东西已被删除。

ViewHolder 类

    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 类

    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;
    }
}

编辑

好的,我现在尝试更改我的 ListAdapter 以遵循您的示例 InitLipton,并且这样做似乎可以正常工作。 如果我在标签中传递实际项目而不是将索引传递给项目并按索引检索该项目,我只是不明白为什么它会失败 - 当滚动列表视图时,什么机制会出错?

更新 ListViewAdapter 类(第 2 次),不重要的东西已被删除或更好的可读性。

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);
    }
}

【问题讨论】:

    标签: android listview xamarin android-viewholder


    【解决方案1】:

    对于您正在创建的 Textview 的标签,请使用 Item 的位置。然后你就可以使用它作为索引返回到项目列表中

    这是我以前做过的适配器,但请查看 CheckBox。当它作为 obj 进入 SetChecked 时,我可以将其解析回一个复选框,然后我拥有 Tag inv,它是列表中项目的位置。

    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];
        }
    

    【讨论】: