windowsmauicarouselmaui-community-toolkit

.NET MAUI CarouselView Decrementing Position Malfunction on Windows


I'm currently working on implementing a CarouselView in .NET MAUI and want the position be changed by buttons rather than scrolling.

Incrementing works just fine, decrementing however seems to malfunction.

CarouselView in View:

<CarouselView Grid.Row="2"
              ItemsSource="{Binding Questions}"
              HorizontalScrollBarVisibility="Never"
              IndicatorView="indicatorView"
              Position="{Binding CarouselViewPosition}"
              EmptyView="No items to display."
              IsBounceEnabled="False"
              Loop="False">
    <CarouselView.ItemsLayout>
        <LinearItemsLayout Orientation="Horizontal"
                           SnapPointsType="MandatorySingle"
                           SnapPointsAlignment="Center" />
    </CarouselView.ItemsLayout>

    <CarouselView.ItemTemplate>
        <DataTemplate>
            <Frame BorderColor="DarkBlue">
                <Label Text="{Binding QuestionContent}" />
            </Frame>
        </DataTemplate>
    </CarouselView.ItemTemplate>
</CarouselView>

Code in ViewModel:

private int _carouselViewPosition = 0;
public int CarouselViewPosition
{
    get { return _carouselViewPosition; }

    set
    {
        var stackFrame = new StackFrame(1);
        var method = stackFrame.GetMethod();
        Debug.WriteLine($"[set_CarouselViewPosition] Set method called from {method.Name}");

        _carouselViewPosition = value;
        OnPropertyChanged(nameof(CarouselViewPosition));
    }
}

// ...

        [RelayCommand(CanExecute = nameof(CanGoNextQuestion))]
        private void OnNextQuestion()
        {
            Debug.WriteLine($"[OnNextQuestion] About to increment the position in carousel: {CarouselViewPosition}");
            CarouselViewPosition += 1;
            Debug.WriteLine($"[OnNextQuestion] Incremented position in carousel: {CarouselViewPosition}");
        }

        [RelayCommand(CanExecute = nameof(CanGoPreviousQuestion))]
        private void OnPreviousQuestion()
        {
            Debug.WriteLine($"[OnPreviousQuestion] About to decrement the position in carousel: {CarouselViewPosition}");
            CarouselViewPosition = CarouselViewPosition - 1;
            Debug.WriteLine($"[OnPreviousQuestion] Decremented position in carousel: {CarouselViewPosition}");
        }

// ...

        private bool CanGoNextQuestion()
        {
            Debug.WriteLine($"[CanGoNextQuestion] Position in carousel: {CarouselViewPosition}");
            if (CarouselViewPosition < (Questions.Count() - 1))
                return true;
            else
                return false;

        }

        private bool CanGoPreviousQuestion()
        {
            Debug.WriteLine($"[CanGoPreviousQuestion] Position in carousel: {CarouselViewPosition}");
            if (CarouselViewPosition > 0)
                return true;
            else
                return false;
        }

I've tried to follow the value with Debug.WriteLine.

The output is the following when moving up two times:

[OnNextQuestion] About to increment the position in carousel: 0
[set_CarouselViewPosition] Set method called from OnNextQuestion
[set_CarouselViewPosition] Set method called from InvokeMethod
[OnNextQuestion] Incremented position in carousel: 1
[set_CarouselViewPosition] Set method called from InvokeStub_InspectChapterViewModel.set_CarouselViewPosition
[set_CarouselViewPosition] Set method called from InvokeStub_InspectChapterViewModel.set_CarouselViewPosition

[OnNextQuestion] About to increment the position in carousel: 1
[set_CarouselViewPosition] Set method called from OnNextQuestion
[set_CarouselViewPosition] Set method called from InvokeStub_InspectChapterViewModel.set_CarouselViewPosition
[OnNextQuestion] Incremented position in carousel: 2
[set_CarouselViewPosition] Set method called from InvokeStub_InspectChapterViewModel.set_CarouselViewPosition
[set_CarouselViewPosition] Set method called from InvokeStub_InspectChapterViewModel.set_CarouselViewPosition

Now, two problems:

  1. On the one hand, I can't decrement because the Button is blocked...? Why? The CarouselViewPosition is larger than 0, isn't it??

  2. If I deactivate the checking on CanGoPreviousQuestion by always returning true, I receive the following code output after trying the decrement two times:

[OnPreviousQuestion] About to decrement the position in carousel: 2
[set_CarouselViewPosition] Set method called from OnPreviousQuestion
[set_CarouselViewPosition] Set method called from InvokeStub_InspectChapterViewModel.set_CarouselViewPosition
[OnPreviousQuestion] Decremented position in carousel: 1
[set_CarouselViewPosition] Set method called from InvokeStub_InspectChapterViewModel.set_CarouselViewPosition

[OnPreviousQuestion] About to decrement the position in carousel: 2
[set_CarouselViewPosition] Set method called from OnPreviousQuestion
[set_CarouselViewPosition] Set method called from InvokeStub_InspectChapterViewModel.set_CarouselViewPosition
[OnPreviousQuestion] Decremented position in carousel: 1
[set_CarouselViewPosition] Set method called from InvokeStub_InspectChapterViewModel.set_CarouselViewPosition

Does anybody understand, what is happening here?

UPDATE: After applying suggestions, the code is the following:

In ViewModel:

[ObservableProperty]
private ObservableCollection<Question> _questions = new();

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(NextQuestionCommand))]
[NotifyCanExecuteChangedFor(nameof(PreviousQuestionCommand))]
private int _carouselViewPosition = 0;

partial void OnCarouselViewPositionChanged(int oldValue, int newValue)
{
    var stackFrame = new StackFrame(1);
    var method = stackFrame.GetMethod();
    Debug.WriteLine($"[set_CarouselViewPosition] Set method called from {method.Name}");
}

// ...

[RelayCommand(CanExecute = nameof(CanGoNextQuestion))]
private void OnNextQuestion()
{
    Debug.WriteLine($"[OnNextQuestion] About to increment the position in carousel: {CarouselViewPosition}");
    CarouselViewPosition += 1;
    Debug.WriteLine($"[OnNextQuestion] Incremented position in carousel: {CarouselViewPosition}");
}

[RelayCommand(CanExecute = nameof(CanGoPreviousQuestion))]
private void OnPreviousQuestion()
{
    Debug.WriteLine($"[OnPreviousQuestion] About to decrement the position in carousel: {CarouselViewPosition}");
    CarouselViewPosition -= 1;
    Debug.WriteLine($"[OnPreviousQuestion] Decremented position in carousel: {CarouselViewPosition}");
}

private bool CanGoNextQuestion()
{
    Debug.WriteLine($"[CanGoNextQuestion] Position in carousel: {CarouselViewPosition}");
    if (CarouselViewPosition < (Questions.Count() - 1))
        return true;
    else
        return false;

}

private bool CanGoPreviousQuestion()
{
    Debug.WriteLine($"[CanGoPreviousQuestion] Position in carousel: {CarouselViewPosition}");
    if (CarouselViewPosition > 0)
        return true;
    else
        return false;
}

UPDATE 2: I've tried to follow a new implementation of CarouselView an another project, however the problem persists.

The file structure is the following:

SampleMauiApp
|-- [ViewModels]
     |-- MainViewModel.cs
|-- [Views]
     |-- MainPage.xaml
     |-- MainPage.xaml.cs
|-- App.xaml
|-- App.xaml.cs
|-- AppShell.xaml
|-- AppShell.xaml.cs
|-- MauiProgram.cs

Source Code(s):

MainViewModel.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Diagnostics;

namespace SampleMauiApp.ViewModels
{ 
    public partial class MainViewModel : ObservableObject
    {

        public class QuestionModel
        {
            public string QuestionContent { get; set; } = "";
        }

        [ObservableProperty]
        private ObservableCollection<QuestionModel> _questions = new();

        public MainViewModel()
        {
            Questions.Add(new QuestionModel() { QuestionContent = "Ques No.1"});
            Questions.Add(new QuestionModel() { QuestionContent = "Ques No.2" });
            Questions.Add(new QuestionModel() { QuestionContent = "Ques No.3" });
            Questions.Add(new QuestionModel() { QuestionContent = "Ques No.4" });
            Questions.Add(new QuestionModel() { QuestionContent = "Ques No.5" });
        }

        [ObservableProperty]
        [NotifyCanExecuteChangedFor(nameof(NextQuestionCommand))]
        [NotifyCanExecuteChangedFor(nameof(PreviousQuestionCommand))]
        private int _carouselViewPosition = 0;

        partial void OnCarouselViewPositionChanged(int oldValue, int newValue)
        {
            var stackFrame = new StackFrame(1);
            var method = stackFrame.GetMethod();
            Debug.WriteLine($"[set_CarouselViewPosition] Set method called from {method.Name}");
        }

        [RelayCommand(CanExecute = nameof(CanGoNextQuestion))]
        private void OnNextQuestion()
        {
            Debug.WriteLine($"[OnNextQuestion] About to increment the position in carousel: {CarouselViewPosition}");
            CarouselViewPosition += 1;
            Debug.WriteLine($"[OnNextQuestion] Incremented position in carousel: {CarouselViewPosition}");
        }

        [RelayCommand(CanExecute = nameof(CanGoPreviousQuestion))]
        private void OnPreviousQuestion()
        {
            Debug.WriteLine($"[OnPreviousQuestion] About to decrement the position in carousel: {CarouselViewPosition}");
            CarouselViewPosition -= 1;
            Debug.WriteLine($"[OnPreviousQuestion] Decremented position in carousel: {CarouselViewPosition}");
        }

        private bool CanGoNextQuestion()
        {
            Debug.WriteLine($"[CanGoNextQuestion] Position in carousel: {CarouselViewPosition}");
            if (CarouselViewPosition < (Questions.Count() - 1))
                return true;
            else
                return false;
        }

        private bool CanGoPreviousQuestion()
        {
            Debug.WriteLine($"[CanGoPreviousQuestion] Position in carousel: {CarouselViewPosition}");
            if (CarouselViewPosition > 0)
                return true;
            else
                return false;
        }
    }
}

MainPage.xaml:

<?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:ViewModels="clr-namespace:SampleMauiApp.ViewModels"
             x:Class="SampleMauiApp.Views.MainPage"
             Title="CarouselView Test">
    <ContentPage.BindingContext>
        <ViewModels:MainViewModel x:Name="ViewModels" />
    </ContentPage.BindingContext>
    <Grid RowDefinitions="*,Auto">
        <CarouselView Grid.Row="0"
                      ItemsSource="{Binding Questions}"
                      HorizontalScrollBarVisibility="Never"
                      Position="{Binding CarouselViewPosition}"
                      EmptyView="No items to display."
                      IsBounceEnabled="False"
                      Loop="False">
            <CarouselView.ItemsLayout>
                <LinearItemsLayout Orientation="Horizontal"
                                   SnapPointsType="MandatorySingle"
                                   SnapPointsAlignment="Center" />
            </CarouselView.ItemsLayout>

            <CarouselView.ItemTemplate>
                <DataTemplate>
                    <Frame BorderColor="DarkBlue">
                        <Label Text="{Binding QuestionContent}" />
                    </Frame>
                </DataTemplate>
            </CarouselView.ItemTemplate>
        </CarouselView>

        <HorizontalStackLayout Grid.Row="1"
                               HorizontalOptions="Center">
            <Button Text="OnPreviousQuestion"
                    Command="{Binding PreviousQuestionCommand}" />
            <Button Text="OnNextQuestion"
                    Command="{Binding NextQuestionCommand}" />
        </HorizontalStackLayout>
    </Grid>
</ContentPage>

MainPage.xaml.cs:

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using SampleMauiApp.ViewModels;

namespace SampleMauiApp.Views
{
    public partial class MainPage : ContentPage
    {
        public MainPage()
        {
            InitializeComponent();
        }
    }
}

App.xaml:

<?xml version = "1.0" encoding = "UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:SampleMauiApp"
             x:Class="SampleMauiApp.App">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Resources/Styles/Colors.xaml" />
                <ResourceDictionary Source="Resources/Styles/Styles.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

App.xaml.cs:

namespace SampleMauiApp
{
    public partial class App : Application
    {
        public App()
        {
            InitializeComponent();

            MainPage = new AppShell();
        }
    }
}

AppShell.xaml:

<?xml version="1.0" encoding="UTF-8" ?>
<Shell
    x:Class="SampleMauiApp.AppShell"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:Views="clr-namespace:SampleMauiApp.Views"
    Shell.FlyoutBehavior="Disabled"
    Title="SampleMauiApp">

    <ShellContent
        Title="Home"
        ContentTemplate="{DataTemplate Views:MainPage}"
        Route="MainPage" />
</Shell>

AppShell.xaml.cs:

namespace SampleMauiApp
{
    public partial class AppShell : Shell
    {
        public AppShell()
        {
            InitializeComponent();
        }
    }
}

MauiProgram.cs:

using Microsoft.Extensions.Logging;
using CommunityToolkit.Maui;
using CommunityToolkit.Mvvm;

namespace SampleMauiApp
{
    public static class MauiProgram
    {
        public static MauiApp CreateMauiApp()
        {
            var builder = MauiApp.CreateBuilder();
            builder
                .UseMauiApp<App>()
                .UseMauiCommunityToolkit()
                .ConfigureFonts(fonts =>
                {
                    fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                    fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
                });

#if DEBUG
            builder.Logging.AddDebug();
#endif

            return builder.Build();
        }
    }
}

UPDATE 3: Here is a gif of the problem: CarouselView Malfunction


Solution

  • I can reproduce this issue. Seems the Position Property doesn't work as expected.

    The First workaround is that you may try using CurrentItem Property. Just make small changes to your code. Consider the following code,

    this is xaml file,

    <CarouselView Grid.Row="0" x:Name="mycarousel"
                  ItemsSource="{Binding Questions}"
                  CurrentItem="{Binding CurrentItem}"
                  HorizontalScrollBarVisibility="Never"
                  ...                                 
                  >
    

    this is ViewModel. Instead of using Position we use CurrentItem Property,

        [ObservableProperty]
        [NotifyCanExecuteChangedFor(nameof(NextQuestionCommand))]
        [NotifyCanExecuteChangedFor(nameof(PreviousQuestionCommand))]
        private QuestionModel currentItem ;
    
        [RelayCommand(CanExecute = nameof(CanGoNextQuestion))]
        private void OnNextQuestion()
        {
            var preItem = Questions.IndexOf(CurrentItem);
            CurrentItem = Questions[preItem+1];
        }
    
        [RelayCommand(CanExecute = nameof(CanGoPreviousQuestion))]
        private void OnPreviousQuestion()
        {
            var preItem = Questions.IndexOf(CurrentItem);
            CurrentItem = Questions[preItem - 1];
        }
    

    And this works as expected.

    The Second one is using CarouselView scrollto method. It's easy to implement but it's not a MVVM way.

        private void PreButton_Clicked(object sender, EventArgs e)
        {                
            mycarousel.ScrollTo(mycarousel.Position - 1);
        }
    
        private void NextButton_Clicked(object sender, EventArgs e)
        {
            mycarousel.ScrollTo(mycarousel.Position + 1);
        }
    

    By the way, since you have submitted an issue on GitHub, you may also follow the progress of this issue and get the latest updates.

    Hope it helps!