dotnet/maui · error · ArgumentException

Index '{Math.Max(e.NewStartingIndex, e.OldStartingIndex)}' i

Error message

Index '{Math.Max(e.NewStartingIndex, e.OldStartingIndex)}' is greater than the number of rows '{lastIndex}'.

What it means

Thrown by the iOS ListViewRenderer when a NotifyCollectionChangedEventArgs reports a NewStartingIndex or OldStartingIndex greater than the current number of rows in the section (only checked for RetainElement strategy and non-grouped lists). This indicates the bound ObservableCollection raised a change event whose index is inconsistent with the actual row count.

Source

Thrown at src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/ListViewRenderer.cs:590

		{
			var exArgs = e as NotifyCollectionChangedEventArgsEx;
			if (exArgs != null)
				_dataSource.Counts[section] = exArgs.Count;

			// This means the UITableView hasn't rendered any cells yet
			// so there's no need to synchronize the rows on the UITableView
			if (Control.IndexPathsForVisibleRows == null && e.Action != NotifyCollectionChangedAction.Reset)
				return;

			var groupReset = resetWhenGrouped && Element.IsGroupingEnabled;

			// We can't do this check on grouped lists because the index doesn't match the number of rows in a section.
			// Likewise, we can't do this check on lists using RecycleElement because the number of rows in a section will remain constant because they are reused.
			if (!groupReset && Element.CachingStrategy == ListViewCachingStrategy.RetainElement)
			{
				var lastIndex = Control.NumberOfRowsInSection(section);
				if (e.NewStartingIndex > lastIndex || e.OldStartingIndex > lastIndex)
					throw new ArgumentException(
						$"Index '{Math.Max(e.NewStartingIndex, e.OldStartingIndex)}' is greater than the number of rows '{lastIndex}'.");
			}

			switch (e.Action)
			{
				case NotifyCollectionChangedAction.Add:
					if (e.NewStartingIndex == -1 || groupReset)
						goto case NotifyCollectionChangedAction.Reset;

					InsertRows(e.NewStartingIndex, e.NewItems.Count, section);

					break;

				case NotifyCollectionChangedAction.Remove:
					if (e.OldStartingIndex == -1 || groupReset)
						goto case NotifyCollectionChangedAction.Reset;

					DeleteRows(e.OldStartingIndex, e.OldItems.Count, section);

View on GitHub (pinned to f377ff1c5e)

Solutions

  1. Marshal all collection mutations to the main/UI thread.
  2. Ensure INotifyCollectionChanged indices always match the post-mutation list state (use standard ObservableCollection).
  3. Prefer ListViewCachingStrategy.RecycleElement or grouping if index semantics differ, but first fix the source of bad indices.
  4. Validate that NewStartingIndex/OldStartingIndex are within [0, Count] before raising CollectionChanged in custom collections.

Example fix

// before — mutating off thread
task.ContinueWith(_ => observableCollection.Insert(10, item));

// after
MainThread.BeginInvokeOnMainThread(() => observableCollection.Insert(index, item));
Defensive patterns

Strategy: validation

Validate before calling

// Marshal collection mutations to the main thread and validate indices
MainThread.BeginInvokeOnMainThread(() =>
{
    if (index >= 0 && index <= collection.Count)
        collection.Insert(index, item);
});

Prevention

When it happens

Trigger: An ObservableCollection<T> raises CollectionChanged with a starting index that exceeds the section's row count. Manipulating the underlying list from a background thread (cross-thread mutation). Calling collection.Move/Insert with a stale index. A custom INotifyCollectionChanged implementation emitting bad indices.

Common situations: Mutating the bound collection off the main thread. Batch operations that remove and re-add with mismatched indices. Wrapping a list with a custom notifier that miscalculates indices. Race between source mutation and renderer measurement.

Related errors


AI-assisted analysis of dotnet/maui@f377ff1c5e (2026-08-13). Data as JSON: /api/errors/23268ee5c43b4fd9. Report an issue: GitHub.