Search code examples
c#data-bindingcarouselmaui

How to display the last item of a CarouselView when it is loaded?


I'm developing an app and I'm facing an issue with the CarouselView in .NET MAUI.

When the Carousel is launched, there is the disposition (white squares are images): enter image description here

I want, when the application starts, this disposition: enter image description here

I tried some properties like PeekAreaInsets or HeightRequest but nothing changed.

This is my XAML :

<StackLayout>
    <CarouselView x:Name="carrouselView" ItemsSource="{Binding Publications}" 
                  HorizontalOptions="Start" IndicatorView="indicatorView" 
                  PeekAreaInsets="100" FlowDirection="MatchParent" 
                  HeightRequest="350" CurrentItem="{Binding CurrentPublication}"  >
        <CarouselView.ItemsLayout>
            <LinearItemsLayout Orientation="Horizontal" ItemSpacing="10" />
        </CarouselView.ItemsLayout>
        <CarouselView.ItemTemplate>
            <DataTemplate x:DataType="DTO:Publication" >
                <StackLayout HorizontalOptions="FillAndExpand">
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualState x:Name="CurrentItem">
                                <VisualState.Setters>
                                   <Setter Property="Scale" Value="1.3"/>
                                </VisualState.Setters>
                            </VisualState>
                            <VisualState x:Name="PreviousItem">
                                <VisualState.Setters>
                                    <Setter Property="Opacity" Value="0.7"/>
                                    <Setter Property="Scale" Value="0.8"/>
                                </VisualState.Setters>
                            </VisualState>
                            <VisualState x:Name="NextItem">
                                <VisualState.Setters>
                                    <Setter Property="Opacity" Value="0.7"/>
                                    <Setter Property="Scale" Value="0.8"/>
                                </VisualState.Setters>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <StackLayout Orientation="Vertical">
                      <Image Source="{Binding Image, Converter={StaticResource byteToImageSourceConverter}}" 
                             HeightRequest="300" HorizontalOptions="StartAndExpand" />
                    </StackLayout>
                    <StackLayout.GestureRecognizers>
                      <TapGestureRecognizer Command="{Binding Source={RelativeSource AncestorType={x:Type ViewModels:MainPageViewModel}}, Path=DisplayPopupCommand}" CommandParameter="{Binding .}" />
                    </StackLayout.GestureRecognizers>
                </StackLayout>
            </DataTemplate>
        </CarouselView.ItemTemplate>
    </CarouselView>
    <IndicatorView x:Name="indicatorView"
                   IndicatorColor="LightGray"
                   SelectedIndicatorColor="DarkSlateGray"
                   HorizontalOptions="Center" />
</StackLayout>

This is part of the ViewModel:

[ObservableProperty]
ObservableCollection<Publication> publications = new ObservableCollection<Publication>();

[ObservableProperty]
Publication currentPublication = new Publication();

private async Task LoadDailyPublication()
{
    var publicationSQLite = await _serviceToPublication.GetAllAsync();
    if (publicationSQLite.Any(p => DateTime.Compare(p.DateEvent, DateTime.Now.AddDays(-1)) > 0))
        Publications = new ObservableCollection<Publication>(Mapper<Model.SQLite.Publication, Model.DTO.Publication>.Map(publicationSQLite));
    else
    {
        var categoriePublication = await _serviceToCategoriePublication.GetByName("Programme de la semaine");
        List<Publication> publications = await _serviceFromPublication.GetWeeklyPublication(categoriePublication);
        await _serviceToPublication.DeleteAllWeeklyPublication(categoriePublication.CategoriePublicationId);
        await _serviceToPublication.InsertAll(Mapper<Model.DTO.Publication, Model.SQLite.Publication>.Map(publications));
        Publications = new ObservableCollection<Publication>(publications);
        CurrentPublication = Publications.ElementAt(1);
    }
}

Do you have some tips about that ?


Solution

  • There are two ways to programmatically set the currently presented item of a CarouselView:

    1. Set CurrentItem property

    This can either be done in the code-behind or in the ViewModel.

    Option A: Code-behind

    Provided that you've set the x:Name property of the CarouselView, e.g. to MyCarousel:

    XAML

    <CarouselView
        x:Name="MyCarousel"
        ItemsSource="{Binding MyItems}">
    
        <!-- ... -->
    
    </CarouselView>
    

    Code-behind

    public class SomePage : ContentPage
    {
        private readonly SomeViewModel _viewModel;
    
        public SomePage()
        {
            InitializeComponent();
            BindingContext = _viewModel = new SomeViewModel();
        }
    
        private void UpdateCurrentItem()
        {
            // note: this assumes that the data is already loaded in the ViewModel
            MyCarousel.CurrentItem = _viewModel.MyItems.Last();
        }
    }
    

    Option B: Data Binding

    It can also be done in the ViewModel and then you'll bind to the respective property, e.g. SelectedItem:

    ViewModel

    public partial class SomeViewModel : ObservableObject
    {
        [ObservableProperty]
        private ObservableCollection<SomeItem> _myItems;
    
        [ObservableProperty]
        private SomeItem _selectedItem;
    
        public async Task LoadItemsAsync()
        {
            var items = await _someApi.GetItemsAsync();
            MyItems = new ObservableCollection<SomeItems>(items);
            SelectedItem = MyItems.Last();
        }
    }
    

    In your XAML, you'll then bind to the SelectedItem:

    XAML

    <CarouselView
        ItemsSource="{Binding MyItems}"
        CurrentItem="{Binding SelectedItem}">
    
        <!-- ... -->
    
    </CarouselView>
    

    2. Set Position property

    Alternatively, you can use the index of the item instead of the object reference, which can be useful when you don't know which object to go to but you know which index you need to show.

    Again, there are the same two ways to do this:

    Option A: Code-behind

    Provided that you've set the x:Name property of the CarouselView, e.g. to MyCarousel:

    XAML

    <CarouselView
        x:Name="MyCarousel"
        ItemsSource="{Binding MyItems}">
    
        <!-- ... -->
    
    </CarouselView>
    

    Code-behind

    public class SomePage : ContentPage
    {
        private readonly SomeViewModel _viewModel;
    
        public SomePage()
        {
            InitializeComponent();
            BindingContext = _viewModel = new SomeViewModel();
        }
    
        private void UpdateCurrentItem()
        {
            // note: this assumes that the data is already loaded in the ViewModel
            MyCarousel.Position = _viewModel.Count - 1;
        }
    }
    

    Option B: Data Binding

    It can also be done in the ViewModel and then you'll bind to the respective property, e.g. SelectedIndex:

    ViewModel

    public partial class SomeViewModel : ObservableObject
    {
        [ObservableProperty]
        private ObservableCollection<SomeItem> _myItems;
    
        [ObservableProperty]
        private int _selectedIndex;
    
        public async Task LoadItemsAsync()
        {
            var items = await _someApi.GetItemsAsync();
            MyItems = new ObservableCollection<SomeItems>(items);
            SelectedIndex = MyItems.Count - 1;
        }
    }
    

    In your XAML, you'll then bind to the SelectedIndex:

    XAML

    <CarouselView
        ItemsSource="{Binding MyItems}"
        Position="{Binding SelectedIndex}">
    
        <!-- ... -->
    
    </CarouselView>
    

    Note

    At the time of writing, both ways seem to be broken.

    There are two bugs filed in the official GitHub repository:

    There is a functioning workaround, which is mentioned in issue #7575:

    If I start a new thread and add a delay of 1 second, and then I set the CurrentItem, the CarouselView scrolls correctly to the selected CurrentItem.

    In order to make this work, there apparently must be a delay of 1-2 seconds after initially setting up the View and the bindings, this can be achieved using await Task.Delay(TimeSpan.FromSeconds(2)); before setting the new CurrentItem or updating the Position property, for example.