Hopefully someone can help me with the following, because I am completely stuck.
I receive the exception below in my MvvmCross Xamarin.iOS application when I bind on my TableView. This only happens when I change the datasource (each time I change the date, the TableView needs to get updated).
Incident Identifier: 7E7C2B15-7CC4-4AE7-9891-C4FD82358009
CrashReporter Key: 46CC21C0-DDE1-4313-9658-EC79D767939B
Hardware Model: iPhone7,2
Process: UurwerkiOS [4326]
Path: /var/containers/Bundle/Application/75969477-A516-44C3-A5A3-5B24DDDC89C8/UurwerkiOS.app/UurwerkiOS
Identifier: com.route2it.uurwerk
Version: 1.0 (1.0.96)
Code Type: ARM-64
Parent Process: ??? [1]
Date/Time: 2016-07-04T13:16:38Z
Launch Time: 2016-07-04T13:16:31Z
OS Version: iPhone OS 9.3.2 (13F69)
Report Version: 104
Exception Type: SIGABRT
Exception Codes: #0 at 0x1816ac11c
Crashed Thread: 5
Application Specific Information:
*** Terminating app due to uncaught exception 'System.AggregateException', reason: 'System.AggregateException: A Task's exception(s) were not observed either by Waiting on the Task or accessing its Exception property. As a result, the unobserved exception was rethrown by the finalizer thread. ---> System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
at System.ThrowHelper.ThrowInvalidOperationException (ExceptionResource resource) <0x10044bec0 + 0x00024> in <filename unknown>:0
at System.Collections.Generic.List`1+Enumerator[T].MoveNextRare () <0x1003bf900 + 0x0002f> in <filename unknown>:0
at System.Collections.Generic.List`1+Enumerator[T].MoveNext () <0x1003bf830 + 0x0009f> in <filename unknown>:0
at MvvmCross.Binding.BindingContext.MvxTaskBasedBindingContext.<OnDataContextChange>b__20_0 () <0x1007c1990 + 0x0023f> in <filename unknown>:0
at System.Threading.Tasks.Task.InnerInvoke () <0x10043f1f0 + 0x0005f> in <filename unknown>:0
at System.Threading.Tasks.Task.Execute () <0x10043ea20 + 0x00043> in <filename unknown>:0
--- End of inner exception stack trace ---
---> (Inner Exception #0) System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
at System.ThrowHelper.ThrowInvalidOperationException (ExceptionResource resource) <0x10044bec0 + 0x00024> in <filename unknown>:0
at System.Collections.Generic.List`1+Enumerator[T].MoveNextRare () <0x1003bf900 + 0x0002f> in <filename unknown>:0
at System.Collections.Generic.List`1+Enumerator[T].MoveNext () <0x1003bf830 + 0x0009f> in <filename unknown>:0
at MvvmCross.Binding.BindingContext.MvxTaskBasedBindingContext.<OnDataContextChange>b__20_0 () <0x1007c1990 + 0x0023f> in <filename unknown>:0
at System.Threading.Tasks.Task.InnerInvoke () <0x10043f1f0 + 0x0005f> in <filename unknown>:0
at System.Threading.Tasks.Task.Execute () <0x10043ea20 + 0x00043> in <filename unknown>:0
At first I thought it was related to one of my Async methods (which maybe not complete in time while the next one was already running). So I removed all my async code, but the exception still occurs. I also made sure I don't change the enumerable collection myself. I fetch the data (which is simply a in memory array) and return it as a new list to the property to which the TableView is bound. Below are the code snippets that make up the binding (it's a lot of information but I wanted to be as complete as possible):
CalendarViewController:
public override void ViewDidLoad()
{
base.ViewDidLoad();
if (NavigationController != null)
NavigationController.NavigationBarHidden = false;
InitCalendar();
InitNavigationItem();
InitTableView();
ApplyConstraints();
var shiftForDateTableViewSource = new MvxSimpleTableViewSource(_tableView, CalendarTableViewCell.Key, CalendarTableViewCell.Key);
shiftForDateTableViewSource.DeselectAutomatically = true;
_tableView.RowHeight = 45;
_tableView.Source = shiftForDateTableViewSource;
var set = this.CreateBindingSet<CalendarView, CalendarViewModel>();
set.Bind(shiftForDateTableViewSource).To(vm => vm.ShiftsForSelectedDate);
set.Bind(shiftForDateTableViewSource).For(vm => vm.SelectionChangedCommand).To(vm => vm.ShiftSelectedCommand);
set.Apply();
_tableView.ReloadData();
}
private void InitTableView()
{
_tableView = new UITableView();
_tableView.RegisterClassForCellReuse(typeof(UITableViewCell), CalendarTableViewCell.Key);
Add(_tableView);
}
CalendarTableViewCell:
public partial class CalendarTableViewCell : MvxTableViewCell
{
public static readonly NSString Key = new NSString("CalendarTableViewCell");
public static readonly UINib Nib;
static CalendarTableViewCell()
{
Nib = UINib.FromName("CalendarTableViewCell", NSBundle.MainBundle);
}
protected CalendarTableViewCell(IntPtr handle) : base(handle)
{
}
public override void LayoutSubviews()
{
base.LayoutSubviews();
var set = this.CreateBindingSet<CalendarTableViewCell, Shift>();
set.Bind(StartTimeLabel).To(vm => vm.StartDate).WithConversion("StringFormat", "HH:mm");
set.Bind(EndTimeLabel).To(vm => vm.EndDate).WithConversion("StringFormat", "HH:mm");
set.Bind(ColorBarView).For("BackgroundColor").To(vm => vm.Color).WithConversion("RGB");
set.Bind(TitleLabel).To(vm => vm).WithConversion("ConcatenatedEventTitle");
set.Bind(LocationLabel).To(vm => vm.Location);
set.Apply();
}
}
CalendarViewModel:
public class CalendarViewModel
: MvxViewModel
{
private readonly IShiftService _shiftService;
public CalendarViewModel(IShiftService shiftService)
{
if (shiftService == null)
throw new ArgumentNullException(nameof(shiftService));
_shiftService = shiftService;
}
public override void Start()
{
base.Start();
Shifts = _shiftService.GetShiftsForEmployeeAsync(1);
}
private IEnumerable<Shift> _shifts;
public IEnumerable<Shift> Shifts
{
get { return _shifts; }
set
{
SetProperty(ref _shifts,
value,
nameof(Shifts));
}
}
private IEnumerable<Shift> _shiftsForSelectedDate;
public IEnumerable<Shift> ShiftsForSelectedDate
{
get { return _shiftsForSelectedDate; }
private set
{
if (_shiftsForSelectedDate == value)
return;
SetProperty(ref _shiftsForSelectedDate,
value,
nameof(ShiftsForSelectedDate));
}
}
private DateTime? _selectedDate;
public DateTime? SelectedDate
{
get { return _selectedDate; }
set
{
if (_selectedDate == value)
return;
SetProperty(ref _selectedDate,
value,
nameof(SelectedDate));
if (_selectedDate.HasValue)
FetchShiftsForSelectedDate();
}
}
private void FetchShiftsForSelectedDate()
{
ShiftsForSelectedDate = _shiftService.GetShiftsForSelectedDateAsync(_selectedDate.Value);
}
}
MockShiftService (implements the IShiftService interface):
public class MockShiftService
: IShiftService
{
private IList<Shift> _shifts;
public MockShiftService()
{
Initialize();
}
public IEnumerable<Shift> GetShiftsForEmployeeAsync(int employeeId)
{
return _shifts;
}
public IEnumerable<Shift> GetShiftsForSelectedDateAsync(DateTime selectedDate)
{
var endDate = selectedDate.Date.Add(new TimeSpan(23, 59, 59));
return _shifts
.Where(s => s.StartDate <= endDate && s.EndDate >= selectedDate)
.ToList();
}
public Shift GetShiftByIdAsync(int shiftId)
{
return _shifts.First((shift) => shift.Id == shiftId);
}
private void Initialize()
{
var shifts = new List<Shift>();
// The in memory array gets populated here which
// is straight forward creating instances of the
// 'Shift' class and assigning it's properties before
// adding it to the 'shifts' collection. I left
// this code out to keep it as short as possible.
}
}
UPDATE:
I have referenced my project directly to the debug assemblies of MvvmCross and figured out that the exception is thrown on line 127 of the MvxTaskBasedBindingContext
class and always happens on the second iteration. From this I conclude that the collection is changed during the first iteration. Unfortunately I cannot figure out why or how.
I noticed that the MvxTaskBasedBindingContext
replaces the MvxBindingContext
(changed by softlion on 11-5-2016). When I force my application to use the MvxBindingContext
class all works well (although a bit laggy). This makes me believe the problem is in the MvxTaskBasedBindingContext
but I really can't figure out why, any help would be greatly appreciated.
UPDATE 2:
After some more debugging and fiddling around I found out that the exception is related to the bindings set by my CalendarTableViewCell
class (which should provide the layout for each item in the tableview defined in my CalendarViewController
. When I comment out the bindings in the CalendarTableViewCell
class the exception does not occur (see my code above). I still don't know what could be wrong though.
You can make use of DelayBind
in your CalendarTableViewCell
to delay binding until your DataContext
gets set on your BindingContext
public partial class CalendarTableViewCell : MvxTableViewCell
{
...
public override void LayoutSubviews()
{
base.LayoutSubviews();
this.DelayBind(() =>
{
var set = this.CreateBindingSet<CalendarTableViewCell, Shift>();
set.Bind(StartTimeLabel).To(vm => vm.StartDate).WithConversion("StringFormat", "HH:mm");
set.Bind(EndTimeLabel).To(vm => vm.EndDate).WithConversion("StringFormat", "HH:mm");
set.Bind(ColorBarView).For("BackgroundColor").To(vm => vm.Color).WithConversion("RGB");
set.Bind(TitleLabel).To(vm => vm).WithConversion("ConcatenatedEventTitle");
set.Bind(LocationLabel).To(vm => vm.Location);
set.Apply();
});
}
}