Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Picker ItemsSource fixes #19728

Merged
merged 10 commits into from
Jun 2, 2024
29 changes: 27 additions & 2 deletions src/Controls/src/Core/Picker/Picker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,11 @@ void OnItemDisplayBindingChanged(BindingBase oldValue, BindingBase newValue)

void OnItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// Do not execute when Items is locked because updates to ItemsSource will
// take care of it
if (((LockableObservableListWrapper)Items).IsLocked)
return;

var oldIndex = SelectedIndex;
var newIndex = SelectedIndex.Clamp(-1, Items.Count - 1);
//FIXME use the specificity of the caller
Expand Down Expand Up @@ -278,8 +283,9 @@ void OnItemsSourceChanged(IList oldValue, IList newValue)
}
else
{
((LockableObservableListWrapper)Items).InternalClear();
// Unlock, then clear, so OnItemsCollectionChanged executes
((LockableObservableListWrapper)Items).IsLocked = false;
((LockableObservableListWrapper)Items).InternalClear();
}
}

Expand All @@ -306,13 +312,27 @@ void AddItems(NotifyCollectionChangedEventArgs e)
int index = e.NewStartingIndex < 0 ? Items.Count : e.NewStartingIndex;
foreach (object newItem in e.NewItems)
((LockableObservableListWrapper)Items).InternalInsert(index++, GetDisplayMember(newItem));
if (index <= SelectedIndex)
UpdateSelectedItem(SelectedIndex);
}

void RemoveItems(NotifyCollectionChangedEventArgs e)
{
int index = e.OldStartingIndex < Items.Count ? e.OldStartingIndex : Items.Count;
foreach (object _ in e.OldItems)
((LockableObservableListWrapper)Items).InternalRemoveAt(index--);
if (index <= SelectedIndex)
{
var oldIndex = SelectedIndex;
var newIndex = SelectedIndex.Clamp(-1, Items.Count - 1);

SetValue(SelectedIndexProperty, newIndex, SetterSpecificity.FromHandler);
// Update SelectedItem separately if index hasn't changed
if (newIndex == oldIndex)
{
UpdateSelectedItem(newIndex);
}
}
}

void ResetItems()
Expand All @@ -323,7 +343,12 @@ void ResetItems()
foreach (object item in ItemsSource)
((LockableObservableListWrapper)Items).InternalAdd(GetDisplayMember(item));
Handler?.UpdateValue(nameof(IPicker.Items));
UpdateSelectedItem(SelectedIndex);

var oldIndex = SelectedIndex;
var newIndex = SelectedIndex.Clamp(-1, Items.Count - 1);
SetValue(SelectedIndexProperty, newIndex, SetterSpecificity.FromHandler);
if (oldIndex == newIndex)
UpdateSelectedItem(SelectedIndex);
}

static void OnSelectedIndexChanged(object bindable, object oldValue, object newValue)
Expand Down
4 changes: 2 additions & 2 deletions src/Controls/tests/Core.UnitTests/DeviceUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ async Task<bool> boom()
Assert.False(invoked, "Action invoked early.");

async Task MethodThatThrows() => await task;
Assert.ThrowsAsync<ApplicationException>(MethodThatThrows);
await Assert.ThrowsAsync<ApplicationException>(MethodThatThrows);
});

[Fact]
Expand Down Expand Up @@ -144,7 +144,7 @@ async Task boom()
Assert.False(invoked, "Action invoked early.");

async Task MethodThatThrows() => await task;
Assert.ThrowsAsync<ApplicationException>(MethodThatThrows);
await Assert.ThrowsAsync<ApplicationException>(MethodThatThrows);
});

private void MockPlatformServices(Action onInvokeOnMainThread, Action<Action> invokeOnMainThread = null)
Expand Down
2 changes: 1 addition & 1 deletion src/Controls/tests/Core.UnitTests/NavigationUnitTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -836,7 +836,7 @@ public async Task PopModalWithEmptyStackThrows()
var window = new TestWindow(new ContentPage());
var contentPage1 = new ContentPage();
var navigationPage = new TestNavigationPage(true, contentPage1);
Assert.ThrowsAsync<InvalidOperationException>(() => window.Navigation.PopModalAsync());
await Assert.ThrowsAsync<InvalidOperationException>(() => window.Navigation.PopModalAsync());
}

[Fact]
Expand Down
162 changes: 162 additions & 0 deletions src/Controls/tests/Core.UnitTests/PickerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,168 @@ public void TestItemsSourceCollectionChangedRemove()
Assert.Equal("Ringo", picker.Items[1]);
}

[Fact]
public void TestSelectedIndexAssignedItemsSourceCollectionChangedAppend()
{
var items = new ObservableCollection<object>
{
new { Name = "John" },
new { Name = "Paul" },
"Ringo"
};
var picker = new Picker
{
ItemDisplayBinding = new Binding("Name"),
ItemsSource = items,
};

picker.PropertyChanged += (sender, e) =>
{
if (e.PropertyName == nameof(Picker.SelectedIndex))
{
items.Add(new { Name = "George" });
}
};

picker.SelectedIndex = 1;

Assert.Equal(4, picker.Items.Count);
Assert.Equal("George", picker.Items[picker.Items.Count - 1]);
Assert.Equal(1, picker.SelectedIndex);
Assert.Equal(items[1], picker.SelectedItem);
}

[Fact]
public void TestSelectedIndexAssignedItemsSourceCollectionChangedClear()
{
var items = new ObservableCollection<object>
{
new { Name = "John" },
"Paul",
"Ringo"
};
var picker = new Picker
{
ItemDisplayBinding = new Binding("Name"),
ItemsSource = items,
};

picker.PropertyChanged += (sender, e) =>
{
if (e.PropertyName == nameof(Picker.SelectedIndex))
{
items.Clear();
}
};

picker.SelectedIndex = 1;

Assert.Empty(picker.Items);
Assert.Equal(-1, picker.SelectedIndex);
Assert.Null(picker.SelectedItem);
}

[Fact]
public void TestSelectedIndexAssignedItemsSourceCollectionChangedInsert()
{
var items = new ObservableCollection<object>
{
new { Name = "John" },
"Paul",
"Ringo"
};
var picker = new Picker
{
ItemDisplayBinding = new Binding("Name"),
ItemsSource = items,
};

picker.PropertyChanged += (sender, e) =>
{
if (e.PropertyName == nameof(Picker.SelectedIndex))
{
items.Insert(1, new { Name = "George" });
}
};

picker.SelectedIndex = 2;

Assert.Equal(4, picker.Items.Count);
Assert.Equal("George", picker.Items[1]);
Assert.Equal(2, picker.SelectedIndex);
Assert.Equal(items[2], picker.SelectedItem);
}

[Fact]
public void TestSelectedIndexAssignedItemsSourceCollectionChangedReAssign()
{
var items = new ObservableCollection<object>
{
new { Name = "John" },
"Paul",
"Ringo"
};
var bindingContext = new { Items = items };
var picker = new Picker
{
ItemDisplayBinding = new Binding("Name"),
BindingContext = bindingContext
};
picker.SetBinding(Picker.ItemsSourceProperty, "Items");

picker.PropertyChanged += (sender, e) =>
{
if (e.PropertyName == nameof(Picker.SelectedIndex))
{
items = new ObservableCollection<object>
{
"Peach",
"Orange"
};
picker.BindingContext = new { Items = items };
picker.ItemDisplayBinding = null;
}
};

picker.SelectedIndex = 1;

Assert.Equal(2, picker.Items.Count);
Assert.Equal("Peach", picker.Items[0]);
Assert.Equal(1, picker.SelectedIndex);
Assert.Equal("Orange", picker.SelectedItem);
}

[Fact]
public void TestSelectedItemAssignedItemsSourceCollectionChangedRemove()
{
var items = new ObservableCollection<object>
{
new { Name = "John" },
new { Name = "Paul" },
new { Name = "Ringo"},
};
var picker = new Picker
{
ItemDisplayBinding = new Binding("Name"),
ItemsSource = items,
};

picker.PropertyChanged += (sender, e) =>
{
if (e.PropertyName == nameof(Picker.SelectedIndex))
{
items.RemoveAt(1);
}
};

picker.SelectedIndex = 1;

Assert.Equal(2, picker.Items.Count);
Assert.Equal("Ringo", picker.Items[1]);
Assert.Equal(1, picker.SelectedIndex);
Assert.Equal(items[1], picker.SelectedItem);
}

[Fact]
public void SettingSelectedIndexUpdatesSelectedItem()
{
Expand Down
6 changes: 3 additions & 3 deletions src/Controls/tests/Core.UnitTests/ScrollViewUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -267,12 +267,12 @@ public void TestScrollToElementNotAnimated()
}

[Fact]
public void TestScrollToInvalid()
public async void TestScrollToInvalid()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

async Task

{
var scrollView = new ScrollView();

Assert.ThrowsAsync<ArgumentException>(() => scrollView.ScrollToAsync(new VisualElement(), ScrollToPosition.Center, true));
Assert.ThrowsAsync<ArgumentException>(() => scrollView.ScrollToAsync(null, (ScrollToPosition)500, true));
await Assert.ThrowsAsync<ArgumentException>(() => scrollView.ScrollToAsync(new VisualElement(), ScrollToPosition.Center, true));
await Assert.ThrowsAsync<ArgumentException>(() => scrollView.ScrollToAsync(null, (ScrollToPosition)500, true));
}

[Fact]
Expand Down
2 changes: 1 addition & 1 deletion src/Controls/tests/Core.UnitTests/ShellNavigatingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,7 @@ public async Task AbsoluteRoutingToPage()

Routing.RegisterRoute("catdetails", typeof(ContentPage));

Assert.ThrowsAnyAsync<Exception>(async () => await shell.GoToAsync($"//catdetails"));
await Assert.ThrowsAnyAsync<Exception>(async () => await shell.GoToAsync($"//catdetails"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting. Is there no CS4014 warning when await is missing?

Copy link
Contributor

@MartyIX MartyIX Jan 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW: The regex ^[ \t]+Assert\.ThrowsAsync seems to discover similar cases:

9 results - 6 files

src\Controls\tests\Core.UnitTests\DeviceUnitTests.cs:
  110  			async Task MethodThatThrows() => await task;
  111: 			Assert.ThrowsAsync<ApplicationException>(MethodThatThrows);
  112  		});

  146  			async Task MethodThatThrows() => await task;
  147: 			Assert.ThrowsAsync<ApplicationException>(MethodThatThrows);
  148  		});

src\Controls\tests\Core.UnitTests\NavigationUnitTest.cs:
  838  			var navigationPage = new TestNavigationPage(true, contentPage1);
  839: 			Assert.ThrowsAsync<InvalidOperationException>(() => window.Navigation.PopModalAsync());
  840  		}

src\Controls\tests\Core.UnitTests\ScrollViewUnitTests.cs:
  273  
  274: 			Assert.ThrowsAsync<ArgumentException>(() => scrollView.ScrollToAsync(new VisualElement(), ScrollToPosition.Center, true));
  275: 			Assert.ThrowsAsync<ArgumentException>(() => scrollView.ScrollToAsync(null, (ScrollToPosition)500, true));
  276  		}

src\Essentials\test\UnitTests\AppActions_Tests.cs:
  10  		public void AppActions_SetActions() =>
  11: 			Assert.ThrowsAsync<NotImplementedInReferenceAssemblyException>(() => AppActions.SetAsync(new List<AppAction>()));
  12  

  14  		public void AppActions_GetActions() =>
  15: 			Assert.ThrowsAsync<NotImplementedInReferenceAssemblyException>(() => AppActions.GetAsync());
  16  

src\Essentials\test\UnitTests\Sms_Tests.cs:
  11  		public Task Sms_Fail_On_NetStandard() =>
  12: 			Assert.ThrowsAsync<NotImplementedInReferenceAssemblyException>(() => Sms.ComposeAsync());
  13  	}

src\Essentials\test\UnitTests\TextToSpeech_Tests.cs:
  10  		public void TextToSpeech_Speak_Fail_On_NetStandard() =>
  11: 			 Assert.ThrowsAsync<NotImplementedInReferenceAssemblyException>(() => TextToSpeech.SpeakAsync("Xamarin Essentials!"));
  12  

Edit: regex ^[ \t]+Assert\.Throws[a-zA-Z]*Async seems to be better.

}

[Fact]
Expand Down
4 changes: 2 additions & 2 deletions src/Controls/tests/Core.UnitTests/ShellUriHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ public async Task RelativeNavigationToShellElementThrows()
shell.Items.Add(item1);
shell.Items.Add(item2);

Assert.ThrowsAnyAsync<Exception>(async () => await shell.GoToAsync($"domestic"));
await Assert.ThrowsAnyAsync<Exception>(async () => await shell.GoToAsync($"domestic"));
}


Expand All @@ -266,7 +266,7 @@ public async Task RelativeNavigationWithRoute()
shell.Items.Add(item2);

Routing.RegisterRoute("catdetails", typeof(ContentPage));
Assert.ThrowsAnyAsync<Exception>(async () => await shell.GoToAsync($"cats/catdetails?name=domestic"));
await Assert.ThrowsAnyAsync<Exception>(async () => await shell.GoToAsync($"cats/catdetails?name=domestic"));

// once relative routing with a stack is fixed then we can remove the above exception check and add below back in
// await shell.GoToAsync($"cats/catdetails?name=domestic")
Expand Down
Loading