xamarin/mvvmcross UITableView with static content - uitableview

I've searched around the xamarin tutorials and over the various posts about UITableView in xamarin but I couldn't found whatever about UITableView with static cells.
What I'm trying to achieve is a simple detail screen like twitterific app, but without Nib or storyboards (using mvvmcross, storyboards aren't available and Nib files prevent from using static UITableView, at least I couldn't find any way to do it)
Furthermore, after trying differents solutions I've ended up with something like this:
UITableViewController
public override int NumberOfSections(UITableView tableView)
{
return 1;
}
public override int RowsInSection(UITableView tableview, int section)
{
return 1;
}
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
UITableViewCell cell = tableView.DequeueReusableCell("test");
if (cell == null)
{
cell = new SejourInfoViewPatientCell();
//cell = new UITableViewCell(UITableViewCellStyle.Subtitle, "test");
}
//cell.TextLabel.Text = "test";
return cell;
}
But now the mvvmcross binding doesn't works. If I take the exact same binding and use it on a non static UITableViewController everything works fine
If someone could point me to a direction I'll be glad

I've been struggling with the same problem. I was able to get around the binding issue by having my view inherit from MvxViewController then in ViewDidLoad:
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
UITableView table = new UITableView();
Action<UITableViewCell> initializers = new Action<UITableViewCell>[] {
(cell) => {
this.CreateBinding(cell.TextLabel)
.For(c => c.Text)
.To(x => x.FirstName)
.Apply();
},
(cell) => {
this.CreateBinding(cell.TextLabel)
.For(c => c.Text)
.To(x => x.LastName)
.Apply();
}
};
StubDataSource source = new StubDataSource(table, initializers);
table.Source = source;
}
StubDataSource inherits from MvxStandardTableViewSource (though depending on your needs you'll probably want to inherit off UITableViewSource (overriding GetCell instead).
public class StubDataSource : MvxStandardTableViewSource
{
private readonly Action<UITablleViewCell>[] _inits;
public StubDataSource(
UITableView tableView,
Action<UITablleViewCell>[] inits)
: base(tableView)
{
_inits = inits;
}
public override int NumberOfSections(UITableView tableView)
{
return 1;
}
public override int RowsInSection(UITableView tableview, int section)
{
return 2;
}
protected override UITableViewCell GetOrCreateCellFor(UITableView tableView, NSIndexPath indexPath, object item)
{
var reuse = base.CreateDefaultBindableCell(tableView, indexPath, item);
switch(indexPath.Row)
{
case 0:
_inits[0](reuse);
case 1:
_inits[1](reuse);
}
return reuse;
}
}
Two points to note,
This is a bit of hack (thanks Closures), but its the only way to bind to static tables in ios that I can think of. Hopefully someone will suggest a better way.
The second is when you create custom cells with subviews (e.g. UITextFields etc) you should create a custom cell type and have those subviews as a class variable, otherwise the GC will collect them you'll get SIGSEGV exceptions see

I'm not sure what a static TableView or Cell are, but I've just posted a complete sample using "custom cells" on https://github.com/slodge/ListApp in answer to MvxTableViewSource DequeueReusableCell issue when scrolling
The key parts were:
ensure the custom cell includes an IntPtr constructor
use RegisterClassForCellReuse on the custom cell type
override GetOrCreateCellFor in your table source - and use DequeueReusableCell to create the cell
For variable height cells, you'll also need to override GetHeightForRow within the table source

Related

ios: UITableVIewCell expand not working as expected

My TableView cell has a button. On click, I want to expand the cell. For expanding I'm changing the height of the cell on button click and reload the row. But when one row is expanded and then I try to expand another row, the behaviour is not as expected. The previously expanded row closes instead of clicked row.
Also, sometimes I've to click the button twice to expand.
public class ProgramMeasurementDetailsTableViewSource : UITableViewSource
{
List<ProgramMeasurementDetail> mList;
string HeaderIdentfier = "programMeasurementDetailsHeader";
string CellIdentifier = "programMeasurementDetailsCell";
int TAG_CHECKBOX = 61;
int TAG_LABEL_VITAL_NAME = 62;
int TAG_LABEL_GOAL = 63;
int TAG_BUTTON_EDIT = 64;
int TAG_VIEW_HEADER_COLUMN_SEPARATOR = 66;
int TAG_VIEW_ROW_COLUMN_SEPARATOR = 65;
List<bool> expandStatusList = new List<bool>();
UITableView tableView;
public ProgramMeasurementDetailsTableViewSource(List<ProgramMeasurementDetail> mList, UITableView tableView)
{
this.tableView = tableView;
this.mList = mList;
for (int i = 0; i < mList.Count; i++)
expandStatusList.Add(false);
}
public override nint NumberOfSections(UITableView tableView)
{
return 1;
}
public override nint RowsInSection(UITableView tableview, nint section)
{
return mList.Count;
}
public override nfloat GetHeightForHeader(UITableView tableView, nint section)
{
return 32;
}
public override nfloat GetHeightForRow(UITableView tableView, Foundation.NSIndexPath indexPath)
{
if (expandStatusList[indexPath.Row])
return 70;
else
return 32;
}
public override UITableViewCell GetCell(UITableView tableView, Foundation.NSIndexPath indexPath)
{
UITableViewCell cell = tableView.DequeueReusableCell(CellIdentifier);
if (cell == null)
{
cell = new UITableViewCell(UITableViewCellStyle.Default, CellIdentifier);
}
CheckBox checkBox = (CheckBox)cell.ViewWithTag(TAG_CHECKBOX);
UILabel vitalNameLabel = (UILabel)cell.ViewWithTag(TAG_LABEL_VITAL_NAME);
UILabel goalLabel = (UILabel)cell.ViewWithTag(TAG_LABEL_GOAL);
UIButton editGoalButton = (UIButton)cell.ViewWithTag(TAG_BUTTON_EDIT);
editGoalButton.TouchUpInside += (object sender, EventArgs e) =>
{
expandStatusList[indexPath.Row] = !expandStatusList[indexPath.Row];
tableView.ReloadRows(new Foundation.NSIndexPath[] { indexPath }, UITableViewRowAnimation.Automatic);
};
.....
return cell;
}
}
I tried moving the button click event handling inside the if (cell == null), then but button click/expand is not working at all. I put breakpoint and it seems that part never gets executed.
I've the cell layout designed in storyboard.
I've struggling with this problem for quite sometime now. Any help is appreciated.
Does the bug you see happen when you scroll the list up and down? This is happening because you are wiring up the TouchUpInside event handler in the GetCell method and then it never get unhooked when the cell is reused, so you end up with multiple cells wired to the same handler or even multiple handlers!
In general, you don't want to do these types on things in the GetCell method, it should only be used to DequeueReusableCell or create a new one. Everything else should be done within a custom cell.
Looking at your code example, it doesn't look like you are using a custom cell, so what you could do is this:
1.) In GetCell set the tag of the button as the row of the IndexPath
editGoalButton.Tag = indexPath.Row;
2.) In ProgramMeasurementDetailsTableViewSource Create a separate method for your event handler:
private void ToggleRow(object sender, EventArgs e)
{
var btn = sender as UIButton;
expandStatusList[btn.Tag] = !expandStatusList[btn.Tag];
tableView.ReloadRows(new Foundation.NSIndexPath[] { NSIndexPath.FromRowSection(btn.Tag, 0) }, UITableViewRowAnimation.Automatic);
}
3.) In GetCell Unhook before you rehook the TouchUpInside handler:
editGoalButton.TouchUpInside -= ToggleRow;
editGoalButton.TouchUpInside += ToggleRow;
While this will make sure that the button is only hooked up once, it really isn't the right way to handle a situation like this. You should be using a custom cell and doing all the wiring in the cell and then the unhook and clean up in the PrepareForReuse override in UITableViewCell. I would highly recommend going though this Xamarin tutorial
If you do go through it...notice how consolidated the GetCell method is.
--UPDATE--
In your CustomCell class:
private EventHandler _tapAction;
public void Setup(EventHandler tapAction, int row, ....)
{
//keep a reference to the action, so we can unhook it later
_tapAction = tapAction;
editGoalButton.Tag = row;
....
editGoalButton.TouchUpInside += _tapAction;
}
public override void PrepareForReuse()
{
....
editGoalButton.TouchUpInside -= _tapAction;
_tapAction = null;
}
And you would just pass through the ToggleRow method as the Action parameter when you call Setup in GetCell.

Outlets in Xamarin custom TableViewCell are null

I am trying to use a custom UITableViewCell. I am able to get the cell to instantiate, so that it is not null. However, all of the UILabel outlets in the cell are null. Thus, I am getting a null pointer exception.
This is similar to iOS custom TableViewCell class subviews return null and Label in custom cell is null when populating UITableViewController. Nevertheless, neither solutions have solved my problem. They suggest to make sure the identifier matches, which it does. They also tell me to register the cells for reuse, which I also do.
I have made sure that my identifier in the Xib file of the cell matches what I am using.
In my Source class:
public override UITableViewCell GetCell (UITableView tableView, NSIndexPath indexPath)
{
tableView.RegisterClassForCellReuse(typeof(BoardListCell), new NSString(BoardListCell.Key));
var cell = (BoardListCell) tableView.DequeueReusableCell(BoardListCell.Key);
if (cell == null)
{
var nib = UINib.FromName(BoardListCell.Key, NSBundle.MainBundle);
cell = (BoardListCell)nib.Instantiate (null, null) [0];
}
cell.setData("top", "not top"); //Sample data
return cell;
}
In my BoardListCell class:
public partial class BoardListCell : UITableViewCell
{
public static readonly UINib Nib = UINib.FromName ("BoardListCell", NSBundle.MainBundle);
public static readonly NSString Key = new NSString ("BoardListCell");
public BoardListCell() : base()
{
}
public BoardListCell (IntPtr handle) : base (handle)
{
}
public void setData(String top, String bottom)
{
exp.Text = top;
playTime.Text = bottom;
}
}
When I debug, the cell is being created. However, its outlets are null. If I instantiate them in the constructor, I don't get the error, but this obviously defeats the purpose of making prototypes in xcode as it wipes out the layout, etc.
I was having the same issue described above. The solution for me was to remove the call to TableView.RegisterClassForCellReuse(typeof(YourCustomeTableCell), "YourTableCellName");
If you created the Cell in the Xcode designer, there is no need to register the Cell.
Hope this helps anyone still having this issue.

iOS custom TableViewCell class subviews return null

I use Xamarin iOS designer to design custom TableViewCell class for my TableView.
But all cell subviews properties (outlets) return null except cell itself.
My custom cell class:
partial class VehiclesTableCell : UITableViewCell
{
public VehiclesTableCell(IntPtr handle) : base(handle) { }
public void UpdateCell(string licensePlate) {
licensePlateLabel.Text = licensePlate; //hit the null reference exception for licensePlateLabel
}
}
Generated partial class:
[Register ("VehiclesTableCell")]
partial class VehiclesTableCell
{
[Outlet]
[GeneratedCode ("iOS Designer", "1.0")]
UILabel licensePlateLabel { get; set; }
void ReleaseDesignerOutlets ()
{
if (licensePlateLabel != null) {
licensePlateLabel.Dispose ();
licensePlateLabel = null;
}
}
}
And GetCell of my Table Source class:
public class VehiclesTableSource : UITableViewSource
{
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath) {
// Dequeueing the reuse cell
var cell = (VehiclesTableCell)tableView.DequeueReusableCell(new NSString(typeof(VehiclesTableCell).Name));
// Set cell properties
cell.UpdateCell(LicensePlate);
// Return the cell with populated data
return cell;
}
}
As we see genereted code has an outlet for licensePlate so why is its property null?
Is storyboard not supposed to instantiate all of its subviews automatically?
At least its happening in all other situations.
I was having the same problem with my custom TableViewCell. I found out the issue was with TableView.RegisterClassForCellReuse (typeof(MyCell), MyCellId). I had to change it to TableView.RegisterNibForCellReuse(UINib.FromName("MyCell", NSBundle.MainBundle), MyCellId) in order to get my .xib loaded.
I came across this issue myself. This is what I did to resolve it:
Instantiate the components of the subview in the constructor of the custom cell. In your case, something like:
public VehiclesTableCell(IntPtr handle) : base(handle) {
licensePlateLabel = new UILabel();
}
Override the method LayoutSubviews:
public override void LayoutSubviews () {
base.LayoutSubviews ();
licensePlateLabel.Frame = new RectangleF(63, 5, 33, 33); // Your layout here
}
The info on this guide got me this far, but ideally there would be a way to accomplish this without instantiating and laying out the subviews components manually as they're already defined in the IOS designer. Hopefully someone can chime in with a better way to do this...
EDIT: I came across this answer which explained why I wasn't able to bind the custom table cell I created in the designer. TLDR: Ensure that Identity -> Class and Table View Cell -> Identifier are both set to the custom table view cell class name in the inspector window.
I had this same issue, and as far as I have found out, if you are using storyboard you can just enter your custom class name in the properties of your table cell in the iOS designer and it should work. Calling RegisterClassForCellReuse() is what causes the null sub views issue. Once I removed that method call it seemed to work

iOS : Expandable list view not drawing newly added control

I am trying to create an expandable list view in iOS using Xamarin with MvvmCross.
The scenario is that I have a listview, and when a row in the listview is selected, it expands (animates) to reveal a collection view, loaded in via lazyloading.
Here is the code I have so far:
Adapter :
public class MercatoAnimatedExpandableTableSource : MvxTableViewSource
{
private readonly string _key;
private readonly List<object> items;
private Dictionary<object, bool> expandableState = new Dictionary<object, bool>();
public MercatoAnimatedExpandableTableSource(UITableView tableView, IEnumerable<object> items, UINib nib, string key)
: base(tableView)
{
_key = key;
this.items = items.ToList();
this.items.ForEach(x => expandableState[x] = true);
tableView.RegisterNibForCellReuse(nib, key);
}
protected override UITableViewCell GetOrCreateCellFor(UITableView tableView, NSIndexPath indexPath, object item)
{
var cell = tableView.DequeueReusableCell(_key);
cell.Frame = new RectangleF(cell.Frame.X, cell.Frame.Y, cell.Frame.Width, GetHeightForRow(tableView, indexPath));
return cell;
}
public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{
base.RowSelected(tableView, indexPath);
var isExpanded = expandableState[items[indexPath.Row]];
expandableState[items[indexPath.Row]] = !isExpanded;
var row = this.GetCell(tableView, indexPath);
(row as FamilysubgroupViewCell).ExpandCells();
tableView.ReloadRows(new[] { indexPath }, UITableViewRowAnimation.Automatic);
}
public override float GetHeightForRow(UITableView tableView, NSIndexPath indexPath)
{
var isExpanded = expandableState[items[indexPath.Row]];
if (isExpanded)
{
return 25;
}
return 400;
}
protected override object GetItemAt(NSIndexPath indexPath)
{
return items[indexPath.Row];
}
}
Cell :
public FamilySubgroupViewCell (IntPtr handle) : base (handle)
{
this.DelayBind(() =>
{
this.FamilyCollectionViewContainer.BackgroundColor = UIColor.Blue;
var bindingset = this.CreateBindingSet<FamilySubgroupViewCell, FamilySubTypeViewModel>();
bindingset.Bind(this.FamilyGroupTitleLabel).To(x => x.FamilySubType.FamilySubGroupDescription);
bindingset.Bind(this.FamilyGroupDescLabel).To(x => x.FamilySubType.FamilySubGroupDetail);
bindingset.Apply();
});
}
public void ExpandCells(Action onUpdate)
{
var context = this.DataContext as FamilySubTypeViewModel;
context.PopulateAndRun(() =>
{
Debug.WriteLine("Populating Models complete : now showing cells");
var collection = new FamilySubGroupModelsCollection();
collection.ViewModel = context;
Debug.WriteLine("FamilySubGroupCell : Showing {0} Items", context.FamilyModels.Count);
this.FamilyCollectionViewContainer.Add(collection.View);
if (onUpdate != null) onUpdate();
});
}
Issue
So when a cell is selected, it flips the 'isExpanded' property which gives it a new height - that works fine. The cell is then sent a message to 'expand' the cell, which in turn loads a new collection and adds it to the view (via View outlet).
However, the collection is never re-rendered on the view. It populates fine, adds to the view but it is never actually visible on the newly expanded cell. If i load it in when the cell is initially created in the DelayBind() method (ie - no lazy loading) then it works ok.
Have tried things like redrawing the cell
this.Draw(this.Bounds);
after the collection was added to the view, but to no avail.
What am I doing wrong?
This block of code feels like it might be confusing your display:
var row = this.GetCell(tableView, indexPath);
(row as FamilysubgroupViewCell).ExpandCells();
tableView.ReloadRows(new[] { indexPath }, UITableViewRowAnimation.Automatic);
What this does is:
get the current cell and tell it to expand
then ask the table to reload the row (at which point a different cell instance might be used).
I'd also worry that the line:
var context = this.DataContext as FamilySubTypeViewModel;
would link your cells sub-display to the current viewmodel - and this wouldn't update if the cell were reused (given a new viewmodel).
One quick (and only slightly dirty) way around both of these problems would be to change the override for GetOrCreateCellFor to something like:
protected override UITableViewCell GetOrCreateCellFor(UITableView tableView, NSIndexPath indexPath, object item)
{
var cell = tableView.DequeueReusableCell(_key);
cell.Frame = new RectangleF(cell.Frame.X, cell.Frame.Y, cell.Frame.Width, GetHeightForRow(tableView, indexPath));
((MyCellType)cell).ShowExpandedState(expandableState[items[indexPath.Row]]);
return cell;
}
Where ShowExpandedState(bool) would be capable of resetting the cell's expanded area after reuse.
Other options:
Instead of this, you could simply return a different cell type for expanded versus non-expanded rows?
Or you could put the expanded/non-expanded information in the Cell's ViewModel so that it can data-bind to that expanded state and update it's own layout? (you'd still need some mechanism to tell the table about the size change though)
On:
this.Draw(this.Bounds);
I'm not too surprised this didn't work - generally you have to encourage the system to redraw views - using things like SetNeedsDisplay (and sometimes after child changes SetNeedsLayout too)

How to add to static table in Xamarin iOS(Monotouch)

I am trying to add cells to a static table that I created using storyboard. I want to be able to add cell to one section of it dynamically from code, I implemented a custom UITableViewSource but it keeps overwriting my static cells. How should I implement GetCell() method so it will work with static cells too? Looks like in ObjectiveC it can be done by following:
return [super tableView:tableView numberOfRowsInSection:section]
But how to implement it in Xamarin iOS?
I was able to get it to work using this code:
public partial class StaticTableController : UITableViewController
{
public StaticTableController (IntPtr handle) : base (handle)
{
}
[Export("tableView:cellForRowAtIndexPath:")]
public UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
var selector = new Selector("tableView:cellForRowAtIndexPath:");
//WTF?
var cell = new UITableViewCell(Messaging.IntPtr_objc_msgSendSuper_IntPtr_IntPtr(SuperHandle, selector.Handle, tableView.Handle, indexPath.Handle));
if (indexPath.Row == 2)
{
cell.TextLabel.Text = "Dynamic thingy";
}
return cell;
}
}
Looks a bit weird, you might be better off using a dynamic table instead. One thing I tried that doesn't work is modifying the number of cells in a section -- this just seems to directly crash with the Obj-C version of an "index out of range" inside UITableView.

Resources