Listing information is an fundamental requirement in any mobile application development. Android developers usually prefers Listview control for this.It makes easier user interaction to data model.
In the previous post I explained how to Requesting REST Webservice with JSON in C# Xamarin Android, How to use Google Place API with Autocomplete in Xamarin Android.
I have observed some of the below issues with ListView operation.
->Listview Item click showing wrong view position.
->ItemClick event fires more then once.
->Out of memory exception.
Let us proceed by creating new android project, Bind the Listview by using custom adapter. Fill the list with sample data of type "ItemClass" and passed to custom adapter class "ItemAdapterClass".
In the previous post I explained how to Requesting REST Webservice with JSON in C# Xamarin Android, How to use Google Place API with Autocomplete in Xamarin Android.
I have observed some of the below issues with ListView operation.
->Listview Item click showing wrong view position.
->ItemClick event fires more then once.
->Out of memory exception.
Let us proceed by creating new android project, Bind the Listview by using custom adapter. Fill the list with sample data of type "ItemClass" and passed to custom adapter class "ItemAdapterClass".
ListView listViewItem; ItemAdapterClass objItemAdapter; Listk < ItemClass > lstItem; void BindListView() { lstItem = new List < ItemClass > (); FillList (); objItemAdapter = new ItemAdapterClass (this, lstItem); listViewItem.Adapter = objItemAdapter; } void FillList() { ItemClass objItem; for (int i = 0; i < 2000 ; i++) { objItem = new ItemClass (); objItem.ItemName =string.Format( "Item_{0} ",i); objItem.ItemImage = "Image name"; lstItem.Add (objItem); } } public class ItemClass { public string ItemName { get; set;} public string ItemImage{ get; set; } }On run of application,screen looks like this :
In my custom adapter class "GetView" method is as follows,
public override View GetView (int position, View convertView, ViewGroup parent) { View rowView = convertView; //reuse view if (rowView == null) { rowView = _context.LayoutInflater.Inflate (Resource.Layout.ItemCustomLayout, parent, false); TextView txtTemName = rowView.FindViewById<TextView> (Resource.Id.lblItemName); ImageView imgItem = rowView.FindViewById<ImageView> (Resource.Id.imgItem); CheckBox chkItem = rowView.FindViewById<CheckBox> (Resource.Id.checkitem); chkItem.Click += delegate(object sender, EventArgs e) { Toast.MakeText(_context,string.Format( "Position:{0}, {1}",position, _lstItem [position].ItemName),ToastLength.Long).Show(); }; } txtTemName.Text = _lstItem [position].ItemName; imgItem.SetImageResource (Resource.Drawable.imgItem); return rowView; }Here my emulator screen takes 11 view row's. Within that 11 rows chkItem.Click shows the correct position.
But on scrolling to down [below 11 row]and again chkItem.Click returns wrong position.
Even calling chkItem.Click method after influating View row ( the if statement) has no change.
so it shows that view row recycling is not properly done. Correct position is not attached to view row.
Following below method helped to get ride of these issues :
-> Defined View holder class to declare all the Control, Viewholder is an inner class which holds reference to the relevant rows.
This reference can be attached to the rowView(custom row) by using the "Tag" property.
And when reusing the row get the instance of the viewholder class attached previously to the rowView by using the same property "Tag". This approach approximately 15 % faster then the using in-line findViewById() method.
//Adapter class GetView()
public override View GetView (int position, View convertView, ViewGroup parent) { View rowView = convertView; if (rowView == null) { rowView = _context.LayoutInflater.Inflate (Resource.Layout.ItemCustomLayout, parent, false); viewHolder = new ViewHolderItem (); viewHolder.txtTemName = rowView.FindViewById < TextView > (Resource.Id.lblItemName); viewHolder.imgItem = rowView.FindViewById < ImageView > (Resource.Id.imgItem); viewHolder.chkItem = rowView.FindViewById < CheckBox > (Resource.Id.checkitem); rowView.Tag = viewHolder; //attaching viewholder reference to row } else { viewHolder = (ViewHolderItem)rowView.Tag; //during row re-use get the instance of the view holder } viewHolder.txtTemName.Text = strX; viewHolder.imgItem.SetImageResource (Resource.Drawable.imgX); return rowView; } class ViewHolderItem :Java.Lang.Object { internal TextView txtTemName; internal ImageView imgItem; internal CheckBox chkItem; }To solve a wrong position issue,
Method#1:
->During each row binding, Tag the row position to the Control(here check box) which is used to get the click event.
And get that tagged position from the Control during item click operation.
public override View GetView (int position, View convertView, ViewGroup parent) { View rowView = convertView; if (rowView == null) { rowView = _context.LayoutInflater.Inflate (Resource.Layout.ItemCustomLayout, parent, false); viewHolder = new ViewHolderItem (); viewHolder.txtTemName = rowView.FindViewById < TextView > (Resource.Id.lblItemName); viewHolder.imgItem = rowView.FindViewById < ImageView > (Resource.Id.imgItem); viewHolder.chkItem = rowView.FindViewById < CheckBox > (Resource.Id.checkitem); viewHolder.chkItem.Click += delegate(object sender, EventArgs e) { var chkBx= (CheckBox)sender; int pos = (int)chkBx.Tag; //get the tagged position from the Control if (lstSelectedItem.Contains (_lstItem [pos].ItemName)) { lstSelectedItem.Remove (_lstItem [pos].ItemName); } else { lstSelectedItem.Add (_lstItem [pos].ItemName); } }; rowView.Tag = viewHolder; //attaching viewholder reference to row } else { viewHolder = (ViewHolderItem)rowView.Tag; //during row re-use get an instance of the view holder class } viewHolder.txtTemName.Text = _lstItem [position].ItemName; viewHolder.imgItem.SetImageResource (Resource.Drawable.imgItem); viewHolder.chkItem.Checked = lstSelectedItem.Contains (_lstItem [position].ItemName) ? true :false ; viewHolder.chkItem.Tag=position; //here attached row position to control chkItem return rowView; } class ViewHolderItem :Java.Lang.Object { internal TextView txtTemName; internal ImageView imgItem; internal CheckBox chkItem; }
Method#2:
-> Another method is by using "event action". This triggers the method in Activity class from the adapter class getview method.
Here Action is a standard delegate that accepts parameters and doesn't return value. It's used to represent an action.[ http://stackoverflow.com/questions/2834094/what-is-actionstring].
In other word event Action<type> is used to notify the subscriber along with defined <type> from publisher.
//Adapter class GetView()
internal event Action<string> ActionImgSelectedToActivity; public override View GetView (int position, View convertView, ViewGroup parent) { View rowView = convertView; if (rowView == null) { rowView = _context.LayoutInflater.Inflate (Resource.Layout.ItemCustomLayout, parent, false); viewHolder = new ViewHolderItem (); viewHolder.txtTemName = rowView.FindViewById<TextView> (Resource.Id.lblItemName); viewHolder.imgItem = rowView.FindViewById<ImageView> (Resource.Id.imgItem); viewHolder.chkItem = rowView.FindViewById<CheckBox> (Resource.Id.checkitem); viewHolder.Initialize (rowView);//to listen click event from viewholder class rowView.Tag = viewHolder; } else { viewHolder = (ViewHolderItem)rowView.Tag; } //subscribe to click event from viewholder class viewHolder.eventHandlerImgViewSelected= () => { //publish click event to activity class along with itemname if(ActionImgSelectedToActivity!=null) ActionImgSelectedToActivity(_lstItem[position].ItemName); }; viewHolder.txtTemName.Text = _lstItem [position].ItemName; viewHolder.imgItem.SetImageResource (Resource.Drawable.imgItem); return rowView; }//viewholder class
class ViewHolderItem :Java.Lang.Object { internal TextView txtTemName; internal ImageView imgItem; internal CheckBox chkItem; //to publish ImgView click event to Adapter GetView() internal event EventHandler eventHandlerImgViewSelected; internal void Initialize(View view) { imgItem=view.FindViewById<ImageView> (Resource.Id.imgItem); //to publish ImgView click event to Adapter GetView() imgItem.Click += (object sender, EventArgs e) => eventHandlerImgViewSelected (); } }Activity class BindListView():
void BindListView() { lstItem = new List<ItemClass> (); FillList (); if(lstItem!=null && lstItem.Count >0) { if ( objItemAdapter != null ) { //Un-subscribe to click event objItemAdapter.ActionImgSelectedToActivity -= SelectedItem; objItemAdapter = null; } objItemAdapter = new ItemAdapterClass (this, lstItem); //subscribe to click event from Adapter class GetView() method objItemAdapter.ActionImgSelectedToActivity += SelectedItem; listViewItem.Adapter = objItemAdapter; } } void SelectedItem( string strItemName) { Toast.MakeText ( this , string.Format("From Activity :{0}",strItemName ), ToastLength.Short ).Show (); }Have a look at the complete code here : https://github.com/suchithm/SampleListView/
These are the some issues and the solution i find out during my coding. Hope it might helpful to somebody who encounter with the same issues.
This comment has been removed by a blog administrator.
ReplyDeleteThanks for your comment,but I don't allow comments with Ads
DeleteHi Suchith Madavu,
ReplyDeleteI'm having the same problems with xamarin Android lists. I just tried your first method to get the things done but i've got a cast roblem when i try to use
viewGroup = (ViewGroupItem)rowView.Tag;
It's telling me "Specified Cast is not valid".
Have you got any idea?
This is my code
public override View GetView(int position, View convertView, ViewGroup parent)
{
var item = items[position];
View rowView = convertView; // re-use an existing view, if one is available
if (rowView == null)
{
rowView = context.LayoutInflater.Inflate(Resource.Layout.showFamilyFoodItem, null);
viewHolder = new ViewHolderItem();
viewHolder.txtName = rowView.FindViewById(Resource.Id.txtFoodFamilyItem);
viewHolder.lin = rowView.FindViewById(Resource.Id.linFamilyFood);
viewHolder.lin.Click += delegate (object sender, EventArgs e)
{
int pos = (int)((LinearLayout)sender).Tag;
Variables.selected_foodItem = items[pos];
Variables.conto.Insert(0, new FoodConto()
{
item = Variables.selected_foodItem,
Quantità = 1,
Portata = Variables.portata,
Aggiunte = null,
Comanda = 0,
Separato = Variables.separato
});
Thread.Sleep(300);
lsConto.Adapter = new ContoListAdapter(context, Variables.conto.FindAll(x => x.Separato == Variables.separato), lsConto);
};
//attaching viewholder reference to row
rowView.Tag = viewHolder;
}
else
{
//during row re-use get an instance
viewHolder = (ViewHolderItem)rowView.Tag;
}
viewHolder.lin.Tag = position;
viewHolder.txtName.Text = String.Concat(item.Name, " - ", item.Price.ToString("n2"));
return rowView;
}
class ViewHolderItem : Java.Lang.Object
{
internal TextView txtName;
internal LinearLayout lin;
}
Hi, do you have view holder class with name "ViewGroupItem", as it is not there in ur posted code here.
ReplyDeleteviewHolder = (ViewHolderItem)rowView.Tag;
DeleteThis line of code tells me "Specified cast is not valid".
Sorry my bad for what i wrote before.
Just Found the solution. The problem was that set the click event in my row LinearLayout, after i set ot in the txtName and delete the LinearLayou reference it worked perfect. Thanks for your help.
ReplyDelete