Monotouch dialog with MvvmCross cell reuse mixes up cell data when scrolling - ios

I am building an app with quite a number sub classed monotouch dialog RootElements . Part of customising the look and feel of the cell was adding a notification view on the far right of the cell based on boolean value bound from the viewmodel, as part of the validation of that specific cell.
This works great if all the cells never go out of view. But when scrolling comes into play, these UIViews added to the cells get mixed up.
I have tried overriding the CellKey getter and an assortment of other options to no avail, any assistance would be greatly appreciated. Thank you.
public class PgpStyledRootElement : RootElement, IElementSizing, IValidatableElement
{
float _height;
bool _isRequired = false;
UIView notificationView;
protected IMvxMessenger _messenger;
protected MvxSubscriptionToken _token;
public WeakReference WeakNotificationView { get; set;}
public UIView NotificationView
{
get
{
if (WeakNotificationView == null || !WeakNotificationView.IsAlive)
return null;
return WeakNotificationView.Target as UIView;
}
}
private UITableViewCell _activeCell;
public UITableViewCell ActiveCell
{
get
{
if(_activeCell == null)
{
_activeCell = this.GetActiveCell ();
}
return _activeCell;
}
set
{
_activeCell = value;
}
}
private bool _isValid = true;
public bool IsValid
{
get { return _isValid; }
set
{
_isValid = value;
if (_isValid)
{
UIApplication.SharedApplication.InvokeOnMainThread (RemoveNotificationView);
}
else
{
UIApplication.SharedApplication.InvokeOnMainThread (AddNotificationView);
}
}
}
public PgpStyledRootElement(string caption, bool isRequired, Group group = null) : base(caption, group) {
}
public PgpStyledRootElement(string caption, bool isRequired, float height, bool fullPageWidth, Group group = null) : base(caption, group) {
_height = height;
}
public PgpStyledRootElement(string caption, bool isRequired, float height, Group group = null, string defaultDetailText = null) : base(caption, group) {
_height = height;
_isRequired = isRequired;
_messenger = Mvx.Resolve<IMvxMessenger> ();
_token = _messenger.Subscribe<ScreenWillRotateMessage> (HandleScreenWillRotate);
}
public PgpStyledRootElement (string caption, bool isRequired, float height, Func<RootElement, UIViewController> createOnSelected) : base (caption, createOnSelected)
{
this.Sections = new List<Section> ();
_height = height;
_messenger = Mvx.Resolve<IMvxMessenger> ();
_token = _messenger.Subscribe<ScreenWillRotateMessage> (HandleScreenWillRotate);
}
public PgpStyledRootElement(string caption, float height, Group group = null) : base(caption, group) {
_height = height;
}
protected override UITableViewCell GetCellImpl (UITableView tv)
{
var cell = base.GetCellImpl (tv);
HandleInitialLoadValidation (cell);
return cell;
}
protected override void UpdateCellDisplay (UITableViewCell cell)
{
base.UpdateCellDisplay (cell);
if (cell != null) {
cell.TextLabel.TextColor = CaptureListingsStyleConfig.DefaultTableCellTextColor;
cell.SetNeedsLayout ();
}
}
public void HandleInitialLoadValidation (UITableViewCell cell)
{
if (_isRequired)
{
if (!IsValid)
{
AddInitialNotificationView (cell);
}
}
}
static void GetRequiredIndicatorFrame (UITableViewCell cell, out float indicatorPointX, out float indicatorHeight)
{
if (UIApplication.SharedApplication.StatusBarOrientation == UIInterfaceOrientation.Portrait || UIApplication.SharedApplication.StatusBarOrientation == UIInterfaceOrientation.PortraitUpsideDown) {
indicatorPointX = cell.Frame.Width + 120;
indicatorHeight = cell.Frame.Height;
}
else {
indicatorPointX = cell.Frame.Width * 2 + 56;
indicatorHeight = cell.Frame.Height;
}
}
static void GetRequiredIndicatorFrameForRotationChange (UITableViewCell cell, out float indicatorPointX, out float indicatorHeight)
{
if (UIApplication.SharedApplication.StatusBarOrientation == UIInterfaceOrientation.Portrait || UIApplication.SharedApplication.StatusBarOrientation == UIInterfaceOrientation.PortraitUpsideDown) {
indicatorPointX = cell.Frame.Width - 263;
indicatorHeight = cell.Frame.Height;
}
else {
indicatorPointX = cell.Frame.Width + 249;
indicatorHeight = cell.Frame.Height;
}
}
void HandleScreenWillRotate (ScreenWillRotateMessage obj)
{
RectangleF newFrame;
if(NotificationView != null)
{
const int indicatorpointY = 0;
const int indicatorWidth = 7;
float indicatorPointX;
float indicatorHeight;
GetRequiredIndicatorFrameForRotationChange (ActiveCell, out indicatorPointX, out indicatorHeight);
CreateNewNotificationViewFrame (out newFrame, indicatorpointY, indicatorWidth, indicatorPointX, indicatorHeight);
NotificationView.Frame = newFrame;
}
}
static void CreateNewNotificationViewFrame (out RectangleF newFrame, int indicatorpointY, int indicatorWidth, float indicatorPointX, float indicatorHeight)
{
newFrame = new RectangleF (indicatorPointX, indicatorpointY, indicatorWidth, indicatorHeight);
}
public void AddInitialNotificationView (UITableViewCell cell)
{
if (cell != null) {
const int indicatorpointY = 0;
const int indicatorWidth = 7;
float indicatorPointX;
float indicatorHeight;
GetRequiredIndicatorFrame (cell, out indicatorPointX, out indicatorHeight);
if (Util.UserInterfaceIdiomIsPhone) {
indicatorPointX = cell.Frame.Width - indicatorWidth;
}
AddViewToCell (cell, indicatorpointY, indicatorWidth, indicatorPointX, indicatorHeight);
}
}
public void AddNotificationView ()
{
if (ActiveCell != null) {
const int indicatorpointY = 0;
const int indicatorWidth = 7;
var indicatorPointX = ActiveCell.Frame.Width - indicatorWidth;
var indicatorHeight = ActiveCell.Frame.Height;
if (Util.UserInterfaceIdiomIsPhone) {
indicatorPointX = ActiveCell.Frame.Width - indicatorWidth;
}
AddViewToCell (ActiveCell, indicatorpointY, indicatorWidth, indicatorPointX, indicatorHeight);
}
}
void AddViewToCell (UITableViewCell cell, int indicatorpointY, int indicatorWidth, float indicatorPointX, float indicatorHeight)
{
if (NotificationView == null) {
notificationView = new UIView (new RectangleF (indicatorPointX, indicatorpointY, indicatorWidth, indicatorHeight));
notificationView.BackgroundColor = UIColor.Red;
WeakNotificationView = new WeakReference (notificationView);
cell.Add (NotificationView);
}
}
public void RemoveNotificationView()
{
if(NotificationView != null)
{
NotificationView.RemoveFromSuperview ();
WeakNotificationView = null; //This should technically not be necessary, but lets do it anyway
}
}
#region IElementSizing implementation
public float GetHeight (UITableView tableView, NSIndexPath indexPath)
{
if(_height == 0.0f)
return CaptureListingsStyleConfig.DefaultTextCellHeight; //return CaptureListingsStyleConfig.DefaultRootCellHeight ();
return _height;
}
#endregion
}

Whenever a cell is required for display, then the DialogViewController calls the Element's GetCell method. This method then executes GetCellImpl followed by UpdateCellDisplay.
In the case of reuse, I suspect your RootElement is returning the reused Cell in GetCellImpl (because that's what the base class returns) and then not fully updating it. You could try modifying your UpdateCellDisplay method so that it updates the cell display to reflect the current state of your Element - i.e. adding/removing the notification display.

Related

Xamarin: How to avoid internal ListView from change its content size when showing keyboard on iOS

Im creating this content page:
Content = new StackLayout()
{
Spacing = 0,
Orientation = StackOrientation.Vertical,
Children = {
(listView = new ListView
{
HasUnevenRows = true,
SeparatorVisibility = SeparatorVisibility.None,
IsPullToRefreshEnabled = true,
HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.FillAndExpand,
ItemsSource = listItems,
ItemTemplate = new MyDataTemplateSelector(userName),
BackgroundColor = Constants.Cor_ChatFundo
}),
(grid = new Grid
{
RowSpacing = 1,
ColumnSpacing = 2,
Padding = new Thickness(5),
BackgroundColor = Color.White,
VerticalOptions = LayoutOptions.End,
HorizontalOptions = LayoutOptions.FillAndExpand,
ColumnDefinitions =
{
new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) },
new ColumnDefinition { Width = new GridLength(1, GridUnitType.Auto) }
},
RowDefinitions =
{
new RowDefinition { Height = new GridLength(40) }
}
})
}
};
grid.Children.Add(sendMessageEntry = new Entry
{
FontSize = 18,
HeightRequest = 30,
Placeholder = "Type here...",
Keyboard = Keyboard.Chat
}, 0, 0);
grid.Children.Add(buttonSend = new Button
{
Text = "Send"
}, 1, 0);
I'm using a modified version of the KeyboardOverlapRenderer to move the entire page UP when the keyboard is shown.
The modified version of the KeyboardOverlapRenderer is to handle the suggestion bar above the iOS8 keyboard... the original version doesn't handle that.
The KeyboardOverlapRenderer class:
using System;
using Xamarin.Forms.Platform.iOS;
using Foundation;
using UIKit;
using Xamarin.Forms;
using CoreGraphics;
using EficienciaEnergetica.iOS.KeyboardOverlap;
using System.Diagnostics;
using EficienciaEnergetica.ContentPages;
[assembly: ExportRenderer(typeof(Page), typeof(KeyboardOverlapRenderer))]
namespace EficienciaEnergetica.iOS.KeyboardOverlap
{
[Preserve(AllMembers = true)]
public class KeyboardOverlapRenderer : PageRenderer
{
Rectangle initialViewState;
NSObject _keyboardShowObserver;
NSObject _keyboardHideObserver;
private bool _pageWasShiftedUp;
private double _activeViewBottom;
private bool _isKeyboardShown;
public static void StaticInit()
{
var now = DateTime.Now;
Debug.WriteLine("Keyboard Overlap plugin initialized {0}", now);
}
public override void ViewWillAppear(bool animated)
{
base.ViewWillAppear(animated);
var page = Element as ContentPage;
if (page != null)
{
var contentScrollView = page.Content as ScrollView;
if (contentScrollView != null)
return;
initialViewState = Element.Bounds;
RegisterForKeyboardNotifications();
}
}
public override void ViewWillDisappear(bool animated)
{
base.ViewWillDisappear(animated);
UnregisterForKeyboardNotifications();
}
void RegisterForKeyboardNotifications()
{
if (_keyboardShowObserver == null)
_keyboardShowObserver = NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillShowNotification, OnKeyboardShow);
if (_keyboardHideObserver == null)
_keyboardHideObserver = NSNotificationCenter.DefaultCenter.AddObserver(UIKeyboard.WillHideNotification, OnKeyboardHide);
}
void UnregisterForKeyboardNotifications()
{
_isKeyboardShown = false;
if (_keyboardShowObserver != null)
{
NSNotificationCenter.DefaultCenter.RemoveObserver(_keyboardShowObserver);
_keyboardShowObserver.Dispose();
_keyboardShowObserver = null;
}
if (_keyboardHideObserver != null)
{
NSNotificationCenter.DefaultCenter.RemoveObserver(_keyboardHideObserver);
_keyboardHideObserver.Dispose();
_keyboardHideObserver = null;
}
}
protected virtual void OnKeyboardShow(NSNotification notification)
{
if (!IsViewLoaded)
return;
_isKeyboardShown = true;
var activeView = View.FindFirstResponder();
if (activeView == null)
return;
var keyboardFrame = UIKeyboard.FrameEndFromNotification(notification);
var isOverlapping = activeView.IsKeyboardOverlapping(View, keyboardFrame);
if (!isOverlapping)
return;
if (isOverlapping)
{
System.Diagnostics.Debug.WriteLine(keyboardFrame);
_activeViewBottom = activeView.GetViewRelativeBottom(View);
ShiftPageUp(keyboardFrame.Height, _activeViewBottom);
}
}
private void OnKeyboardHide(NSNotification notification)
{
if (!IsViewLoaded)
return;
_isKeyboardShown = false;
var keyboardFrame = UIKeyboard.FrameEndFromNotification(notification);
if (_pageWasShiftedUp)
ShiftPageDown(keyboardFrame.Height, _activeViewBottom);
}
private void ShiftPageUp(nfloat keyboardHeight, double activeViewBottom)
{
var pageFrame = initialViewState;// Element.Bounds;
var newY = pageFrame.Y + CalculateShiftByAmount(pageFrame.Height, keyboardHeight, activeViewBottom);
Element.LayoutTo(new Rectangle(pageFrame.X, newY,
pageFrame.Width, pageFrame.Height));
_pageWasShiftedUp = true;
}
private void ShiftPageDown(nfloat keyboardHeight, double activeViewBottom)
{
Element.LayoutTo(initialViewState);
_pageWasShiftedUp = false;
}
private double CalculateShiftByAmount(double pageHeight, nfloat keyboardHeight, double activeViewBottom)
{
return (pageHeight - activeViewBottom) - keyboardHeight;
}
}
}
The problem I have is when editing the entry. In iOS it shows the keyboard, but the content of the internal listview seems to allocate an empty space for the keyboard also, and it is possible to scroll down the list more than the elements that are inside it.
Is it possible to disable the listview keyboard notification behaviour in this situation? The ListView is not the main component in this page.
I found a solution for me!
Set lv.HeightRequest on the entry focus!
Seems to be

Multi Column Picker for Xamarin 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);
}

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.

CALayer Custom Property Animation with Xamarin

i am really frustrated, because I am trying to animate a Pie Chart (Arc Segment with a transparent hole) on iOS with CoreAnimation since the last week.
At the Moment, I am drawing the ArcSegment with a CAShapeLayer with its Path-Property. It looks great, but I can't animate this property.
I want to animate the Layer-Property like Radius, Segments etc, with a CABasicAnimation.
Is there anyone here, who can tell me how to solve this?
Thank you.
Regards
Ronny
public class ArcSegmentLayer : CAShapeLayer {
private const string StartAngleProperty = "StartAngle";
private const string EndAngleProperty = "EndAngle";
public static void RegisterProperties() {
ObjCProperties.RegisterDynamicProperty(typeof(ArcSegmentLayer), StartAngleProperty, typeof(float));
ObjCProperties.RegisterDynamicProperty(typeof(ArcSegmentLayer), EndAngleProperty, typeof(float));
}
public ArcSegmentLayer() { }
[Export("initWithLayer:")]
public ArcSegmentLayer(ArcSegmentLayer layer) {
this.LineWidth = layer.LineWidth;
this.Frame = layer.Frame;
this.FillColor = layer.FillColor;
this.StrokeColor = layer.StrokeColor;
this.Segments = layer.Segments;
this.Margin = layer.Margin;
}
#region Properties
public float StartAngle {
get { return ObjCProperties.GetFloatProperty(Handle, StartAngleProperty); }
set {
ObjCProperties.SetFloatProperty(Handle, StartAngleProperty, value);
}
}
public float EndAngle {
get { return ObjCProperties.GetFloatProperty(Handle, EndAngleProperty); }
set {
ObjCProperties.SetFloatProperty(Handle, EndAngleProperty, value);
}
}
public nint Segments {
get { return segments; }
set {
if (segments != value) {
segments = value;
this.SetNeedsDisplay();
}
}
}
public nfloat Margin {
get {
return margin;
}
set {
if (margin != value) {
margin = value;
this.SetNeedsDisplay();
}
}
}
#endregion
[Export("needsDisplayForKey:")]
public static bool NeedsDisplayForKey(NSString key) {
return key == StartAngleProperty
|| key == EndAngleProperty
|| key == "Margin"
|| key == "Segments"
|| key == "LineWidth"
|| key == "StrokeColor"
|| CALayer.NeedsDisplayForKey(key);
}
[Export("display")]
public override void Display() {
base.Display();
Console.WriteLine(this.EndAngle);
this.Path = CreateSegments().CGPath;
}
[Export("actionForKey:")]
public override NSObject ActionForKey(string eventKey) {
/*
if (eventKey == EndAngleProperty) {
CABasicAnimation animation = CABasicAnimation.FromKeyPath(eventKey);
animation.TimingFunction = CAMediaTimingFunction.FromName(CAMediaTimingFunction.Linear);
animation.From = new NSNumber(this.EndAngle); //PresentationLayer.ValueForKey(new NSString(eventKey));
//animation.Duration = CATransition. 1;
animation.Duration = 0;
return animation;
} else if (eventKey == StartAngleProperty) {
CABasicAnimation animation = CABasicAnimation.FromKeyPath(eventKey);
animation.TimingFunction = CAMediaTimingFunction.FromName(CAMediaTimingFunction.Linear);
animation.From = new NSNumber(this.StartAngle);
animation.Duration = 0;
return animation;
}*/
return base.ActionForKey(eventKey);
}
private UIBezierPath CreateSegments() {
var path = new UIBezierPath();
nfloat segmentSize = (nfloat)(360.0 / (nfloat)this.Segments);
nfloat startSegAngle = 0;
nfloat endSegAngle = startSegAngle + segmentSize;
if (this.Segments > 1) {
var fromSeg = (nint)((((double)this.Segments) * this.StartAngle) / 360.0);
var toSeg = (nint)((((double)this.Segments) * this.EndAngle) / 360.0);
for (var seg = 0; seg < this.Segments; seg++) {
var hiddenLayer = !(seg >= fromSeg && seg < toSeg);
if (!hiddenLayer) {
path.AppendPath(
this.CreateSegmentPath(
startSegAngle, endSegAngle - this.Margin));
}
startSegAngle += segmentSize;
endSegAngle += segmentSize;
}
} else if (this.Segments == 1) {
path.AppendPath(this.CreateSegmentPath(this.StartAngle, this.EndAngle));
}
return path;
}
private UIBezierPath CreateSegmentPath(nfloat startSegAngle, nfloat endSegAngle) {
var center = new CGPoint(x: this.Bounds.Width / 2f, y: this.Bounds.Height / 2f);
var radius = (nfloat)Math.Max(this.Bounds.Width, this.Bounds.Height) / 2f - this.LineWidth / 2f;
var path = UIBezierPath.FromArc(
center,
radius,
Deg2Rad(startSegAngle - 90f),
Deg2Rad(endSegAngle - 90f),
true);
path.MoveTo(center);
path.ClosePath();
path.Stroke();
return path;
}
private static nfloat Deg2Rad(nfloat value) {
return (nfloat)(floatPI / 180.0 * value);
}
private static readonly nfloat floatPI = (nfloat)Math.PI;
private nint segments;
private nfloat margin;
}
[DesignTimeVisible(true)]
public partial class ArcSegmentView : UIView {
public ArcSegmentView(IntPtr handle) : base(handle) {
this.strokeColor = UIColor.Black.CGColor;
}
#region Properties
[Export("StartAngle"), Browsable(true)]
public nfloat StartAngle {
get { return startAngle; }
set {
if (startAngle != value) {
startAngle = value;
((ArcSegmentLayer)this.Layer).StartAngle = (float)value;
this.SetNeedsDisplay();
}
}
}
[Export("EndAngle"), Browsable(true)]
public nfloat EndAngle {
get { return endAngle; }
set {
if (endAngle != value) {
endAngle = value;
((ArcSegmentLayer)this.Layer).EndAngle = (float)value;
this.SetNeedsDisplay();
}
}
}
[Export("Segments"), Browsable(true)]
public nint Segments {
get { return segments; }
set {
if (segments != value) {
segments = value;
((ArcSegmentLayer)this.Layer).Segments = value;
this.SetNeedsDisplay();
}
}
}
[Export("Margin"), Browsable(true)]
public nfloat Margin {
get { return margin; }
set {
if (margin != value) {
margin = value;
((ArcSegmentLayer)this.Layer).Margin = value;
this.SetNeedsDisplay();
}
}
}
[Export("LineWidth"), Browsable(true)]
public nfloat LineWidth {
get { return lineWidth; }
set {
if (lineWidth != value) {
lineWidth = value;
((ArcSegmentLayer)this.Layer).LineWidth = value;
this.SetNeedsDisplay();
}
}
}
[Export("StrokeColor"), Browsable(true)]
public CGColor StrokeColor {
get { return strokeColor; }
set {
if (StrokeColor != value) {
strokeColor = value;
((ArcSegmentLayer)this.Layer).StrokeColor = value;
//this.SetNeedsDisplay();
}
}
}
#endregion
[Export("layerClass")]
static Class LayerClass() {
return new Class(typeof(ArcSegmentLayer));
}
private nfloat lineWidth;
private nfloat margin;
private nint segments;
private nfloat startAngle;
private nfloat endAngle;
private CGColor strokeColor;
}
public partial class ViewController : UIViewController {
protected ViewController(IntPtr handle) : base(handle) { }
public override void ViewDidLoad() {
base.ViewDidLoad();
arcSegment.StartAngle = 45;
arcSegment.EndAngle = 90;
arcSegment.Margin = 2;
arcSegment.StrokeColor = UIColor.Red.CGColor;
arcSegment.Segments = 70;
arcSegment.LineWidth = 10;
CABasicAnimation animation = CABasicAnimation.FromKeyPath("EndAngle");
animation.TimingFunction = CAMediaTimingFunction.FromName(CAMediaTimingFunction.Linear);
animation.From = new NSNumber(45);
animation.To = new NSNumber(360);
animation.Duration = 10;
arcSegment.Layer.AddAnimation(animation, "EndAngle");
}
}
We have a sample that shows how to do this:
https://github.com/xamarin/ios-samples/tree/master/CustomPropertyAnimation
In particular:
https://github.com/xamarin/ios-samples/blob/master/CustomPropertyAnimation/AppDelegate.cs

Resources