androidxamarinexpandablelistview

Xamarin BaseExpandableListAdapter returns wrong View in OnClick method on button


I have same basic Expandable list in Xamarin with items that have EditText inside. So basically every item in list has "add 1" or "subtract 1" and EditText that shows value. Its basically a shopping cart where you have group of items and inside a group you can purchase multiple items.

Problem is when I click on "add 1" (+1) button I get completely different view and I update some other EditText instead EditText that is in same child view.

public override View GetChildView(int groupPosition, int childPosition, bool isLastChild, View convertView, ViewGroup parent)
    {
        Holder<HolderObject> holder;
        var child = (JavaObjectWrapper<MobilePOSSubItem>)GetChild(groupPosition, childPosition);

        if (convertView == null)
        {
            convertView = context.LayoutInflater.Inflate(Resource.Layout.mobilePOSTicketsSubItem, null);
            holder = new Holder<HolderObject>(
                new HolderObject {
                    GroupPosition = groupPosition,
                    ChildPosition = childPosition,
                    Amount = convertView.FindViewById<EditText>(Resource.Id.etAmount),
                    Tariff = convertView.FindViewById<TextView>(Resource.Id.tariff),
                    Name = convertView.FindViewById<TextView>(Resource.Id.name),
                    Rule = convertView.FindViewById<TextView>(Resource.Id.rule),
                    IconAllotment = convertView.FindViewById<ImageView>(Resource.Id.iconAllotment),
                    AddButton = convertView.FindViewById<ImageButton>(Resource.Id.add),
                    RemoveButton = convertView.FindViewById<ImageButton>(Resource.Id.remove)
            });

            convertView.Tag = holder;
            convertView.SetTag(Resource.Id.etAmount, holder.Value.Amount);
            convertView.SetTag(Resource.Id.tariff, holder.Value.Tariff);
            convertView.SetTag(Resource.Id.name, holder.Value.Name);
            convertView.SetTag(Resource.Id.rule, holder.Value.Rule);
            convertView.SetTag(Resource.Id.iconAllotment, holder.Value.IconAllotment);
            convertView.SetTag(Resource.Id.add, holder.Value.AddButton);
            convertView.SetTag(Resource.Id.remove, holder.Value.RemoveButton);
        } else
        {
            holder = (Holder<HolderObject>)convertView.Tag;
            holder.Value.Amount.AfterTextChanged -= EtAmount_TextChanged;
        }

        holder.Value.Amount.SetText(child.Object.Value.ToString(), TextView.BufferType.Editable);
        holder.Value.Amount.SetSelectAllOnFocus(true);

        holder.Value.Amount.AfterTextChanged += EtAmount_TextChanged;
... 


private void EtAmount_TextChanged(object sender, AfterTextChangedEventArgs e)
    {
        if (sender.GetType().BaseType == typeof(EditText))
        {
            var etAmount = (EditText)sender;
            var parent = (View) etAmount.Parent.Parent;
            Holder<HolderObject> holder = (Holder<HolderObject>)parent.Tag;
            int value = 0;
            string newValue = e.Editable.ToString();
            try
            {
                if (!string.IsNullOrEmpty(newValue))
                {
                    value = Convert.ToUInt16(newValue);
                }
                else
                {
                    holder.Value.Amount.Text = "0";
                }

            }
            catch
            {
                holder.Value.Amount.Text = "0";
            }

            items[holder.Value.GroupPosition].SubItems[holder.Value.ChildPosition].Value = value;
            NotifyDataSetChanged();
            //UpdateSelectedItem(child, value);
        }
    }


public void OnClick(View v)
    {
        HideSoftKeyboard(listView);

        View parent = (View) v.Parent.Parent.Parent;
        HolderObject holder = ((Holder<HolderObject>)parent.Tag).Value;

        var child = (JavaObjectWrapper<MobilePOSSubItem>) GetChild(holder.GroupPosition, holder.ChildPosition);
        if (holder.Amount != null)
        {
            int value;
            if (int.TryParse(holder.Amount.Text, out value))
            {
                if (v.Id.Equals(Resource.Id.add))
                {
                    if (value < 299 && (child.Object.ValueMax == null || value < child.Object.ValueMax))
                    {
                        value++;
                        holder.Amount.Text = value.ToString();
                    }
                }
                else if (v.Id.Equals(Resource.Id.remove))
                {
                    if (value > 0 && (child.Object.ValueMin == null || value > child.Object.ValueMin))
                    {
                        value--;
                        holder.Amount.Text = value.ToString();
                    }
                }
            }
            else
            {
                holder.Amount.Text = "0";
            }
        }
        else
        {
            CultureInfo original = CultureInfo.CurrentCulture;
            NumberFormatInfo noCurrencySymbol = (NumberFormatInfo)original.NumberFormat.Clone();
            noCurrencySymbol.CurrencySymbol = string.Empty;

            var cbArticle = (CheckBox)v;
            child.Object.ArticleList.FirstOrDefault(x => x.ArticleId == cbArticle.Id).Value = (cbArticle.Checked) ? 1 : 0;
            holder.Tariff.Text = string.Format("{0} {1}", child.Object.Currency, child.Object.GetTotalAmount().ToString("C", noCurrencySymbol));
        }
    }

So basically what I thought would work is:

  1. In GetChildView if convertView is null, inflate it with layout and attach Holder object to it with childPosition and GroupPosition
  2. Attach AfterTextChanged on EditText in Holder and attach OnClick method to buttons
  3. Process: user clicks "add 1" -> onClick method triggers -> I get parent of button and get Holder object -> I change value of EditText in that holder object -> AfterTextChanged gets triggered and updates child item in list

If I scroll, EditText values gets to 0, button clicks get completely different view. So if I click in second item in second group (that would be groupPosition: 1 and childPosition: 1 based on indexes), I try to get Holder object and Holder object has groupPosition:0 and childPosition:5 which are completely wrong values.

I also set stable ids and I can confirm that all children has different ids:

public override long GetChildId(int groupPosition, int childPosition)
    {
        return (long) items[groupPosition].SubItems[childPosition].Idx;
    }
public override bool HasStableIds
    {
        get
        {
            return true;
        }
    }

Solution

  • Problem was this line convertView.Tag = holder;. Attaching holder to a view should be last thing you do in function, so I moved that line to the end of function and everything worked, right before return convertView. Mistake was that I first attached holder to view and then change it and added listeners to holder views. That should be last thing, first apply all things to holder views (like text, listeners, colors etc.) and at the end attach it to view.