Pictar Pictar - 2 months ago 4
C# Question

How can I update my indexPath after deleting a Row in a TableView?

I'm new to

Xamarin
and trying to make some basic todolist iOS app.

After struggling for a while with
TableViews
, I now have a TableSource, and custom cells containing a
UISwitch
.

My goal is, when the user activate the
UISwitch
, the cell is deleted for the
TableView
. To do that I created some custom event in my custom cell.

public partial class TodoTableViewCell : UITableViewCell
{
public delegate void DeleteTodoEventHandler(UITableView tableView, NSIndexPath indexPath);
public event DeleteTodoEventHandler DeleteTodo;

partial void DeleteTodoEvent(UISwitch sender)
{
if (DeleteTodo != null)
DeleteTodo(null, new NSIndexPath());
}

public TodoTableViewCell(IntPtr p):base(p)
{

}

public TodoTableViewCell(string reuseIdentifier) : base(UITableViewCellStyle.Default, reuseIdentifier)
{

}

public void UpdateCell(TodoItem Item)
{
TodoLabel.Text = Item.Name;
DateLabel.Text = Formating.formatDate(Item.Date);
HourLabel.Text = Formating.formatTime(Item.Date);
}
}


DeleteTodoEvent is called when the user activate the
UISwitch
. Here is my tablesource :

public class TableSource : UITableViewSource
{

List<TodoItem> TableItems;
string CellIdentifier = "TodoCell";

...

public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
TodoTableViewCell cell = tableView.DequeueReusableCell(CellIdentifier) as TodoTableViewCell;
TodoItem item = TableItems[indexPath.Row];

//---- if there are no cells to reuse, create a new one
if (cell == null)
{
cell = new TodoTableViewCell(CellIdentifier);
}

cell.DeleteTodo += (table, index) => DeleteTodoHandler(tableView, indexPath);

cell.UpdateCell(item);

return cell;
}

...

private void DeleteTodoHandler(UITableView tableView, NSIndexPath indexPath)
{
TableItems.RemoveAt(indexPath.Row);
// delete the row from the table
tableView.DeleteRows(new NSIndexPath[] { indexPath }, UITableViewRowAnimation.Fade);
}
}


Basically, DeleteTodoHandler is called whenever the user click on the
UISwitch
. It correctly deletes the cell at first. However, the indexPath is never updated.

By that I mean : Imagine I have 3 cells in my TodoList.


Cell1 -> indexPath.Row = 0
Cell2 -> indexPath.Row = 1
Cell3 -> indexPath.Row = 2


If I delete the second one, Cell2, the indexPath from Cell3 should be = to 1 and not 2. This is not the case. Which means that if I try to delete Cell3 after that,

TableItems.RemoveAt(indexPath.Row)


will try to remove the item 3 from the list which is leading us to an exception since there are only 2 items in the list.

I'm following the examples I found in the Xamarin Doc for the deletion of rows in a TableView but in my case it's obviously not working. The indexPath.Row being read-only, what can I do guys to correctly update the indexPath after deleting an item from the TableView ?

Answer

You should use ViewController which include both of the UITableView and you custom UITableSource object to handle the delete event.

Then you can get the correct row you clicked by the method from UITableView like this:

Foundation.NSIndexPath indexPath = tableView.IndexPathForCell(cell); 

And delete the row's data from your data list and your UI in the same time.

This is a sample code I wrote for you, take a look:

This is the ViewController:

public override void ViewDidLoad ()
        {
            //Create a custom table source
            MyTableSource mySource = new MyTableSource ();

            //Create a UITableView
            UITableView tableView = new UITableView ();
            CGRect tbFrame = this.View.Bounds;
            tbFrame.Y += 20;
            tbFrame.Height -= 20;
            tableView.Frame = tbFrame;
            tableView.Source = mySource;
            tableView.RowHeight = 50;
            this.Add (tableView);

            //Handle delete event
            mySource.DeleteCell += (cell) => {
                //Get the correct row
                Foundation.NSIndexPath indexPath = tableView.IndexPathForCell(cell);
                //Remove data from source list
                mySource.RemoveAt(indexPath.Row);
                //Remove selected row from UI
                tableView.BeginUpdates();
                tableView.DeleteRows(new Foundation.NSIndexPath[]{indexPath},UITableViewRowAnimation.Fade);
                tableView.EndUpdates();
            };
        }

This MyTableSource.cs:

public delegate void DeleteCellHandler(UITableViewCell cell);
        public event DeleteCellHandler DeleteCell;

        private string CellID = "MyTableCell";
        private List<string> tableItems;

        public MyTableSource ()
        {
            tableItems = new List<string> ();
            for (int i = 0; i < 10; i++) {
                tableItems.Add ("Test Cell " + i.ToString ());
            }
        }

        public void RemoveAt(int row)
        {
            tableItems.RemoveAt (row);
        }

        #region implement from UITableViewSource

        public override nint RowsInSection (UITableView tableview, nint section)
        {
            return tableItems.Count;
        }

        public override UITableViewCell GetCell (UITableView tableView, Foundation.NSIndexPath indexPath)
        {
            MyTableCell cell = tableView.DequeueReusableCell (CellID) as MyTableCell;
            if (null == cell) {
                //Init custom cell
                cell = new MyTableCell (UITableViewCellStyle.Default, CellID);
                //Bind delete event
                cell.DeleteCell += delegate {
                    if(null != DeleteCell)
                        DeleteCell(cell);
                };
            }
            cell.Title = tableItems [indexPath.Row];
            return cell;
        }

        #endregion

This is MyTableCell.cs:

public class MyTableCell : UITableViewCell
    {
        public delegate void DeleteCellHandler();
        public event DeleteCellHandler DeleteCell;

        UILabel lbTitle = new UILabel ();
        UIButton btnDelete = new UIButton (UIButtonType.System);

        public string Title{
            get{ 
                return lbTitle.Text;
            }
            set{ 
                lbTitle.Text = value;
            }
        }

        public MyTableCell (UITableViewCellStyle style, string reuseID) : base(style,reuseID)
        {
            lbTitle.Text = "";
            lbTitle.Frame = new CoreGraphics.CGRect (0, 0, 100, 50);
            this.AddSubview (lbTitle);

            btnDelete.SetTitle ("Delete", UIControlState.Normal);
            btnDelete.Frame = new CoreGraphics.CGRect (120, 0, 50, 50);
            this.AddSubview (btnDelete);
            btnDelete.TouchUpInside += delegate {
                if(null != DeleteCell)
                    DeleteCell();
            };
        }
    }

Hope it can help you.

If you still need some advice, leave me a message here.

------------------------------New------------------------------------

I just suggest you move all logic code into the ViewController, because according to the MVC model, it should in Controller layer.

And if you want to manage your data by invoking some API or loading data from your local database, it will be more convenient.

Of cause you can put your delete event in your TableSource, just need to remove the event from controller and put it in TableSource, like:

ViewController.cs:

public override void ViewDidLoad ()
        {
            //Create a custom table source
            MyTableSource mySource = new MyTableSource ();

            //Create a UITableView
            UITableView tableView = new UITableView ();
            CGRect tbFrame = this.View.Bounds;
            tbFrame.Y += 20;
            tbFrame.Height -= 20;
            tableView.Frame = tbFrame;
            tableView.Source = mySource;
            tableView.RowHeight = 50;
            this.Add (tableView);

//          //Handle delete event
//          mySource.DeleteCell += (cell) => {
//              //Get the correct row
//              Foundation.NSIndexPath indexPath = tableView.IndexPathForCell(cell);
//              //Remove data from source list
//              mySource.RemoveAt(indexPath.Row);
//              //Remove selected row from UI
//              tableView.BeginUpdates();
//              tableView.DeleteRows(new Foundation.NSIndexPath[]{indexPath},UITableViewRowAnimation.Fade);
//              tableView.EndUpdates();
//          };
        }

In TableSource.cs, change GetCell method like this one:

public override UITableViewCell GetCell (UITableView tableView, Foundation.NSIndexPath indexPath)
        {
            MyTableCell cell = tableView.DequeueReusableCell (CellID) as MyTableCell;
            if (null == cell) {
                //Init custom cell
                cell = new MyTableCell (UITableViewCellStyle.Default, CellID);
                //Bind delete event
                cell.DeleteCell += delegate {
//                  if(null != DeleteCell)
//                      DeleteCell(cell);
                    //Get the correct row
                    Foundation.NSIndexPath dIndexPath = tableView.IndexPathForCell(cell);
                    int deleteRowIndex = dIndexPath.Row;
                    Console.WriteLine("deleteRowIndex = "+deleteRowIndex);
                    //Remove data from source list
                    RemoveAt(deleteRowIndex);
                    //Remove selected row from UI
                    tableView.BeginUpdates();
                    tableView.DeleteRows(new Foundation.NSIndexPath[]{dIndexPath},UITableViewRowAnimation.Fade);
                    tableView.EndUpdates();
                };
            }
            cell.Title = tableItems [indexPath.Row];
            return cell;
        }

The only mistake you made is you should not add any parameter in your custom cell's delete event handle.

Because when you delete a cell from your table, the other cells will never update their own IndexPath.

For example, if you have 3 rows, and you delete the second one, it works. But when you try to delete the third row(It's the second right now,because you just delete the second one), you will get a exception about that you don't have 3 rows or index out of bounds, because this cell's IndexPath is still 2(The correct one is 1).

So you need calculate the index in your delete event handler using UITableView's method "IndexPathForCell" like I mention above. It will always get the correct index.

------------------About your new question--------------------

The mistake you made is about delete event init, you must init it when cell is constructed, like I did:

public override UITableViewCell GetCell (UITableView tableView, Foundation.NSIndexPath indexPath)
        {
            MyTableCell cell = tableView.DequeueReusableCell (CellID) as MyTableCell;
            if (null == cell) {
                cell = new MyTableCell (UITableViewCellStyle.Default, CellID);
                //Bind delete event
                cell.DeleteCell += delegate {
                    //Delete stuff            
                };
            }
            cell.Title = tableItems [indexPath.Row];
            return cell;
        }

If you just use it like yours:

if (null == cell) {
    cell = new MyTableCell (UITableViewCellStyle.Default, CellID);
}
//Bind delete event
cell.DeleteCell += delegate {
    //Delete stuff            
};
cell.Title = tableItems [indexPath.Row];

It will cause duplicated binding problem, because your table cell is reused, so when you try to refresh/insert/delete or even scroll your UITableView, "GetCell" method will be trigged, so when you try to invoke your cell's delete event, there are multiple delegates will be invoked, the first is working, and if you have at least 2 delegates, it will cause the problem you met.

The effect is like this screenshot: Error screenshot Reproduce step: 1 Add a new item for tableview; 2 Delete the item you just added; 3 Add a new item again; 4 Delete the item you just added again;

Because there a 2 delegates, so it thrown an exception.

At last, I have uploaded the complete sample code to my GitHub, download it if you need.

UITalbViewManagement Sample code

Comments