Multi Column Picker for Xamarin iOS - ios

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);
}

Related

Bindable Property is not updating view on iOS

I am creating a cross platform app on xamarin.forms. I needed a custom renderer with gradient background and i've created it and it works well on both android and ios.
However, when i want to change the colors of the gradient background, it does not work on iOS.
Test.xaml.cs
namespace KiaiDay
{
public class TesteViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private Color _startColor = Color.Green;
public Color SStartColor
{
get => _startColor;
set { _startColor = value; OnPropertyChanged(nameof(SStartColor)); }
}
private Color _endColor = Color.Blue;
public Color EEndColor
{
get => _endColor;
set { _endColor = value; OnPropertyChanged(nameof(EEndColor)); }
}
public ICommand Select
{
get => new Command(() =>
{
SStartColor = Color.Red;
EEndColor = Color.Brown;
});
}
#region INotifyPropertyChanged Implementation
void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
if (PropertyChanged == null)
return;
PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class teste : ContentPage
{
public teste()
{
BindingContext = new TesteViewModel();
InitializeComponent();
}
}
}
Test.xaml
<ContentPage.Content>
<StackLayout HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
<local:ShadowFrame StartColor="{Binding SStartColor}" EndColor="{Binding EEndColor}" HorizontalOptions="Center" VerticalOptions="Center">
<Label Text="Teste"/>
<local:ShadowFrame.GestureRecognizers>
<TapGestureRecognizer NumberOfTapsRequired="1" Command="{Binding Select}"/>
</local:ShadowFrame.GestureRecognizers>
</local:ShadowFrame>
</StackLayout>
</ContentPage.Content>
Custom Renderer in PCL
public class ShadowFrame : Frame
{
public static BindableProperty ElevationProperty = BindableProperty.Create(nameof(Elevation), typeof(float), typeof(ShadowFrame), default(float));
public static readonly BindableProperty StartColorProperty = BindableProperty.Create(nameof(StartColor), typeof(Color), typeof(ShadowFrame), default(Color));
public static readonly BindableProperty EndColorProperty = BindableProperty.Create(nameof(EndColor), typeof(Color), typeof(ShadowFrame), default(Color));
public float Elevation
{
get { return (float)GetValue(ElevationProperty); }
set { SetValue(ElevationProperty, value); }
}
public Color StartColor
{
get { return (Color)GetValue(StartColorProperty); }
set { SetValue(StartColorProperty, value); }
}
public Color EndColor
{
get { return (Color)GetValue(EndColorProperty); }
set { SetValue(EndColorProperty, value); }
}
}
iOS Renderer(i think the problem is here)
[assembly: ExportRenderer(typeof(ShadowFrame), typeof(ShadowFrameRenderer))]
namespace KiaiDay.iOS.Renderers
{
public class ShadowFrameRenderer : FrameRenderer
{
CAGradientLayer gradientLayer;
private Color StartColor { get; set; }
private Color EndColor { get; set; }
CGRect rect;
public static void Initialize()
{
// empty, but used for beating the linker
}
public ShadowFrameRenderer() => gradientLayer = new CAGradientLayer();
public override void Draw(CGRect rect)
{
this.rect = rect;
var stack = (ShadowFrame)this.Element;
StartColor = stack.StartColor;
EndColor = stack.EndColor;
CGColor startColor = this.StartColor.ToCGColor();
CGColor endColor = this.EndColor.ToCGColor();
#region for Vertical Gradient
//var gradientLayer = new CAGradientLayer();
#endregion
#region for Horizontal Gradient
//var gradientLayer = new CAGradientLayer()
//{
// StartPoint = new CGPoint(0, 0.5),
// EndPoint = new CGPoint(1, 0.5)
//};
#endregion
gradientLayer.Frame = rect;
gradientLayer.Colors = new CGColor[] {
startColor,
endColor
};
NativeView.Layer.InsertSublayer(gradientLayer, 0);
base.Draw(rect);
}
protected override void OnElementChanged(ElementChangedEventArgs<Frame> e)
{
base.OnElementChanged(e);
var stack = (ShadowFrame)e.NewElement;
if (e.OldElement != null || Element == null)
{
return;
}
try
{
this.StartColor = stack.StartColor;
this.EndColor = stack.EndColor;
UpdateShadow();
Draw(rect);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("ERROR:", ex.Message);
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
var stack = (ShadowFrame) sender;
if (e.PropertyName == ShadowFrame.ElevationProperty.PropertyName)
{
UpdateShadow();
}
if (e.PropertyName == ShadowFrame.StartColorProperty.PropertyName)
{
this.StartColor = stack.StartColor;
}
if (e.PropertyName == ShadowFrame.EndColorProperty.PropertyName)
{
this.EndColor = stack.EndColor;
}
}
private void UpdateShadow()
{
var shadowFrame = (ShadowFrame)Element;
// Update shadow to match better material design standards of elevation
Layer.ShadowRadius = shadowFrame.Elevation;
Layer.ShadowColor = UIColor.Gray.CGColor;
Layer.ShadowOffset = new CGSize(2, 2);
Layer.ShadowOpacity = 0.80f;
Layer.ShadowPath = UIBezierPath.FromRect(Layer.Bounds).CGPath;
Layer.MasksToBounds = false;
}
}
}
I would like to update iOS view immediatly after i change the color through the command.
If you change a property and the view doesn't update the issue has to be in OnElementPropertyChanged.
You need to call UpdateShadow() and Draw().
After digging a lot i think i have found a solution:
iOS Custom Renderer :
public class GradientStackColorRenderer : VisualElementRenderer<Frame>
{
private Color StartColor { get; set; }
private Color EndColor { get; set; }
public override CGRect Frame
{
get
{
return base.Frame;
}
set
{
if (value.Width > 0 && value.Height > 0)
{
foreach (var layer in NativeView?.Layer.Sublayers.Where(layer => layer is CAGradientLayer))
layer.Frame = new CGRect(0, 0, value.Width, value.Height);
}
base.Frame = value;
}
}
protected override void OnElementChanged(ElementChangedEventArgs<StackLayout> e)
{
base.OnElementChanged(e);
if (e.OldElement == null)
{
try
{
var stack = e.NewElement as GradientColorStack;
this.StartColor = stack.StartColor;
this.EndColor = stack.EndColor;
AdicionarGradiente();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(#"ERROR:", ex.Message);
}
}
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
var stack = this.Element as GradientColorStack;
if(e.PropertyName == GradientColorStack.StartColorProperty.PropertyName)
{
this.StartColor = stack.StartColor;
AdicionarGradiente();
}
if (e.PropertyName == GradientColorStack.EndColorProperty.PropertyName)
{
this.EndColor = stack.EndColor;
AdicionarGradiente();
}
}
public void AdicionarGradiente()
{
var gradient = new CAGradientLayer();
gradient.CornerRadius = NativeView.Layer.CornerRadius = 5;
gradient.Colors = new CGColor[] { StartColor.ToCGColor(), EndColor.ToCGColor() };
var layer = NativeView?.Layer.Sublayers.LastOrDefault();
NativeView?.Layer.InsertSublayerBelow(gradient, layer);
}
public static CGColor ToCGColor(Color color)
{
return new CGColor(CGColorSpace.CreateSrgb(), new nfloat[] { (float)color.R, (float)color.G, (float)color.B, (float)color.A });
}
}

Xamarin iOS TableView Checkmark row checks other row too

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!

UITableViewSource never creates cells

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.

ReactiveList updates doesn't seem to marshal correctly the first time in a UITableView

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; }
}

MvvMCross UITableView bind local property to UITableViewCellAccessory.CheckMark

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");
}

Resources