Xamarin Android 具有 ViewHolder 奇怪行为的 ListView

Xamarin Android ListView with ViewHolder odd behavior

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

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

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

这是我的场景:

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

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

在我滚动第一个列表视图之前,这似乎有效。一旦完成,列表视图仍然 "looks" 正确,但是当我单击一个项目时,它不是被选中的正确项目。

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

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

实际上我不想 post 立即编写一堆代码,但我还是会这样做,因为我打赌迟早会要求我这样做 - 这是class正在使用中,只删除了无关紧要的内容。

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()
    {
    }
}

列表适配器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;
    }
}

编辑

好的,我现在已经尝试更改我的 ListAdapter 以按照您的示例 InitLipton 进行操作,这样做似乎很有效。 我只是不明白为什么如果我传递标签中的实际项目而不是将索引传递给项目并按索引检索该项目它会失败 - 当滚动列表视图时导致此错误的机制是什么?

更新了 ListViewAdapter class(再次采取 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);
    }
}

对于您正在创建的 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];
    }