maui
maui copied to clipboard
[Android] CarouselView: VirtualView cannot be null here, when clearing and adding items on second navigation
Description
In Android only, starting from Maui 8.0.6 SR1 and continues to at least 8.0.20 SR4 (~might be in 8.0.21 SR4.1 but haven't tested that one~ EDIT: it is still in 8.0.21 SR4.1), an exception occurs when adding items to the ObservableCollection backing a CarouselView after having navigated to the same page for a second time. This is very similar to what used to happen to Collection Views in .NET 7 as chronicled in #12219 (and fixed in https://github.com/dotnet/maui/pull/13385), except now it happens for CarouselViews. Case-in-point, my repro project is merely an updated and modified version of jaldinger's repro project from back in that issue, and it manifests the same behavior for CarouselViews now as it did for CollectionViews back then (I have verified that CollectionViews do not experience this issue anymore, so the previous bugfix hasn't been regressed).
https://github.com/dotnet/maui/assets/20206332/729e8bb6-ffdb-49ab-8982-7ba93add664c
Occurs in 8.0.21 SR 4.1, 8.0.20 SR4, 8.0.14 SR3.1, 8.0.10 SR3, 8.0.7 SR2, ~Might be in the newest 8.0.21 SR 4.1 but haven't tested that one yet since it just came out hours ago.~ It is tested now
Steps to Reproduce
- Create a File > New .NET MAUI App
- Add a second ContentPage to the project, named "Page2"
- In AppShell.xaml.cs add: Routing.RegisterRoute("Page2", typeof(Page2));
- In MauiProgram.cs add: builder.Services.AddScoped<Page2>();
- Add a class to the project called "Foo" and populate it as follows:
public class Foo
{
public string ImagePath { get; set; }
}
- Copy all the images from here and paste them in your project's Resources/Images folder
- In Page2.xaml replace all the content with:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:MauiAppRepro1"
x:Class="MauiAppRepro1.Page2"
x:DataType="local:Page2"
Title="Page2"
x:Name="this"
BindingContext="{x:Reference this}">
<VerticalStackLayout>
<CarouselView ItemsSource="{Binding FooImages}" MaximumHeightRequest="150">
<CarouselView.ItemTemplate>
<DataTemplate x:DataType="local:Foo">
<VerticalStackLayout>
<Image Source="{Binding ImagePath}" />
</VerticalStackLayout>
</DataTemplate>
</CarouselView.ItemTemplate>
</CarouselView>
<Button
Text="Load items"
Pressed="Button_Pressed" />
</VerticalStackLayout>
</ContentPage>
- In Page2.xaml.cs replace the class with:
public partial class Page2 : ContentPage
{
public ObservableCollection<Foo> FooImages { get; set; } = new();
public Page2()
{
InitializeComponent();
this.BindingContext = this;
}
private async void Button_Pressed(object sender, EventArgs e)
{
FooImages.Clear();
await Task.Delay(1000);
FooImages.Add(new Foo { ImagePath = "red_mm.png" });
FooImages.Add(new Foo { ImagePath = "yellow_mm.png" });
FooImages.Add(new Foo { ImagePath = "blue_mm.png" });
FooImages.Add(new Foo { ImagePath = "orange_mm.png" });
}
}
- In MainPage.xaml.cs modify OnCounterClicked as follows:
private async void OnCounterClicked(object sender, EventArgs e)
{
count++;
if (count == 1)
CounterBtn.Text = $"Clicked {count} time";
else
CounterBtn.Text = $"Clicked {count} times";
SemanticScreenReader.Announce(CounterBtn.Text);
await Shell.Current.GoToAsync("/Page2");
}
- Run the app on Android
- Click the button on MainPage
- Click the button on Page2 two or more times --> works fine
- Go back to MainPage
- Click the button on MainPage again
- Click the button on Page2 --> Exception occurs:
**System.InvalidOperationException:** 'VirtualView cannot be null here'
Link to public reproduction project repository
https://github.com/RedZone908/CarouselVirtualViewErrorRepro/tree/master
Version with bug
8.0.6 SR1
Is this a regression from previous behavior?
Yes, this used to work in .NET MAUI
Last version that worked well
8.0.3 GA
Affected platforms
Android
Affected platform versions
Android 21 and up
Did you find any workaround?
Simply instantiating a new ObservableCollection
in the page's OnAppearing
handler instead of reusing the existing one will not trigger the error.
Relevant log output
**System.InvalidOperationException:** 'VirtualView cannot be null here'
[libc] Requested dump for pid 17324 (e.mauiapprepro1)
**System.InvalidOperationException:** 'VirtualView cannot be null here'
[mono-rt] [ERROR] FATAL UNHANDLED EXCEPTION: System.InvalidOperationException: VirtualView cannot be null here
[mono-rt] at Microsoft.Maui.Handlers.ViewHandler`2[[Microsoft.Maui.Controls.CarouselView, Microsoft.Maui.Controls, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null],[AndroidX.RecyclerView.Widget.RecyclerView, Xamarin.AndroidX.RecyclerView, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].get_VirtualView() in D:\a\_work\1\s\src\Core\src\Handlers\View\ViewHandlerOfT.cs:line 42
[mono-rt] at Microsoft.Maui.Controls.Handlers.Items.CarouselViewHandler.CreateAdapter() in D:\a\_work\1\s\src\Controls\src\Core\Handlers\Items\CarouselViewHandler.Android.cs:line 17
[mono-rt] at Microsoft.Maui.Controls.Handlers.Items.MauiRecyclerView`3[[Microsoft.Maui.Controls.CarouselView, Microsoft.Maui.Controls, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null],[Microsoft.Maui.Controls.Handlers.Items.ItemsViewAdapter`2[[Microsoft.Maui.Controls.CarouselView, Microsoft.Maui.Controls, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null],[Microsoft.Maui.Controls.Handlers.Items.IItemsViewSource, Microsoft.Maui.Controls, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], Microsoft.Maui.Controls, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null],[Microsoft.Maui.Controls.Handlers.Items.IItemsViewSource, Microsoft.Maui.Controls, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].UpdateAdapter() in D:\a\_work\1\s\src\Controls\src\Core\Handlers\Items\Android\MauiRecyclerView.cs:line 236
[mono-rt] at Microsoft.Maui.Controls.Handlers.Items.MauiCarouselRecyclerView.UpdateAdapter() in D:\a\_work\1\s\src\Controls\src\Core\Handlers\Items\Android\MauiCarouselRecyclerView.cs:line 140
[mono-rt] at Microsoft.Maui.Controls.Handlers.Items.MauiCarouselRecyclerView.CollectionItemsSourceChanged(Object sender, NotifyCollectionChangedEventArgs e) in D:\a\_work\1\s\src\Controls\src\Core\Handlers\Items\Android\MauiCarouselRecyclerView.cs:line 251
[mono-rt] at Microsoft.Maui.Controls.Handlers.Items.ObservableItemsSource.CollectionChanged(NotifyCollectionChangedEventArgs args) in D:\a\_work\1\s\src\Controls\src\Core\Handlers\Items\Android\ItemsSources\ObservableItemsSource.cs:line 131
[mono-rt] at Microsoft.Maui.Controls.Handlers.Items.ObservableItemsSource.<>c__DisplayClass33_0.<CollectionChanged>b__0() in D:\a\_work\1\s\src\Controls\src\Core\Handlers\Items\Android\ItemsSources\ObservableItemsSource.cs:line 106
[mono-rt] at Microsoft.Maui.Controls.DispatcherExtensions.DispatchIfRequired(IDispatcher dispatcher, Action action) in D:\a\_work\1\s\src\Controls\src\Core\DispatcherExtensions.cs:line 59
[mono-rt] at Microsoft.Maui.Controls.Handlers.Items.ObservableItemsSource.CollectionChanged(Object sender, NotifyCollectionChangedEventArgs args) in D:\a\_work\1\s\src\Controls\src\Core\Handlers\Items\Android\ItemsSources\ObservableItemsSource.cs:line 106
[mono-rt] at Microsoft.Maui.Controls.WeakNotifyCollectionChangedProxy.OnCollectionChanged(Object sender, NotifyCollectionChangedEventArgs e) in D:\a\_work\1\s\src\Controls\src\Core\Internals\WeakEventProxy.cs:line 78
[mono-rt] at Microsoft.Maui.Controls.MarshalingObservableCollection.OnCollectionChanged(NotifyCollectionChangedEventArgs args) in D:\a\_work\1\s\src\Controls\src\Core\Items\MarshalingObservableCollection.cs:line 49
[mono-rt] at Microsoft.Maui.Controls.MarshalingObservableCollection.Reset(NotifyCollectionChangedEventArgs args) in D:\a\_work\1\s\src\Controls\src\Core\Items\MarshalingObservableCollection.cs:line 152
[mono-rt] at Microsoft.Maui.Controls.MarshalingObservableCollection.HandleCollectionChange(NotifyCollectionChangedEventArgs args) in D:\a\_work\1\s\src\Controls\src\Core\Items\MarshalingObservableCollection.cs:line 85
[mono-rt] at Microsoft.Maui.Controls.MarshalingObservableCollection.<>c__DisplayClass8_0.<InternalCollectionChanged>b__0() in D:\a\_work\1\s\src\Controls\src\Core\Items\MarshalingObservableCollection.cs:line 65
[mono-rt] at Microsoft.Maui.Controls.DispatcherExtensions.DispatchIfRequired(IDispatcher dispatcher, Action action) in D:\a\_work\1\s\src\Controls\src\Core\DispatcherExtensions.cs:line 59
[mono-rt] at Microsoft.Maui.Controls.MarshalingObservableCollection.InternalCollectionChanged(Object sender, NotifyCollectionChangedEventArgs args) in D:\a\_work\1\s\src\Controls\src\Core\Items\MarshalingObservableCollection.cs:line 65
[mono-rt] at System.Collections.ObjectModel.ObservableCollection`1[[MauiAppRepro1.Foo, MauiAppRepro1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].OnCollectionChanged(NotifyCollectionChangedEventArgs e)
[mono-rt] at System.Collections.ObjectModel.ObservableCollection`1[[MauiAppRepro1.Foo, MauiAppRepro1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].OnCollectionReset()
[mono-rt] at System.Collections.ObjectModel.ObservableCollection`1[[MauiAppRepro1.Foo, MauiAppRepro1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].ClearItems()
[mono-rt] at System.Collections.ObjectModel.Collection`1[[MauiAppRepro1.Foo, MauiAppRepro1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]].Clear()
[mono-rt] at MauiAppRepro1.Page2.Button_Pressed(Object sender, EventArgs e) in C:\repos\GitIssues\MauiAppRepro1.App\Page2.xaml.cs:line 18
[mono-rt] at System.Threading.Tasks.Task.<>c.<ThrowAsync>b__128_0(Object state)
[mono-rt] at Android.App.SyncContext.<>c__DisplayClass2_0.<Post>b__0() in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.App/SyncContext.cs:line 36
[mono-rt] at Java.Lang.Thread.RunnableImplementor.Run() in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Java.Lang/Thread.cs:line 36
[mono-rt] at Java.Lang.IRunnableInvoker.n_Run(IntPtr jnienv, IntPtr native__this) in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/obj/Release/net8.0/android-34/mcw/Java.Lang.IRunnable.cs:line 84
[mono-rt] at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PP_V(_JniMarshal_PP_V callback, IntPtr jnienv, IntPtr klazz) in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:line 26
I just updated the repro project to 8.0.21 SR 4.1 and confirmed the bug still exists there.
Verified this issue with Visual Studio 17.10.0 Preview 5(8.0.21&8.0.20&8.0.14&8.0.10&8.0.7&8.0.40-nightly.10515+sha.d6a3c50c93-azdo.9437127). Can repro on android platform with sample project. 8.0.3 works fine.
I concur, this is happening in my carouselview. Please fix.
+1
Can you test with the latest nightly build? https://github.com/dotnet/maui/wiki/Nightly-Builds
I will test
@PureWeen Unfortunately, with the latest nightly 8.0.40-nightly.10602, this bug is still present in the repro project. Did a full clean and build. See below.
https://github.com/dotnet/maui/assets/20206332/a58f85ab-13a6-4b60-abb9-baef8af7711a
If you make Page2 Transient instead of scoped does that fix it?
https://github.com/RedZone908/CarouselVirtualViewErrorRepro/blob/master/MauiProgram.cs#L18
@PureWeen that takes away the error, but, it also makes the CarouselView start over with zero items at each page load (which makes sense, since the page is being recreated), so it's still not technically a return to the 8.0.3 behavior where the carousel items could be retained between page loads before adding/deleting any other items.
@PureWeen that takes away the error, but, it also makes the CarouselView start over with zero items at each page load (which makes sense, since the page is being recreated), so it's still not technically a return to the 8.0.3 behavior where the carousel items could be retained between page loads before adding/deleting any other items.
can you make the viewmodel and data survive longer than the scope of the page?
to be clear I'm not saying this isn't a bug :-) Just proposing some workarounds for now
Ah, gotcha! Well, for the actual proprietary company app where we encountered this bug, we actually didn't need to retain the data so our workaround was to simply instantiate a new ObservableCollection on every page load, and that did take away the error. So this particular bug definitely isn't a blocker for the product I support at this stage. It'd be nice if we didn't have to do it, sure, but it IS an easy one-line workaround, basically:
public override void OnAppearing()
{
this.MyListOfThings = new ObservableCollection() { /*your data here */ };
// can also call a method on your viewModel to do this -- just showing as OnAppearing to keep the example simple
}
I was merely pointing out the "can't retain data" piece because at this point, I was thinking beyond the scope of my application and to the broader implications of the bug itself. But yeah, on Monday I can give the repro project a longer-lived dataset. If that works, then that also would be a viable workaround for others.