ForceUpdateSize ListView issue on iOS - ios

I have a custom ListView using custom ViewCells with radio buttons. On clicking of each of the radio buttons the ListView dynamically resizes its height to hide/show a comment box.
On using ForceUpdateSize in iOS platform, the ListView performance slows down rapidly on clicking the radio buttons. The app eventually hangs and stops responding.
Is there an alternative solution to use instead of ForceUpdateSize to dynamically expand the ListView row at runtime?

Define a ViewCell Size Change event wherever you need to change your ViewCell size
public static event Action ViewCellSizeChangedEvent;
In your case, it should be triggered by your radio button. Call it like this:
ViewCellSizeChangedEvent?.Invoke();
It will then use the ListView renderer to update the iOS TableView.
public class CustomListViewRenderer : ListViewRenderer
{
public CustomListViewRenderer()
{
WhatEverContentView.ViewCellSizeChangedEvent += UpdateTableView;
}
private void UpdateTableView()
{
var tv = Control as UITableView;
if (tv == null) return;
tv.BeginUpdates();
tv.EndUpdates();
}
}
It should solve your performance problem while keep using your Xaml instead of creating a custom ViewCell which is not needed.

My solution is: Try to use custom renderer. When the button clicked, I use tableView.ReloadRows() to dynamically change the size of a cell.
Firstly, define a bool list whose items equal to Rows you want to show in the Source. I initialize its items with false at first time.
List<bool> isExpanded = new List<bool>();
public MyListViewSource(MyListView view)
{
//It depends on how many rows you want to show.
for (int i=0; i<10; i++)
{
isExpanded.Add(false);
}
}
Secondly, Construct the GetCell event(I just put a UISwitch in my Cell for testing) like:
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
MyListViewCell cell = tableView.DequeueReusableCell("Cell") as MyListViewCell;
if (cell == null)
{
cell = new MyListViewCell(new NSString("Cell"));
//This event is constructed in my Cell, when the switch's value changed it will be fired.
cell.RefreshEvent += (refreshCell, isOn) =>
{
NSIndexPath index = tableView.IndexPathForCell(refreshCell);
isExpanded[index.Row] = isOn;
tableView.ReloadRows(new NSIndexPath[] { index }, UITableViewRowAnimation.Automatic);
};
}
cell.switchBtn.On = isExpanded[indexPath.Row];
return cell;
}
At last, we can override the GetHeightForRow event. Set a large or small value depending on the item in the isExpanded:
public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath)
{
if (isExpanded[indexPath.Row])
{
return 80;
}
return 40;
}
Here is some part of my cell for you referring to:
//When switch's value changed, this event will be called
public delegate void RefreshHanle(MyListViewCell cell, bool isOn);
public event RefreshHanle RefreshEvent;
switchBtn.AddTarget((sender, args) =>
{
UISwitch mySwitch = sender as UISwitch;
RefreshEvent(this, mySwitch.On);
}, UIControlEvent.ValueChanged);

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.

uitableview adds new values to stack any way to flush or release?

http://i.stack.imgur.com/W1KGb.png this is my tablecells,
public override void RowSelected (UITableView tableView, NSIndexPath indexPath)
{
long selected = tableItemsX [indexPath.Row].Id;
up.btn2.Clicked += delegate {
Console.Write (selected);
};
if I choose multiple cells, after I click btn2; gets me all selected row Id, but I need only last clicked row value.
I get different values with indxpath.row.id, but if I click btn gives me all rows. . .
Every time you select a row, you are adding a new handler to btn2. You should only assign a handler once.
// put this in your ViewDidLoad
up.btn2.Clicked += delegate {
Console.Write (selected);
};
// declare selected as a class level variable
long selected;
public override void RowSelected (UITableView tableView, NSIndexPath indexPath)
{
selected = tableItemsX [indexPath.Row].Id;
}

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)

Bind MvxBindableTableViewCell's Accessory to boolean

I'm stuck on how to bind the MvxBindableTableViewCell's accessory to a boolean.
I have the table's ItemsSource bound to a list in my ViewModel, showing a nice list of clickable items.
However I want the cell's accessory (UITableViewCellAccessory.Checkmark) to show only when this object is flagged. By flagged I mean a boolean in the model is set to true.
Does anyone know how to bind the cell's accessory?
EDIT:
I can show the Accessory depending on the model's boolean, but it's not bound.
protected override UITableViewCell GetOrCreateCellFor(UITableView tableView, NSIndexPath indexPath, object item)
{
UITableViewCell cell = tableView.DequeueReusableCell(CellIdentifier);
if (cell == null)
cell = new PlotsTableViewCell(UITableViewCellStyle.Subtitle, CellIdentifier);
Plot p = (Plot)item;
if (p.Done)
cell.Accessory = UITableViewCellAccessory.Checkmark;
return cell;
}
I think you can probably do this in your PlotsTableViewCell.
If you declare a custom cell, then you can bind within that cell at runtime.
You can see an example of this in: https://github.com/slodge/MvvmCross/blob/vnext/Sample%20-%20CirriousConference/Cirrious.Conference.UI.Touch/Cells/SessionCell2.cs which is used in the sessions display:
You can see that the cell provides public properties like:
public string RoomText
{
get { return Label2.Text; }
set { if (Label2 != null) Label2.Text = value; }
}
and then provides binding text like:
'RoomText':{'Path':'Item.Session','Converter':'SessionSmallDetails','ConverterParameter':'SmallDetailsFormat'},
For binding an Accessory to a Bool, you should be able to do something like:
public bool IsDone
{
get { return Accessory == UITableViewCellAccessory.Checkmark; }
set
{
if (value)
{
Accessory = UITableViewCellAccessory.Checkmark;
}
else
{
Accessory = UITableViewCellAccessory.None;
}
}
}
with text:
'IsDone':{'Path':'Done'},
As an advanced technique, you could also use a custom image inside a custom drawn button instead of an Accessory in your cell. To see how to do this, take a look at how the IsFavorite property is bound in that conference sample - see the two way custom bindings in https://github.com/slodge/MvvmCross/tree/vnext/Sample%20-%20CirriousConference/Cirrious.Conference.UI.Touch/Bindings

TableView.DeselectRow not triggering Deselected event for custom Element

I may be missing something here, however, I can't get the Deselect event to trigger on my custom Element class when I manually deselect a row. I am overriding the cell to do some custom drawing and I want to change the colour of the font when the cell is selected/deselected.
A working example of the actual issue is detailed below.
public class TestViewController : DialogViewController
{
public TestViewController () : base(UITableViewStyle.Plain, null, true)
{
Root = new RootElement(null);
var section = new Section();
for (int i = 0; i <= 10; i++)
{
var element = new MyCustomElement();
element.Tapped += (dvc, tableView, indexPath) => {
var sheet = new UIActionSheet("", null, "Cancel", null, null);
sheet.Dismissed += delegate(object sender, UIButtonEventArgs e) {
tableView.DeselectRow(indexPath, false);
};
sheet.ShowInView(View);
};
section.Add (element);
}
Root.Add (section);
}
}
public class MyCustomElement : Element, IElementSizing {
static NSString mKey = new NSString ("MyCustomElement");
public MyCustomElement () : base ("")
{
}
public MyCustomElement (Action<DialogViewController,UITableView,NSIndexPath> tapped) : base ("")
{
Tapped += tapped;
}
public override UITableViewCell GetCell (UITableView tv)
{
var cell = tv.DequeueReusableCell (mKey);
if (cell == null)
cell = new UITableViewCell (UITableViewCellStyle.Default, mKey);
return cell;
}
public float GetHeight (UITableView tableView, NSIndexPath indexPath)
{
return 65;
}
public event Action<DialogViewController, UITableView, NSIndexPath> Tapped;
public override void Selected (DialogViewController dvc, UITableView tableView, NSIndexPath path)
{
Console.WriteLine("Selected!");
if (Tapped != null)
Tapped (dvc, tableView, path);
}
public override void Deselected (DialogViewController dvc, UITableView tableView, NSIndexPath path)
{
// does not trigger when deselect manually invoked
Console.WriteLine("Deselected!");
base.Deselected (dvc, tableView, path);
}
}
I have also tried overriding the Deselected event on the DialogViewController itself and even creating a custom Source and overriding the RowDeselected event in there but it's still not triggered. The only way I get it to trigger is if I remove the Tapped handler and select a different cell.
To get around the issue what I am doing at the moment is manually forcing the element to update itself after I call DeselectRow, however, I would like to know why it's not triggering.
you not receive Deselected event because deselectRowAtIndexPath:animated: method does not cause the delegate to receive a tableView:didDeselectRowAtIndexPath: message, nor will it send UITableViewSelectionDidChangeNotification notifications to observers and calling this method does not cause any scrolling to the deselected row.

Resources