Clicking a SegmentedControl to UITableViewCell in Monotouch causes SIGABRT - uitableview

In my Monotouch application for the iPhone, I'm trying to add a segmented controller to a UITableViewCell to act as a toggle for a value in each row. Everything displays correctly, and the first rows will initially work correctly when clicked. However, after the row has left the screen and is reloaded, if I clicked the segmented controller again I receive an exception:
Terminating runtime due to unhandled exception
[ERROR] FATAL UNHANDLED EXCEPTION: System.Exception: Selector invoked from objective-c on a managed object of type MonoTouch.UIKit.UIControlEventProxy (0x16D793C0) that has been GC'ed ---> System.MissingMethodException: No constructor found for MonoTouch.UIKit.UIControlEventProxy::.ctor(System.IntPtr)
I have heard of similar issues with people using Monotouch try to put buttons in table cells. The general recommendation has been to add each table cell to a List to prevent it from being garbage collected, but this has not worked for me.
Is there a solution to get around this problem?
Here is a somewhat simplified version of my GetCell code:
public override UITableViewCell GetCell (UITableView tableView, NSIndexPath indexPath)
{
Hashtable rowData = dataList [indexPath.Row];
FilterListToggleCell cell;
cell = (FilterListToggleCell)tableView.DequeueReusableCell ("FilterTogglePlusCell");
cacheList.Add(cell.Toggle);//cache only the segcontrol
//cacheList.Add(cell);//also previously tried caching the whole cell
cell.Title.Text = (string)rowData["title"];
if(additionalFields.Contains((string)rowData["key"])){
cell.Toggle.SelectedSegment = 0;
} else {
cell.Toggle.SelectedSegment = 1;
}
cell.Toggle.ValueChanged += (sender, e) => {
UISegmentedControl toggle = (UISegmentedControl)sender;
if(toggle.SelectedSegment == 0){
Console.WriteLine("Toggle YES");
} else {
Console.WriteLine("Toggle NO");
}
};
cell.Toggle.Tag = indexPath.Row;
return cell;
}

You're right about the basic condition. This condition happens because there's no managed reference to the UITableViewCell that you return from the GetCell method. As such the GC can collect and free it (and that will cause issues for things like events that wants to call back to the managed instance). See this answer for more details.
Now I'm not sure why this approach did not work for you. However your code above seems incomplete, e.g.
cell = (FilterListToggleCell)tableView.DequeueReusableCell ("FilterTogglePlusCell");
can return null which is when you should be creating more cells (in fact there's no condition where you actually create any new cell, so there's nothing that can be re-used).
Your next line will throw a NullReferenceException when cell is null (because of cell.Toggle).
Note that this does not explain your crash - I suspect there's some missing code that make it works (create cells) and that might not cache them correctly.

I was able to get this working without needing the extra cache based on information from this question: How do I remove/unregister event handler from an event?
The ValueChanged line was replaced with:
cell.Toggle.AddTarget(this, new Selector("ToggleChange"),UIControlEvent.ValueChanged);
and another method was added to my UITableViewDataSource
[Export ("ToggleChange")]
void OnChange(UISementedControl toggle){
if(toggle.SelectedSegment == 0){
Console.WriteLine("Toggle YES");
} else {
Console.WriteLine("Toggle NO");
}
}

Related

Why am I getting a System.InvalidCastException when using a ViewCellRenderer in iOS Xamarin Project

I'm using a ViewModel first approach to Xamarin.Forms and have begun the process of writing my own Bindable TableView (I imagine plenty of people have). The project is going well and I'm already rendering cells in the UI based on my on CellViewModel types and wanted to move to the next phase of adding the 'effects' of things like 'Disclosure' and 'Checkbox' accessories to cells. It transpires that these things only really make sense in iOS projects so I found myself looking into ViewCellRenderers specifically in iOS.
In order to apply the appropriate accessory on the cell, I needed to create a class to do so:
public class AccessoryItemCellRenderer : ViewCellRenderer
which itself is relatively simple. It takes the BindingContext of the Xamarin Cell and then applies the accessory as appropriate:
var viewModel = item.BindingContext as TableCellViewModel;
if (viewModel != null )
{
UITableViewCell cell = base.GetCell(item, reusableCell, tv);
if (viewModel.Accessories == CellIndicators.Disclosure)
cell.Accessory = UIKit.UITableViewCellAccessory.DisclosureIndicator;
else if (viewModel.Accessories == CellIndicators.DisclosureDetail)
cell.Accessory = UIKit.UITableViewCellAccessory.DetailDisclosureButton;
else if (viewModel.Accessories == CellIndicators.Detail)
cell.Accessory = UIKit.UITableViewCellAccessory.DetailButton;
else if (viewModel.Accessories == CellIndicators.CheckMark)
cell.Accessory = UIKit.UITableViewCellAccessory.Checkmark;
}
At least I thought it was straightforward, as when the base.GetCell call gets made, it turns out my reusableCell property is null and this I am assuming is causing a System.InvalidCastException which then blow up. It's not very obvious to me what is causing it, the only real stack trace I get is this:
at Xamarin.Forms.Platform.iOS.ViewCellRenderer.GetCell (Xamarin.Forms.Cell item, UIKit.UITableViewCell reusableCell, UIKit.UITableView tv) [0x00000] in C:\BuildAgent2\work\aad494dc9bc9783\Xamarin.Forms.Platform.iOS\Cells\ViewCellRenderer.cs:28
Is it because somehow my cell doesn't have a reusable id? How do I provide one if this is the case? Any help would be greatly appreciated.
In the comments you mention that you're registering this AccessoryItemCellRenderer for TextCell. In the AccessoryItemCellRenderer you are inheriting from ViewCellRenderer which is for ViewCell. TextCell does not inherit from ViewCell and cannot be cast as a ViewCell and that's most likely where the exception is coming from.

iOS tableview cell is empty at random

Screenshot of weird behavior
The screenshot tells is quite well. I have a tableview with dynamic custom cells. I added a println for one of the contents of the cell to check, if the labels are set. I can see in the debug log, that each cell has its content. Still, on the device there are empty cells at random, which means, the row, where no content appears, is changing a lot. Even just scrolling up and down makes the second row disappear, but the third row is filled. Scrolling again turns this around again. If I close the app and start it again, every row is filled correctly.
Here is the code for the cell generation:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Return a count picker cell
if countPickerTableRow == indexPath.row {
...
}
// Return a normal wish list entry cell
else {
let article = wishListEntries[indexPath.row]!
let cell = tableView.dequeueReusableCellWithIdentifier("ArticleCell", forIndexPath: indexPath) as! WOSArticleCell
// Correct the order in case a count picker cell was inserted
var row = indexPath.row
if countPickerTableRow != -1 && indexPath.row > countPickerTableRow {
row--
}
cell.setThePreviewImage(UIImage(data: article.thumbnail))
cell.setArticleName(article.name)
cell.setArticleDescription(article.text)
cell.setArticleNumber(article.number)
cell.setArticleCount(article.count as Int)
cell.setOrderInTable(row)
cell.setTableViewController(self)
cell.setNeedsDisplay()
cell.setInputAccessoryView(numberToolbar) // do it for every relevant textfield if there are more than one
println(String(indexPath.row) + " " + cell.nameLabel.text!)
return cell
}
}
In the custom cell class there is nothing special. Just a few outlets to the labels.
Here is a screen of the storyboard:
Storyboard
Can anyone please help me finding out what is going on here? I can't find the reason why the debug log can output the contents of a cell, but the device is not able to render them.
You should change the logic of your code. If the PickerCell comes up just call reloadData() and reload everything in the tableview. If the amount of rows you have is small this won’t be an issue and it’s not an expensive operation as you are not doing any heavy calculating during display.
If you need to update only a single cell because of changes you made in the PickerCell then you should be calling reloadRowsAtIndexPaths:withRowAnimation: with the indexPath of the cell to be updated.
Your issue is with your subclass WOSArticleCell. Have you implemented prepareForUse()? If you have, are you setting any properties to nil?
UITableViewCell Class Reference
Discussion
If a UITableViewCell object is reusable—that is, it has a reuse
identifier—this method is invoked just before the object is returned
from the UITableView method dequeueReusableCellWithIdentifier:. For
performance reasons, you should only reset attributes of the cell that
are not related to content, for example, alpha, editing, and selection
state. The table view's delegate in tableView:cellForRowAtIndexPath:
should always reset all content when reusing a cell. If the cell
object does not have an associated reuse identifier, this method is
not called. If you override this method, you must be sure to invoke
the superclass implementation.

UITableView Lags when user scrolling

I'm using Xamarin to develop a mobile app in ios.
The problem is that when you scrolling the UITableView lags and its too slow.
Here my method GetCell:
public override UITableViewCell GetCell (UITableView tableView, NSIndexPath indexPath)
{
string identifier = #"PostCell";
var post = Posts [indexPath.Row];
PostCellVC postCell = tableView.DequeueReusableCell (identifier) as PostCellVC;
if (postCell == null)
{
postCell = new PostCellVC (identifier);
}
postCell.btnComments.SetTitle("0 Comments",UIControlState.Normal);
postCell.lblDescription.Text = post.PostText;
postCell.btnComments.SetTitle(string.Format("{0} Comments",post.Comments.Length.ToString()),UIControlState.Normal);
postCell.btnLikes.SetTitle(post.Likes.ToString() + " Likes",UIControlState.Normal);
postCell.btnUser.SetTitle(post.UserName.ToString(),UIControlState.Normal);
postCell.imgLike.Image = UIImage.FromBundle ("Images/likeOn.png");
var actionSheet = new UIActionSheet ("Share options");
actionSheet.CancelButtonIndex = 2;
actionSheet.AddButton ("Facebook");
actionSheet.AddButton ("Twitter");
actionSheet.AddButton ("Cancel");
postCell.lblFecha.Text = GetPrettyDate (post.Date);
//postCell.btnShare.TouchUpInside += share;
if (post.PictureUrl != null) {
postCell.imgPost.Image = LayoutHelper.ImageFromUrl (post.PictureUrl);
}
return postCell;
}
As #CRDave points out, you should try lazy loading your images. Here is a sample of how to do this in Xamarin.
Also, why are you creating a new ActionSheet for every cell (which you don't use)? Since you can only show one ActionSheet at a time, just create it once and use it for every cell.
Finally, try reusing UIImage for "likeOn.png" instead of loading it from the bundle for every cell.
You shouldn't add views or download images in your GetCell method. This method is called repeatedly as the user scrolls the table, if it does any long-running operation, it will lead to laggy scrolling.
The method implementation should be really lightweight. The only things that should be done is to dequeue a reusable cell or create a new one and update the data for that cell.
In your specific case, the lag is cause by downloading a photo from a remote URL. This image download should be lazy loaded. Ideally the download itself occurs in a background thread and once the photo is downloaded, then only thing done in the UI thread is updating the UIImageView with the photo.
A few good resources for anyone experiencing issues like this:
https://learn.microsoft.com/en-us/xamarin/ios/user-interface/controls/tables/customizing-table-appearance
https://github.com/luberda-molinet/FFImageLoading
Please check this topic Xcode table view lag when scrolling also you can check this topic iOS table view lag issue, i'm using dispatch and save cache you should save your datas on cache and also use dispatch_async

Delete dynamic cell in GetCell method

I am trying trying to remove a cell from the UITableView if the image for the cell is a bad image. Basically my code does a threadPool call for each cell's image to make the flow of binding the data as a user scroll smooth as so inside the GetCell method:
public override UITableViewCell GetCell (UITableView tableView, MonoTouch.Foundation.NSIndexPath indexPath)
{
//other stuff
ThreadPool.QueueUserWorkItem(new WaitCallback(RetrieveImage),(object)cell);
}
within that asynchronous method I call the cell.imageUrl property and bind it to an NSData object as so:
NSData data = NSData.FromUrl(nsUrl); //nsUrl is cell.imageUrl
From there I know that if data==null then there was a problem retrieving the image so I want to remove it. What I currently do is set the hidden property to true, but this leaves a blank space. I want to remove the cell entirety so the user won't even know it existed.
I can't set the cell height to 0 because I don't know if the imageUrl is bad until that indexrowpath initiates getcell call. I don't want to just check all images from the data thats binded to the UITableView because that would be a big performance hit since it can be easily a hundred items. I am currently grabbing the items from a webservice that gives me the first 200 items.
How can I remove the cell altogther if the data==null example
NSUrl nsUrl = new NSUrl(cell.imageUrl);
NSData data = NSData.FromUrl(nsUrl);
if (data != null) {
InvokeOnMainThread (() => {
cell.imgImage = new UIImage (data);
//cell.imgImage.SizeToFit();
});
} else {
//I tried the below line but it does't work, I have a cell.index property that I set in the getCell method
_data.RemoveAt(cell.Index);//_data is the list of items that are binded to the uitableview
InvokeOnMainThread (() => {
//cell.Hidden = true;
});
}
You remove cells by removing them from your source and then you let UITableView know that the source has changed by calling ReloadData(). This will trigger the refresh process. UITableView will ask your source how many rows there are and because you removed one row from your data, the cell representing that row will be gone.
However ReloadData() will reload your entire table. There is a methods which allows you to remove cells specifically, named DeleteRows(). You can find an example here at Xamarin.
When deleting rows it is important that you update your model first, then update the UI.
Alternatively, I recommend you to check out MonoTouch.Dialog which allows interaction with UITableView in a more direct way. It also includes support for lazy loading images.

Monotouch: The correct way to reuse a UITableViewCell

I'm figuring out the correct way to reuse cells in a UITableView and I would know if the mechanism I'm using is correct.
The scenario is the following.
I've a UITableView that displays a list of data obtained from a web service.
_listOfItems = e.Result as List<Item>;
where _listOfItems is an instance variable.
This list is passed to a class that extends UITableViewSource. Obviously this class override GetCell method to visualize data in this manner:
public override UITableViewCell GetCell (UITableView tableView, MonoTouch.Foundation.NSIndexPath indexPath)
{
UITableViewCell cell = tableView.DequeueReusableCell(_cID);
Item item = _listOfItems[indexPath.Row];
int id = item.Id;
if (cell == null)
{
cell = new UITableViewCell(UITableViewCellStyle.Value1, _cID);
cell.Tag = id;
_cellControllers.Add(id, cell);
}
else
{
bool vb = _cellControllers.TryGetValue(id, out cell);
if(vb)
{
cell = _cellControllers[id];
}
else
{
cell = new UITableViewCell(UITableViewCellStyle.Value1, _cID);
cell.Tag = id;
_cellControllers.Add(id, cell);
}
}
cell.TextLabel.Text = item.Title;
return cell;
}
where
_cID is an instance variable identifier for the cell
string _cID = "MyCellId";
_cellControllers is a dictionary to store cell and the relative id for an Item instance
Dictionary<int, UITableViewCell> _cellControllers;
I'm using the dictionary to store cells bacause when a row is tapped, I have to retrieve the id for the cell tapped (through cell.Tag value), do some other operation - i.e.retrieve other some data from the service - and then update that cell again with new values. In this case each cell has to be unique.
So, my question is:
Is this the right manner to reuse cell or is it possible to figured out another solution to reuse cell and guaranteeing each tap for a cell is unique?
I hope it's all clear :) Thank you in advance. Regards.
I think what you are doing here is a bit too much. Especially the part where you are holding extra references to cells in a Dictionary.
I would do it differently. The DequeueReusableCell method is there so that you can retrieve any existing cell, but not necessarily the values it contains. So something like this will suffice:
public override UITableViewCell GetCell (UITableView tableView, NSIndexPath indexPath)
{
int rowIndex = indexPath.Row;
Item item = _listOfItems[rowIndex];
UITableViewCell cell = tableView.DequeueReusableCell(_cID);
if (cell == null)
{
cell = new UITableViewCell(UITableViewCellStyle.Value1, _cID);
}
// Store what you want your cell to display always, not only when you are creating it.
cell.Tag = item.ID;
cell.TextLabel.Text = item.Title;
return cell;
}
Then, in the RowSelected method, change your data source:
public override void RowSelected (UITableView tableView, NSIndexPath indexPath){
// Do something here to change your data source _listOfItems[indexPath.Row] = new Item();
}
If I understand correctly what you want to do, this solution is better.
You will save yourself a lot of trouble if you use my MonoTouch.Dialog library that takes care of all of these details for you and does exactly what you want and more. It will let you focus on your app instead of focusing on the administrivia, and I believe will let you move faster, you can get it from:
http://github.com/migueldeicaza/MonoTouch.Dialog
The first problem with this code is that this hardcodes the cells to a single type. In general, you would have to first check the section/row and based on this information determine the kind of cell that you want.
Once you determine the kind of cell you want, then you use this information to call DequeueReusableCell with the token associated with this cell ID. This is different for example for a cell that contains an entry line vs a cell that contains an image. You need to dequeue the right kind of cell.
You do not need to make every cell unique, all the information that you need is in the section/row, so what you need is something that maps a section/row to your unique cell. A simple approach is that there is a single section, and the row is the index into your data array that you want to fetch more information from.
A small tutorial on best practices when creating these cells can be found here:
http://tirania.org/monomac/archive/2011/Jan-18.html
If you were using MonoTouch.Dialog, your whole code could be:
var elements = "foo, bar, baz";
var dvc = new DialogViewController ();
dvc.Root = new RootElement ("My results") {
from x in elements.Split (',')
select (Element) new StringElement (x);
};
dvc.Root.Add ("Final one");
current.PresentModalViewController (dvc, true);
The above creates a UITableView with 4 rows, one for "foo", "bar" and "baz" using Linq, and adds an extra node at the end just to show how to use the Add API.
There are also many assorted elements you can use, you are not limited to String elements.

Resources