I have read many tutorial out there for UICollectionView for both iOS and Xamarin.iOS
Everybody has shown same sample tutorial with one image inside UICollectionViewCell
Desired output
UICollectionViewCell with custom design
What I have done
This is what I have achieved so far.. please ignore the bad design :P
Problem
When I Click on Book button, it triggers the event multiple times. The weird thing is that its not consistent, sometimes it will trigger only once some time twice or some time three times or whatever...
Code Snippet
List<MeetingRoom> data = new List<MeetingRoom>();
async public override void ViewDidLoad()
{
base.ViewDidLoad();
data = await DashboardServices.getMeetingRooms(DateTime.Now);
MeetingRoomCollection.WeakDataSource = this;
}
public nint GetItemsCount(UICollectionView collectionView, nint section)
{
var count = data.Count;
return count;
}
public UICollectionViewCell GetCell(UICollectionView collectionView, NSIndexPath indexPath)
{
var cell = (MeetingRoomCell)collectionView.DequeueReusableCell("cell", indexPath);
cell.RoomName.Text = data[indexPath.Row].Room_Name;
cell.Status.Text = data[indexPath.Row].Availability;
cell.BookButton.Tag = indexPath.Row;
cell.BookButton.TouchUpInside += Cell_BookButton_TouchUpInside;
return cell;
}
void Cell_BookButton_TouchUpInside(object sender, EventArgs e)
{
var x = sender as UIButton;
Debug.WriteLine("index : {0}, status : {1}", (int)x.Tag, data[(int)x.Tag].Availability);
this.PerformSegue("segue_bookRoom", this);
}
Please suggest what needs to be done to fix this issue, any reference to available tutorial would be appreciated. It will be great if Xamarin.iOS hint is there..
One possible way to solve this issue is to unsubscribe from event before subscribing it, this way it will prevent multiple calls especially if this item is recycled in list.
cell.BookButton.TouchUpInside -= Cell_BookButton_TouchUpInside;
cell.BookButton.TouchUpInside += Cell_BookButton_TouchUpInside;
Related
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);
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.
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
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)
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.