When I scroll on my CollectionView
in my .NET Maui app, the items that are loaded are duplicates of the ones that are already showing.
This is the start of the CollectionView
:
Once we scroll, we can see what should be Product 4 shows a duplicate of Product 3, and Product 5 is instead a duplicate of Product 1:
Interacting with the duplicates is just like interacting with the originals, and the objects that should be rendered stay as they are.
Here is the code - CollectionView
XAML:
<CollectionView x:Name="cartCollectionView" ZIndex="0"
ItemsSource="{Binding CartItems}"
CanReorderItems="False"
SelectionMode="None"
Margin="0, 10"
Grid.Row="0">
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Vertical" />
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate>
<Border Stroke="LightGray" Margin="0, 5" Padding="10">
<Border.StrokeShape>
<RoundRectangle CornerRadius="5" />
</Border.StrokeShape>
<Grid ColumnSpacing="10" RowSpacing="5">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Label
Text="{Binding Name}"
FontSize="18"
VerticalOptions="Center" />
</Grid>
<CollectionView Grid.Row="1" SelectionMode="Single" SelectedItem="{Binding selectedUOFM}" ItemsSource="{Binding UOFMs}" SelectionChanged="CollectionView_SelectionChanged">
<CollectionView.ItemsLayout>
<LinearItemsLayout Orientation="Horizontal" ItemSpacing="10"/>
</CollectionView.ItemsLayout>
<CollectionView.ItemTemplate>
<DataTemplate x:DataType="x:String">
<Border Stroke="DarkGray" Padding="10">
<Border.StrokeShape>
<RoundRectangle CornerRadius="20"/>
</Border.StrokeShape>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="CommonStates">
<VisualState Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="Transparent"></Setter>
</VisualState.Setters>
</VisualState>
<VisualState Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="#6174b7"></Setter>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Label Text="{Binding}" VerticalOptions="Center" HorizontalOptions="Center" />
</Border>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
<Grid Grid.Row="2" ColumnDefinitions="Auto, Auto, Auto, Auto, *">
<Label VerticalOptions="Center" Grid.Column="0" Text="Lot: "/>
<Label VerticalOptions="Center" Grid.Column="1" Text="{Binding selectedLot.lottext}"/>
<Label VerticalOptions="Center" Text="{x:Static local:AppResources.ExpirationDate}" Grid.Column="2" Margin="10,0,0,0"/>
<Picker SelectedItem="{Binding selectedLot}" x:Name="pickerLot" Grid.Column="3" WidthRequest="1" Opacity="0" Loaded="Picker_Loaded"
ItemsSource="{Binding lots}"
ItemDisplayBinding="{Binding pickertext}"
Title="{x:Static local:AppResources.SelectDateLot}"
HorizontalOptions="StartAndExpand"
SelectedIndexChanged="Picker_SelectedIndexChanged"/>
<Label Text="{Binding selectedLot.expirationDateText}" Grid.Column="3" HorizontalTextAlignment="Center" VerticalTextAlignment="Center" />
<Button Text="..." Grid.Column="4" x:Name="bPickLot" Clicked="bPickLot_Clicked" HorizontalOptions="End" IsVisible="{Binding IsPickLotVisible}" BackgroundColor="#6174b7" CornerRadius="5" HeightRequest="5"/>
</Grid>
<Grid Grid.Row="3" ColumnDefinitions="Auto, Auto, Auto, *, Auto">
<Label VerticalOptions="Center" Grid.Column="0">
<Label.FormattedText>
<FormattedString>
<Span Text="{x:Static local:AppResources.Qty}" />
<Span Text=":" />
</FormattedString>
</Label.FormattedText>
</Label>
<Entry Text="{Binding quantity}" Grid.Column="1"
Unfocused="Entry_Unfocused"
Keyboard="Numeric"
HorizontalOptions="StartAndExpand"
WidthRequest="50" />
<StackLayout Orientation="Horizontal" HorizontalOptions="Center" VerticalOptions="Center" Grid.Column="2">
<Label Text="{Binding SalePrice, StringFormat='{0:C}'}" FontSize="16" HorizontalOptions="Center" VerticalOptions="Center" />
<Label Text="{Binding Price, StringFormat='{0:C}'}" IsVisible="{Binding IsSale}" TextColor="Red" TextDecorations="Strikethrough" Margin="5,0,0,0" FontSize="16" HorizontalOptions="End" VerticalOptions="Center" />
</StackLayout>
<Label Text="{Binding ItemTotal, StringFormat='{0:C}'}" FontSize="16" Grid.Column="3" HorizontalOptions="Center" VerticalOptions="Center" />
<ImageButton Grid.Column="4"
VerticalOptions="Center"
HorizontalOptions="Center"
HeightRequest="15"
WidthRequest="15"
Source="x.png"
Clicked="ImageButton_Clicked"/>
</Grid>
</Grid>
</Border>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
This is how the items are loaded:
private async void ContentPage_NavigatedTo(object sender, NavigatedToEventArgs e)
{
await ViewModel?.LoadCartItems();
}
Relevant information from the view model:
private List<CartItem> _cartItems { get; set; }
public List<CartItem> CartItems
{
get => _cartItems;
set
{
_cartItems = value;
OnPropertyChanged(nameof(CartItems));
OnPropertyChanged(nameof(Total));
}
}
public async Task LoadCartItems()
{
CartItems = await databaseHelper.GetCartItems();
}
Database helper method GetCartItem
:
// return test objects, no need to fetch the DB to reproduce bug
public async Task<List<CartItem>> GetCartItems()
{
return new List<CartItem>()
{
new CartItem()
{
itemnmbr = "12345",
nameFR = "Produit 1",
nameEN = "Product 1",
quantity = 1,
selectedUOFM = "kg",
prices = new List<ProductPrice>
{
new ProductPrice { itemnmbr = "12345", price = 10.0m, uofm = "kg" }
},
lots = new List<LotEntity>
{
new LotEntity { itemnmbr = "12345", lot = "Lot 1", expirationDate = "2024-12-31", isSelected = true }
},
selectedLot = new LotEntity { itemnmbr = "12345", lot = "Lot 1", expirationDate = "2024-12-31", isSelected = true }
},
new CartItem()
{
itemnmbr = "12345",
nameFR = "Produit 2",
nameEN = "Product 2",
quantity = 2,
selectedUOFM = "kg",
prices = new List<ProductPrice>
{
new ProductPrice { itemnmbr = "12345", price = 20.0m, uofm = "kg" }
},
lots = new List<LotEntity>
{
new LotEntity { itemnmbr = "12345", lot = "Lot 1", expirationDate = "2024-12-31", isSelected = true }
},
selectedLot = new LotEntity { itemnmbr = "12345", lot = "Lot 1", expirationDate = "2024-12-31", isSelected = true }
},
new CartItem()
{
itemnmbr = "12345",
nameFR = "Produit 3",
nameEN = "Product 3",
quantity = 3,
selectedUOFM = "kg",
prices = new List<ProductPrice>
{
new ProductPrice { itemnmbr = "12345", price = 30.0m, uofm = "kg" }
},
lots = new List<LotEntity>
{
new LotEntity { itemnmbr = "12345", lot = "Lot 1", expirationDate = "2024-12-31", isSelected = true }
},
selectedLot = new LotEntity { itemnmbr = "12345", lot = "Lot 1", expirationDate = "2024-12-31", isSelected = true }
},
new CartItem()
{
itemnmbr = "12345",
nameFR = "Produit 4",
nameEN = "Product 4",
quantity = 4,
selectedUOFM = "kg",
prices = new List<ProductPrice>
{
new ProductPrice { itemnmbr = "12345", price = 40.0m, uofm = "kg" }
},
lots = new List<LotEntity>
{
new LotEntity { itemnmbr = "12345", lot = "Lot 1", expirationDate = "2024-12-31", isSelected = true }
},
selectedLot = new LotEntity { itemnmbr = "12345", lot = "Lot 1", expirationDate = "2024-12-31", isSelected = true }
},
new CartItem()
{
itemnmbr = "12345",
nameFR = "Produit 5",
nameEN = "Product 5",
quantity = 5,
selectedUOFM = "kg",
prices = new List<ProductPrice>
{
new ProductPrice { itemnmbr = "12345", price = 50.0m, uofm = "kg" }
},
lots = new List<LotEntity>
{
new LotEntity { itemnmbr = "12345", lot = "Lot 1", expirationDate = "2024-12-31", isSelected = true }
},
selectedLot = new LotEntity { itemnmbr = "12345", lot = "Lot 1", expirationDate = "2024-12-31", isSelected = true }
}
};
}
This is only happening on this particular CollectionView
, I'm at a loss at what is causing the problem. All tests have been made on an Android phone and an Android VM.
Update: When I remove the second CollectionView (for the UOFMs) and the picker from the DataTemplate, the list acts as it should. Adding either of these controls back makes the problem come back.
Is there a way to make it work with the controls in?
There's several issues with your CartItems implementation:
private List<CartItem> _cartItems { get; set; }
public List<CartItem> CartItems
{
get => _cartItems;
set
{
_cartItems = value;
OnPropertyChanged(nameof(CartItems));
OnPropertyChanged(nameof(Total));
}
}
It's based on INotifyPropertyChanged
, but when working with collections and CollectionView
, you should use INotifyCollectionChanged
. Ideally, the implementation should look like the example below.
public ObservableCollection<CartItem> CartItems { get; } = [ ];
By using ObservableCollection
, you get a built-in implementation of INotifyCollectionChanged
, which automatically updates the CollectionView
when you call methods like Clear
or Add
. Additionally, ObservableCollection
has a Count
property that supports INotifyPropertyChanged
, so you can simply bind to CartItems.Count
to get a live, up-to-date count of items in the cart.