Hi I'm having an issue trying to bind MvxTableViewCell accessory Checkmark to a local property. I've tried following an example found at Bind MvxBindableTableViewCell's Accessory to boolean
I'm quite new to IOS and even newer to MvvmCross so I apologize if I've made any silly mistakes
public partial class TaxaListCellView : MvxTableViewCell
{
public static readonly UINib Nib = UINib.FromName ("TaxaListCellView", NSBundle.MainBundle);
public static readonly NSString Key = new NSString ("TaxaListCellView");
public TaxaListCellView (IntPtr handle) : base (handle)
{
Accessory = UITableViewCellAccessory.Checkmark;
SelectionStyle = UITableViewCellSelectionStyle.None;
this.DelayBind (() => {
var set = this.CreateBindingSet<TaxaListCellView, TaxonViewModel>();
set.Bind(lblSelectedTaxon).To(s => s.Name);
//I've been playing around with both ways below, and a few different
//variants without any success
//set.Bind("IsChecked").To(s => s.IsSelected).TwoWay();
//set.Bind(#"'IsChecked':{'Path':'IsSelected'");
set.Apply();
});
}
public bool IsChecked
{
get { return Accessory == UITableViewCellAccessory.Checkmark; }
set { Accessory = value ? UITableViewCellAccessory.Checkmark : UITableViewCellAccessory.None; }
}
public static TaxaListCellView Create ()
{
return (TaxaListCellView)Nib.Instantiate (null, null) [0];
}
public override void SetSelected (bool selected, bool animated)
{
Accessory = selected ? UITableViewCellAccessory.Checkmark : UITableViewCellAccessory.None;
base.SetSelected (selected, animated);
}
}
I'm not sure if there is anything wrong with my MvxTableViewController but here's the code
public class TaxaListView : MvxTableViewController
{
public TaxaListView()
{
Title = "Taxon List";
}
private UISearchBar _searchBar;
public override void ViewDidLoad()
{
base.ViewDidLoad();
_searchBar = new UISearchBar(new RectangleF(0,0,320, 44))
{
AutocorrectionType = UITextAutocorrectionType.Yes,
};
_searchBar.SearchButtonClicked += SearchBar_SearchButtonClicked;
_searchBar.TextChanged += SearchBarOnTextChanged;
var source = new MvxSimpleTableViewSource(TableView, TaxaListCellView.Key, TaxaListCellView.Key);
var set = this.CreateBindingSet<TaxaListView, TaxaListViewModel> ();
set.Bind (source).To (vm => vm.Taxa);
set.Bind (source)
.For (s => s.SelectionChangedCommand)
.To (vm => vm.ItemSelectedCommand);
set.Apply ();
TableView.RowHeight = 50;
TableView.Source = source;
TableView.AllowsSelection = true;
TableView.AllowsSelectionDuringEditing = true;
TableView.TableHeaderView = _searchBar;
TableView.ReloadData();
}
private void SearchBarOnTextChanged(object sender, UISearchBarTextChangedEventArgs uiSearchBarTextChangedEventArgs)
{
if(string.IsNullOrWhiteSpace(_searchBar.Text))
{
((TaxaListViewModel) ViewModel).SearchTaxaByText(string.Empty);
}
}
void SearchBar_SearchButtonClicked(object sender, System.EventArgs e)
{
((TaxaListViewModel)ViewModel).SearchTaxaByText(_searchBar.Text);
}
}
When I Select an item from the list first
When i start searching, Or happens even if i go back into the list of items
As Stuart alluded too, I needed to tell the ViewModel the value had changed.
I removed the SetSelelted method as this was causing problems when the cell was loading
public partial class TaxaListCellView : MvxTableViewCell
{
public static readonly UINib Nib = UINib.FromName ("TaxaListCellView", NSBundle.MainBundle);
public static readonly NSString Key = new NSString ("TaxaListCellView");
private const string BindingText = "Name Name; IsChecked IsSelected";
public TaxaListCellView() : base(BindingText)
{
Accessory = UITableViewCellAccessory.Checkmark;
SelectionStyle = UITableViewCellSelectionStyle.None;
}
public TaxaListCellView (IntPtr handle) : base (BindingText,handle)
{
Accessory = UITableViewCellAccessory.Checkmark;
SelectionStyle = UITableViewCellSelectionStyle.None;
}
public string Name
{
get { return lblSelectedTaxon.Text; }
set { lblSelectedTaxon.Text = value; }
}
public bool IsChecked
{
get { return Accessory == UITableViewCellAccessory.Checkmark; }
set { Accessory = value ? UITableViewCellAccessory.Checkmark : UITableViewCellAccessory.None; }
}
public static TaxaListCellView Create ()
{
return (TaxaListCellView)Nib.Instantiate (null, null) [0];
}
}
In My TaxaListView class
public override void ViewDidLoad()
{
base.ViewDidLoad();
_searchBar = new UISearchBar(new RectangleF(0,0,320, 44))
{
AutocorrectionType = UITextAutocorrectionType.Yes,
};
_searchBar.SearchButtonClicked += SearchBar_SearchButtonClicked;
_searchBar.TextChanged += SearchBarOnTextChanged;
var source = new MvxSimpleTableViewSource(TableView, TaxaListCellView.Key, TaxaListCellView.Key);
var set = this.CreateBindingSet<TaxaListView, TaxaListViewModel> ();
set.Bind (source).To (vm => vm.Taxa);
set.Bind (source)
.For (s => s.SelectionChangedCommand)
.To (vm => vm.ItemSelectedCommand);
set.Apply ();
TableView.RowHeight = 50;
TableView.Source = source;
TableView.AllowsSelection = true;
TableView.AllowsSelectionDuringEditing = true;
TableView.TableHeaderView = _searchBar;
TableView.ReloadData();
}
I Bind the selectedChangedCommand and in my ViewModel class I raise the Property changed event
private MvxCommand<TaxonViewModel> _itemSelectedCommand;
public ICommand ItemSelectedCommand
{
get
{
_itemSelectedCommand = _itemSelectedCommand ?? new MvxCommand<TaxonViewModel>(DoSelectedItem);
return _itemSelectedCommand;
}
}
private void DoSelectedItem(TaxonViewModel item)
{
Taxa.First(r => r.TaxonId == item.TaxonId).IsSelected = true;
RaisePropertyChanged("Taxon");
}
Related
I am building a multi-column picker control, and I have some issues with the xamarin.ios renderer.
I have two columns in the picker. One for the month and another for the year. I have got the values binded to the picker control properly. However when I make a selection the app crashes.
The error is Specified cast is not valid.
Stacktrace is
at Xamarin.Forms.Platform.iOS.PickerRendererBase`1[TControl].OnEnded
(System.Object sender, System.EventArgs eventArgs) [0x0000b] in
<0648e2dffe9e4201b8c6e274ced6579f>:0 at
UIKit.UIControlEventProxy.Activated () [0x00004] in /Library/Frameworks/Xamarin.iOS.framework/Versions/12.16.0.5/src/Xamarin.iOS/UIKit/UIControl.cs:38
My ios Custom Renderer code is as follows:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using CoreGraphics;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
[assembly: ExportRenderer(typeof(YearMonthPicker), typeof(YearMonthPickerRenderer))]
namespace TestApp.iOS.Renderers
{
public class YearMonthPickerRenderer : PickerRenderer
{
private MonthYear _monthYear = new MonthYear();
private UILabel _monthLabel;
private UILabel _yearLabel;
public YearMonthPickerRenderer()
{
_monthYear.Months = new List<string>();
_monthYear.Years = new List<int>();
InitValues();
}
private void InitValues()
{
_monthYear.Months = new List<string>{
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"Sepetember",
"October",
"November",
"December"
};
}
protected override void OnElementChanged(ElementChangedEventArgs<Picker> e)
{
base.OnElementChanged(e);
if (e.NewElement != null)
{
var customPicker = e.NewElement as YearMonthPicker;
var startYear = customPicker.StartYear;
var endYear = customPicker.EndYear;
do
{
_monthYear.Years.Add(startYear);
startYear++;
} while (startYear <= endYear);
if (_monthLabel == null)
{
_monthLabel = new UILabel();
_monthLabel.Text = customPicker.MonthLabel;
_monthLabel.TextColor = customPicker.LabelColor.ToUIColor();
_monthLabel.Font = _monthLabel.Font.WithSize(14.0f);
}
if (_yearLabel == null)
{
_yearLabel = new UILabel();
_yearLabel.Text = customPicker.YearLabel;
_yearLabel.TextColor = customPicker.LabelColor.ToUIColor();
_yearLabel.Font = _yearLabel.Font.WithSize(14.0f);
}
if (Control is UITextField textField)
{
var pickerView = textField.InputView as UIPickerView;
var yearMonthModel = new YearMonthModel(textField, _monthYear);
var yearMonthDelegate = new YearMonthPickerDelegate(textField, _monthYear);
pickerView.Model = yearMonthModel;
pickerView.Delegate = yearMonthDelegate;
textField.BorderStyle = UITextBorderStyle.None;
textField.AddSubview(_monthLabel);
textField.AddSubview(_yearLabel);
}
}
var toolbar = new UIToolbar(new CGRect(0.0f, 0.0f, Control.Frame.Size.Width, 44.0f));
toolbar.Items = new[]
{
new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace),
new UIBarButtonItem("Done",
UIBarButtonItemStyle.Done,
delegate {
Control.ResignFirstResponder();
})
};
if (this.Control != null)
{
Control.InputAccessoryView = toolbar;
}
}
public override void LayoutSubviews()
{
base.LayoutSubviews();
_monthLabel.Frame = new CGRect(0, -10,
Control.Frame.Width / 2,
Control.Frame.Height - 5);
_yearLabel.Frame = new CGRect(Control.Frame.Width / 2, -10,
Control.Frame.Width / 2,
Control.Frame.Height - 5);
}
protected override void Dispose(bool disposing)
{
_monthLabel?.Dispose();
_yearLabel?.Dispose();
_monthLabel = null;
_yearLabel = null;
base.Dispose(disposing);
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
}
}
public class YearMonthModel : UIPickerViewModel
{
private UITextField _uITextField;
private string _selectedMonth = string.Empty;
private string _selectedYear = string.Empty;
private MonthYear _monthYear;
public YearMonthModel(UITextField uITextField, MonthYear monthYear)
{
_uITextField = uITextField;
_monthYear = monthYear;
}
public override nint GetComponentCount(UIPickerView pickerView)
{
return 2;
}
public override nint GetRowsInComponent(UIPickerView pickerView, nint component)
{
if (component == 0)
return _monthYear.Months.Count;
else
return _monthYear.Years.Count;
}
public override string GetTitle(UIPickerView pickerView, nint row, nint component)
{
if (component == 0)
return _monthYear.Months[(int)row];
else
return _monthYear.Years[(int)row].ToString();
}
public override void Selected(UIPickerView pickerView, nint row, nint component)
{
if (component == 0)
_selectedMonth = _monthYear.Months[(int)row];
if (component == 1)
_selectedYear = _monthYear.Years[(int)row].ToString();
_uITextField.Text = $"{_selectedMonth} {_selectedYear}";
}
public override nfloat GetComponentWidth(UIPickerView picker, nint component)
{
if (component == 0)
return 140f;
else
return 100f;
}
public override nfloat GetRowHeight(UIPickerView picker, nint component)
{
return 40f;
}
}
public class YearMonthPickerDelegate : UIPickerViewDelegate
{
private UITextField _uITextField;
private string _selectedMonth = string.Empty;
private string _selectedYear = string.Empty;
private MonthYear _monthYear;
public YearMonthPickerDelegate(UITextField uITextField, MonthYear monthYear)
{
_uITextField = uITextField;
_monthYear = monthYear;
}
public override string GetTitle(UIPickerView pickerView, nint row, nint component)
{
if (component == 0)
return _monthYear.Months[(int)row];
else
return _monthYear.Years[(int)row].ToString();
}
public override void Selected(UIPickerView pickerView, nint row, nint component)
{
if (component == 0)
_selectedMonth = _monthYear.Months[(int)row];
if (component == 1)
_selectedYear = _monthYear.Years[(int)row].ToString();
_uITextField.Text = $"{_selectedMonth} {_selectedYear}";
}
}
public class MonthYear
{
public List<string> Months { get; set; }
public List<int> Years { get; set; }
}
}
Edit 1
I am adding the YearMonthPicker class
public class YearMonthPicker : Picker
{
public YearMonthPicker()
{
}
public static readonly BindableProperty StartYearProperty =
BindableProperty.Create(nameof(StartYear), typeof(int), typeof(YearMonthPicker), 1900);
public int StartYear
{
get { return (int)GetValue(StartYearProperty); }
set { SetValue(StartYearProperty, value); }
}
public static readonly BindableProperty EndYearProperty =
BindableProperty.Create(nameof(EndYear), typeof(int), typeof(YearMonthPicker), 9999);
public int EndYear
{
get { return (int)GetValue(EndYearProperty); }
set { SetValue(EndYearProperty, value); }
}
public static readonly BindableProperty YearLabelProperty =
BindableProperty.Create(nameof(YearLabel), typeof(string), typeof(YearMonthPicker), string.Empty);
public string YearLabel
{
get { return (string)GetValue(YearLabelProperty); }
set { SetValue(YearLabelProperty, value); }
}
public static readonly BindableProperty MonthLabelProperty =
BindableProperty.Create(nameof(MonthLabel), typeof(string), typeof(YearMonthPicker), string.Empty);
public string MonthLabel
{
get { return (string)GetValue(MonthLabelProperty); }
set { SetValue(MonthLabelProperty, value); }
}
public static readonly BindableProperty LabelColorProperty =
BindableProperty.Create(nameof(LabelColor), typeof(Color), typeof(YearMonthPicker), default(Color));
public Color LabelColor
{
get { return (Color)GetValue(LabelColorProperty); }
set { SetValue(LabelColorProperty, value); }
}
}
I appreciate if someone can help me identify what causes the crash.
Finally, I found your problem is you did not assign the pickerView to the textField's inputView.
To assign the picker to the textField's inputView, use below code:
if (Control is UITextField textField)
{
//Change here
var pickerView = new UIPickerView();
Control.InputView = pickerView;
var yearMonthModel = new YearMonthModel(textField, _monthYear);
var yearMonthDelegate = new YearMonthPickerDelegate(textField, _monthYear);
pickerView.Model = yearMonthModel;
pickerView.Delegate = yearMonthDelegate;
textField.BorderStyle = UITextBorderStyle.None;
textField.AddSubview(_monthLabel);
textField.AddSubview(_yearLabel);
}
I have a TableView where the user can check multiple rows.
Now I have the problem that when I select e.g. the 5th element the 27th element in the list is automatically checked too, but I cant explain why.
I´m using the following Code for the TableViewSource base class:
public abstract class BaseChecklistTableViewSource : UITableViewSource
{
protected int checkCount;
public BaseChecklistTableViewSource()
{
}
public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{
var cell = tableView.CellAt(indexPath);
if (indexPath.Row >= 0)
{
if (cell.Accessory == UITableViewCellAccessory.None)
{
cell.Accessory = UITableViewCellAccessory.Checkmark;
checkCount++;
}
else if (cell.Accessory == UITableViewCellAccessory.Checkmark)
{
cell.Accessory = UITableViewCellAccessory.None;
checkCount--;
}
CheckCheckCount();
}
}
protected void CheckCheckCount()
{
if (checkCount > 0)
{
EnableDoneButton();
}
else if (checkCount == 0)
{
DisableDoneButton();
}
}
protected abstract void EnableDoneButton();
protected abstract void DisableDoneButton();
}
This is the code of the concrete class of "BaseChecklistTableViewSource":
public partial class CheckunitTableViewSource : BaseChecklistTableViewSource
{
ListCheckunitController controller;
private IEnumerable<Checkunit> existingCheckunits;
public CheckunitTableViewSource(ListCheckunitController controller)
{
this.controller = controller;
this.existingCheckunits = new List<Checkunit>();
}
public CheckunitTableViewSource(ListCheckunitController controller, IEnumerable<Checkunit> existingCheckunits) : this(controller)
{
if (existingCheckunits == null || !existingCheckunits.Any())
{
throw new ArgumentNullException(nameof(existingCheckunits));
}
this.existingCheckunits = existingCheckunits;
checkCount = this.existingCheckunits.Count();
CheckCheckCount();
}
// Returns the number of rows in each section of the table
public override nint RowsInSection(UITableView tableview, nint section)
{
if (controller.Checkunits == null)
{
return 0;
}
return controller.Checkunits.Count();
}
//
// Returns a table cell for the row indicated by row property of the NSIndexPath
// This method is called multiple times to populate each row of the table.
// The method automatically uses cells that have scrolled off the screen or creates new ones as necessary.
//
public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
{
var cell = tableView.DequeueReusableCell("checkunitViewCell") ?? new UITableViewCell();
int row = indexPath.Row;
var element = controller.Checkunits.ElementAt(row);
if(existingCheckunits.FirstOrDefault(e => e.Id == element.Id) != null)
{
cell.Accessory = UITableViewCellAccessory.Checkmark;
}
//if(cell.Accessory == UITableViewCellAccessory.Checkmark)
//{
// checkCount++;
//}
cell.TextLabel.Text = $"{element.Order}. {element.Designation}";
cell.Tag = element.Id;
return cell;
}
protected override void EnableDoneButton()
{
controller.EnableDoneButton();
}
protected override void DisableDoneButton()
{
controller.DisableDoneButton();
}
}
I tried to use a breakpoint in the RowSelected method, but the breakpoint will only be hit once!
This is the Code for the TableViewController:
public partial class ListCheckunitController : BaseTableViewController
{
private ImmobilePerpetration masterModel;
private LoadingOverlay loadingOverlay;
public IEnumerable<Checkunit> Checkunits { get; set; }
public IEnumerable<Checkunit> existingCheckunits;
public IEnumerable<CheckpointAssessment> existingAssessments;
public static readonly NSString callHistoryCellId = new NSString("checkunitViewCell");
UIRefreshControl refreshControl;
private UIBarButtonItem doneButton;
public ListCheckunitController(IntPtr handle) : base(handle)
{
existingCheckunits = new List<Checkunit>();
existingAssessments = new List<CheckpointAssessment>();
}
public void Initialize(ImmobilePerpetration masterModel)
{
if (masterModel == null)
{
throw new ArgumentNullException(nameof(masterModel));
}
this.masterModel = masterModel;
}
public void Initialize(ImmobilePerpetration masterModel, IEnumerable<CheckpointAssessment> existingAssessments, IEnumerable<Checkunit> existingCheckunits)
{
Initialize(masterModel);
if(existingAssessments == null || !existingAssessments.Any())
{
throw new ArgumentNullException(nameof(existingAssessments));
}
if (existingCheckunits == null || !existingCheckunits.Any())
{
throw new ArgumentNullException(nameof(existingCheckunits));
}
this.existingAssessments = existingAssessments;
this.existingCheckunits = existingCheckunits;
}
async Task RefreshAsync(bool onlineSync)
{
InvokeOnMainThread(() => refreshControl.BeginRefreshing());
try
{
var syncCtrl = new SynchronizationControllerAsync();
Checkunits = await syncCtrl.SynchronizeCheckUnitAsync(onlineSync).ConfigureAwait(false);
}
catch (Exception e)
{
InvokeOnMainThread(() => { AlertHelper.ShowError(e.Message, this);});
}
InvokeOnMainThread(() =>
{
ListCheckunitTable.ReloadData();
refreshControl.EndRefreshing();
});
}
void AddRefreshControl()
{
refreshControl = new UIRefreshControl();
refreshControl.ValueChanged += async (sender, e) =>
{
await RefreshAsync(true).ConfigureAwait(false);
};
}
public override async void ViewDidLoad()
{
var bounds = UIScreen.MainScreen.Bounds;
loadingOverlay = new LoadingOverlay(bounds);
View.Add(loadingOverlay);
AddRefreshControl();
await RefreshAsync(false).ConfigureAwait(false);
InvokeOnMainThread(() =>
{
doneButton = new UIBarButtonItem(UIBarButtonSystemItem.Done, (s, e) =>
{
PerformSegue("ListCheckpointsSegue", this);
});
doneButton.Enabled = false;
CheckunitTableViewSource source = null;
if(existingCheckunits.Any())
{
source = new CheckunitTableViewSource(this, existingCheckunits);
}
else
{
source = new CheckunitTableViewSource(this);
}
ListCheckunitTable.Source = source;
ListCheckunitTable.Add(refreshControl);
loadingOverlay.Hide();
this.SetToolbarItems(new UIBarButtonItem[] {
new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace) { Width = 50 }
, doneButton
}, false);
this.NavigationController.ToolbarHidden = false;
});
}
public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
{
if (segue.Identifier == "ListCheckpointsSegue")
{
var controller = segue.DestinationViewController as ListCheckpointController;
IList<Checkunit> markedCategories = new List<Checkunit>();
for (int i = 0; i < ListCheckunitTable.NumberOfRowsInSection(0); i++)
{
var cell = ListCheckunitTable.CellAt(NSIndexPath.FromItemSection(i, 0));
if(cell != null)
{
if(cell.Accessory == UITableViewCellAccessory.Checkmark)
{
var originalObject = Checkunits.Where(e => e.Id == cell.Tag).SingleOrDefault();
if(originalObject != null)
{
markedCategories.Add(originalObject);
}
else
{
//TODO: Handle error case!
}
}
}
}
if (markedCategories.Any())
{
if (controller != null)
{
if(existingAssessments.Any() && existingCheckunits.Any())
{
controller.Initialize(masterModel, markedCategories, existingAssessments, existingCheckunits);
}
else
{
controller.Initialize(masterModel, markedCategories);
}
}
}
else
{
//TODO: Print out message, that there are no marked elements!
}
}
}
public void DisableDoneButton()
{
doneButton.Enabled = false;
}
public void EnableDoneButton()
{
doneButton.Enabled = true;
}
}
The used base class only handles a common sidebar view nothing more.
Does anyone have an idea what is going wrong here?
Well, I ended up in creating a bool Array that stores the checked elements.
I tried a demo app from Github from Xamarin and I had the exact same problem.
This leads me to the conclusion that this must be a Xamarin Bug!
I am writing a Xamarin.iOS application with MvvmCross. I am trying to make a table, I can see the items being binded into the source, but I never see any cells being created. The function GetOrCreateCellFor never gets called. Here is my code:
public class ContactsManager
{
ContactsView _contactsView;
public ContactsManager()
{
_contactsView = new ContactsView();
Source = new FriendTableViewSource(_contactsView.FriendsTableView);
_contactsView.FriendsTableView.Source = Source;
}
public FriendTableViewSource Source { get; set; }
}
public class FriendTableViewSource : MvxTableViewSource
{
private readonly List<SeeMyFriendsItemViewModel> _content = new List<SeeMyFriendsItemViewModel>();
private readonly UITableView _tableView;
public FriendTableViewSource(UITableView t) : base(t)
{
_tableView = t;
t.RegisterNibForCellReuse(UINib.FromName(FriendCell.Key, NSBundle.MainBundle), FriendCell.Key);
}
private void Init(IEnumerable<SeeMyFriendsItemViewModel> items)
{
_content.Clear();
_content.AddRange(items);
}
public override System.Collections.IEnumerable ItemsSource
{
get
{
return base.ItemsSource;
}
set
{
// I put a break point here to check if I'm getting the items, and it is, so the binding is fine...
if (value != null)
Init(value.Cast<SeeMyFriendsItemViewModel>());
base.ItemsSource = value;
_tableView.ReloadData();
}
}
public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath)
{
return 60;
}
protected override UITableViewCell GetOrCreateCellFor(UITableView tableView, NSIndexPath indexPath, object item)
{
// This function never gets called!
return TableView.DequeueReusableCell(FriendCell.Key, indexPath);
}
}
[Register("FriendCell")]
public class FriendCell : MvxTableViewCell
{
public static readonly NSString Key = new NSString("FriendCell");
public static readonly UINib Nib;
static FriendCell()
{
Nib = UINib.FromName("FriendCell", NSBundle.MainBundle);
}
protected FriendCell(IntPtr handle) : base(handle)
{
BackgroundColor = UIColor.Red;
}
}
EDIT
This is what a working version of your source should look like. What's also interesting is that GetOrCreateCellFor won't get called if the table is not added to your view.
public class FriendTableViewSource : MvxTableViewSource
{
private readonly List<SeeMyFriendsItemViewModel> _content = new List<SeeMyFriendsItemViewModel>();
private MvxNotifyCollectionChangedEventSubscription _subscription;
public FriendTableViewSource(UITableView t) : base(t)
{
t.RegisterClassForCellReuse(typeof(FriendCell), FriendCell.Key);
}
private void Init(IEnumerable<SeeMyFriendsItemViewModel> items)
{
_content.Clear();
_content.AddRange(items);
}
public override System.Collections.IEnumerable ItemsSource
{
get
{
return base.ItemsSource;
}
set
{
if (value != null)
{
Init(value.Cast<SeeMyFriendsItemViewModel>());
var collectionChanged = value as System.Collections.Specialized.INotifyCollectionChanged;
if (collectionChanged != null)
{
_subscription = collectionChanged.WeakSubscribe(CollectionChangedOnCollectionChanged);
}
}
base.ItemsSource = value;
ReloadTableData();
}
}
protected override void CollectionChangedOnCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs args)
{
if (args.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
foreach (var item in args.NewItems)
{
var chatItem = item as SeeMyFriendsItemViewModel;
_content.Add(chatItem);
}
}
Init(ItemsSource.Cast<SeeMyFriendsItemViewModel>());
base.CollectionChangedOnCollectionChanged(sender, args);
InvokeOnMainThread(() => {
ReloadTableData();
TableView.SetContentOffset(new CGPoint(0, TableView.ContentSize.Height - TableView.Frame.Size.Height), true);
});
}
public override nfloat GetHeightForRow(UITableView tableView, NSIndexPath indexPath)
{
return 60;
}
public override nint RowsInSection(UITableView tableview, nint section)
{
return _content.Count();
}
public override nint NumberOfSections(UITableView tableView)
{
return 1;
}
protected override object GetItemAt(NSIndexPath indexPath)
{
if (indexPath.Row < _content.Count)
return _content[indexPath.Row];
return null;
}
protected override UITableViewCell GetOrCreateCellFor(UITableView tableView, NSIndexPath indexPath, object item)
{
return TableView.DequeueReusableCell(FriendCell.Key, indexPath);
}
}
Override RowsInSection in FriendTableViewSource .
Since tableview needs row count and row height to decide its frame, if height = 0 or count = 0, GetOrCreateCellFor will never be called.
I have a problem with refreshing a UITableView correctly, at least the first time of ViewWillAppear and sometimes (rarely) might even hang the app. When I navigate away from the UIViewController with changing tab item and navigate back, everything starts working perfectly for the entire lifecycle..
Having a ReactiveTableViewSource like below.
public class ATableViewSource : ReactiveTableViewSourceBase<IAViewModel>
{
WeakReference<AListViewController> _weakContainer;
Lazy<AListViewController> _lazyContainerViewController;
AListViewController Container => _lazyContainerViewController.Value;
public ATableViewSource(AListViewController container,
UITableView tableView,
IReactiveNotifyCollectionChanged<IAViewModel> collection)
: base(tableView, collection,
ATableViewCell.Key,
ATableViewCell.Height,
ATableViewCell.Height)
{
_weakContainer = new WeakReference<AListViewController>(container);
tableView.RegisterNibForCellReuse(ATableViewCell.Nib, ATableViewCell.Key);
_lazyContainerViewController = new Lazy<AListViewController>(() =>
{
AListViewController _container;
_weakContainer.TryGetTarget(out _container);
return _container;
});
}
public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{
base.RowSelected(tableView, indexPath);
tableView.DeselectRow(indexPath, false);
var item = ItemAt(indexPath) as IAViewModel;
if (item.IsNotNull())
{
AViewController viewController = new AViewController(item);
Container.NavigationController.PushViewController(viewController, true);
}
}
}
In AListViewController.ViewDidLoad I have this setup.
ATableViewSource _viewSource;
public override void ViewDidLoad()
{
base.ViewDidLoad();
TableView.RowHeight = UITableView.AutomaticDimension;
TableView.EstimatedRowHeight = 350.0f;
TableView.ContentInset = new UIEdgeInsets(8.0f, 0.0f, 8.0f, 0.0f);
_viewSource = new ATableViewSource(this, TableView, ViewModel.TheReactiveList);
TableView.Source = _viewSource;
}
And in ViewWillAppear I always refresh the data (ViewModel.TheReactiveList).
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
ViewModel.RefreshTheReactiveList();
}
The AViewModel setup.
public class TheListViewModel : SchedulersViewModelBase, ITheListViewModel
{
public ReactiveList<IAViewModel> TheReactiveList { get; } = new ReactiveList<IAViewModel> { ChangeTrackingEnabled = true };
protected readonly IDataService DataService;
public TheListViewModel(IScheduler mainScheduler,
IScheduler taskPoolScheduler,
IDataService dataService)
: base(mainScheduler, taskPoolScheduler)
{
DataService = dataService;
}
public void RefreshTheReactiveList()
{
DataService.RefreshData()
.SubscribeOn(MainScheduler)
.ObserveOn(MainScheduler)
.Subscribe(ClearDataAndAddRange,
ex => AppObservables.Errors.OnNext(ex))
.DisposeWith(Disposables);
}
void ClearDataAndAddRange(IEnumerable<IAViewModel> data)
{
using (TheReactiveList.SuppressChangeNotifications())
{
TheReactiveList.Clear();
TheReactiveList.AddRange(data);
}
}
}
I have to mention in this use case I have a parent UIViewController with two child controllers each with the same setup (UITableView, ReactiveTableViewSource, ViewModel.ReactiveList) and Hidden state is controlled for their UIView containers, but I noticed similar effects to UIViewController with one UITableView taking up to 3 seconds to show the results in the UITableView.
For your reference I am posting the ReactiveTableViewSourceBase<TViewModel> below, I have found this long time ago on the internet, so it might be suspicious. Changing the base class to a ReactiveTableViewSource<TViewModel> doesn't really make any difference though.
public abstract class ReactiveTableViewSourceBase<TViewModel> : ReactiveTableViewSource<TViewModel>, IInformsEnd
{
private readonly Subject<Unit> _requestMoreSubject = new Subject<Unit>();
private readonly Subject<CGPoint> _scrollSubject = new Subject<CGPoint>();
public IObservable<CGPoint> DidScroll
{
get { return _scrollSubject.AsObservable(); }
}
public IObservable<Unit> RequestMore
{
get { return _requestMoreSubject; }
}
public override void Scrolled(UIScrollView scrollView)
{
_scrollSubject.OnNext(scrollView.ContentOffset);
}
~ReactiveTableViewSourceBase()
{
Console.WriteLine("Destorying " + GetType().Name);
}
protected ReactiveTableViewSourceBase(UITableView tableView, nfloat height, nfloat? heightHint = null)
: base(tableView)
{
tableView.RowHeight = height;
tableView.EstimatedRowHeight = heightHint ?? tableView.EstimatedRowHeight;
}
protected ReactiveTableViewSourceBase(UITableView tableView, IReactiveNotifyCollectionChanged<TViewModel> collection,
Foundation.NSString cellKey, nfloat height, nfloat? heightHint = null, Action<UITableViewCell> initializeCellAction = null)
: base(tableView, collection, cellKey, (float)height, initializeCellAction)
{
tableView.RowHeight = height;
tableView.EstimatedRowHeight = heightHint ?? tableView.EstimatedRowHeight;
}
public override void WillDisplay(UITableView tableView, UITableViewCell cell, Foundation.NSIndexPath indexPath)
{
if (indexPath.Section == (NumberOfSections(tableView) - 1) &&
indexPath.Row == (RowsInSection(tableView, indexPath.Section) - 1))
{
// We need to skip an event loop to stay out of trouble
BeginInvokeOnMainThread(() => _requestMoreSubject.OnNext(Unit.Default));
}
}
public override void RowSelected(UITableView tableView, Foundation.NSIndexPath indexPath)
{
var item = ItemAt(indexPath) as ICanGoToViewModel;
if (item != null)
{
item.GoToCommand.Execute();
}
base.RowSelected(tableView, indexPath);
}
protected override void Dispose(bool disposing)
{
_requestMoreSubject.Dispose();
_scrollSubject.Dispose();
base.Dispose(disposing);
}
}
public interface IInformsEnd
{
IObservable<Unit> RequestMore { get; }
}
I have a Tag class inflated from a nib file:
using Foundation;
using System;
using UIKit;
using ObjCRuntime;
namespace TagTest
{
public partial class Tag : UIView
{
public Tag (IntPtr handle) : base (handle)
{
}
public static Tag Create()
{
var arr = NSBundle.MainBundle.LoadNib("Tag", null, null);
var v = Runtime.GetNSObject<Tag>(arr.ValueAt(0));
return v;
}
public override void AwakeFromNib()
{
}
public UIButton Hashtag
{
get
{
return HashtagBtn;
}
}
public UILabel HashtagCount
{
get
{
return HashtagCountLbl;
}
}
}
}
which is used by the following viewmodel
using System.Collections.Generic;
using MvvmCross.Binding.BindingContext;
using MvvmCross.iOS.Views;
using TagTest.Core.ViewModels;
using UIKit;
using Cirrious.FluentLayouts.Touch;
using System;
using System.Drawing;
namespace TagTest
{
public partial class SearchView : MvxViewController
{
List<Tag> _tags;
public SearchView () : base ("SearchView", null)
{
}
new SearchViewModel ViewModel
{
get
{
return (SearchViewModel)base.ViewModel;
}
set
{
base.ViewModel = value;
}
}
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
DisplayTags();
View.UserInteractionEnabled = false;
View.SubviewsDoNotTranslateAutoresizingMaskIntoConstraints();
View.TranslatesAutoresizingMaskIntoConstraints = false;
EdgesForExtendedLayout = UIRectEdge.None;
ExtendedLayoutIncludesOpaqueBars = false;
AutomaticallyAdjustsScrollViewInsets = false;
View.ClipsToBounds = true;
View.BackgroundColor = UIColor.Red;
CreateBindings ();
}
public override void ViewDidAppear(bool animated)
{
base.ViewDidAppear(animated);
ViewModel.Init();
}
void CreateBindings()
{
var set = this.CreateBindingSet<SearchView, SearchViewModel> ();
set.Bind (this).For(x => x.Title).To (vm => vm.Title);
set.Apply ();
}
public override void ViewDidLayoutSubviews()
{
base.ViewDidLayoutSubviews();
View.LayoutIfNeeded();
}
void DisplayTags()
{
_tags = new List<Tag>();
if (ViewModel.Tags != null)
{
foreach (var item in ViewModel.Tags)
{
_tags.Add(Tag.Create());
}
UIView lastTemplateAdded = View;
for (int i = 0; i < _tags.Count; i++)
{
var tag = _tags[i];
tag.TranslatesAutoresizingMaskIntoConstraints = false;
tag.Hashtag.SetTitle(ViewModel.Tags[i].Tagname, UIControlState.Normal);
tag.HashtagCount.Text = ViewModel.Tags[i].Count.ToString();
tag.Frame = new RectangleF(100f, 300f, 100f, 50f);
tag.Hashtag.Enabled = true;
tag.Hashtag.UserInteractionEnabled = true;
tag.UserInteractionEnabled = true;
tag.Hashtag.TouchUpInside += (sender, e) =>
{
ViewModel.TagSelectedCommand.Execute(tag);
};
View.AddSubview(tag);
if (i == 0)
{
View.AddConstraints(
tag.AtTopOf(View),
tag.AtLeftOf(View),
tag.Height().EqualTo(20)
);
}
else
{
View.AddConstraints(
tag.Below(lastTemplateAdded, 20),
tag.AtLeftOf(View),
tag.Height().EqualTo(20)
);
}
lastTemplateAdded = tag;
}
}
}
public override void DidReceiveMemoryWarning ()
{
base.DidReceiveMemoryWarning ();
// Release any cached data, images, etc that aren't in use.
}
}
}
but the Hashtag button is not clickable, the TouchupInside doesn't appear to be fired. If I add a single button to the view it is clickable. What could be going wrong?
Found the problem. It was a combination of setting View.UserInteractionEnabled = false; and using the fluent constraints. I've now set UserInteractioEnabled to true and removed the fluent constraints. All working now.