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
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);
I'm working on an iOS app using Xamarin and MVVM Cross.
I have a table view from which one row can be selected at a time, which is indicated using the Checkmark accessory on the UITableViewCell. I'm essentially following the answer from this: ✔ Checkmark selected row in UITableViewCell
The problem is that Reloading the table data (or even reloading just the two rows that changed) seems to interrupt the animation of De-selecting the row. I would like it to fade out nicely. For example, on an iPad, the behavior in Settings -> Notes -> Sort Notes By is exactly what I want. Touching a row highlights it, letting go moves the checkmark, and then the highlight fades out.
After messing with this for several hours, the only solution I found was to Reload the rows to update the checkmark, then manually Re-select and De-select the row. This works fine, but feels like a big hack. Is there a cleaner way to do this?
Here's the relevant snippets:
public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{
SetCellCheckmark(tableView, indexPath);
base.RowSelected(tableView, indexPath); //This handles de-select via the MvxTableViewSource DeselectAutomatically property
}
private void SetCellCheckmark(UITableView tableView, NSIndexPath newSelection)
{
if (newSelection == null)
return;
var rowsToUpdate = new NSIndexPath[_checkedRow == null ? 1 : 2];
rowsToUpdate[0] = newSelection;
if (_checkedRow != null)
rowsToUpdate[1] = _checkedRow;
_checkedRow = newSelection;
tableView.ReloadRows(rowsToUpdate, UITableViewRowAnimation.None);
tableView.SelectRow(newSelection, false, UITableViewScrollPosition.None); //This feels like a hack
}
private UITableViewCell GetOrCreateCellFor(UITableView tableView, NSIndexPath indexPath, object item)
{
var cell = TableView.DequeueReusableCell(MyCellType, indexPath);
if (cell == null)
return null;
if (indexPath == _checkedRow)
{
cell.Accessory = UITableViewCellAccessory.Checkmark;
}
else
{
cell.Accessory = UITableViewCellAccessory.None;
}
return cell;
}
If you have an answer in Objective-C, I could probably translate it into Xamarinland.
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'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.