Search code examples
xamlxamarinmaui

BindablePropertys setter not called only when it is used in xaml


I'm trying to create a content view and I would like to encapsulate in it as much logic as possible to keep my code DRY but I'm not sure if I'm doing something wrong or is just a limitation on how bindable properties and thier accessors trigger the notification change event. Anyway here is my example

ContentView.xaml.cs

public static readonly BindableProperty TotalItemsProperty = BindableProperty.Create(nameof(TotalItems), typeof(int), typeof(SortingPageView),
    defaultBindingMode: BindingMode.TwoWay);

public int TotalItems
{
    get => (int)GetValue(TotalItemsProperty);
    set
    {
        SetValue(TotalItemsProperty, value);
        OnPropertyChanged(nameof(CurrentPageText));
    }
}

public string CurrentPageText { get => $" / {TotalPages}"; }

ContentView.xaml


<Grid ColumnDefinitions="50, auto, 50" Grid.Column="1" HorizontalOptions="End">
    <Image Grid.Column="0" IsVisible="{Binding CanGoBack}" Source="left_arrow.png" HeightRequest="30">
        <Image.GestureRecognizers>
            <TapGestureRecognizer Tapped="GoBack"/>
        </Image.GestureRecognizers>
    </Image>
    <HorizontalStackLayout Margin="10, 0" Grid.Column="1">
        <Entry FontSize="20" Text="{Binding CurrentPage}"/>
        <Label FontSize="20" Text="{Binding CurrentPageText}" VerticalOptions="Center"/>
    </HorizontalStackLayout>
    <Image Grid.Column="2" Source="right_arrow.png" HeightRequest="30">
        <Image.GestureRecognizers>
            <TapGestureRecognizer Tapped="GoNext"/>
        </Image.GestureRecognizers>
    </Image>
</Grid>

ViewModel.cs

public  async Task LoadJobs(bool resetCurrentPage = false)
{
    this.IsLoading = true;

    try
    {
        if(resetCurrentPage)
        {
            this.CurrentPage = 1;
        }

        var jobs = await this.jobsService.GetJobs(Filter, Pagination);

        this.InitialJobs = jobs.Items.Select(x => new JobDto(x)).ToList();
        this.TotalItems = jobs.TotalItems;   -- only this line is importent

        MainThread.BeginInvokeOnMainThread(() =>
        {
            this.Jobs.Clear();
            this.Jobs.AddRange(jobs.Items);
        });
    }
    catch(Exception ex)
    {

    }
    finally 
    { 
        this.IsLoading = false;
    }
}

Please note that the TotalItems property is not displayed in the actual ContentView.xaml Is only used to calculate other properties based of it but it's setter is never called (had a breakpoint on it) only when the TotalItems property has in the xaml, so am I doing something wrong here?

PS ContentView.xaml.cs Constructor

    public SortingPageView()
    {
        InitializeComponent();
        Content.BindingContext = this;
    }

UPDATE

The view model is not for the contentView, it so for the view in witch I want to display the content view.

And from what I understand I need to define a BindableProperty with a accessero property in the content view and in the view model I have that totalItems property that I want to pass down. Kinda like in vue, angular


Solution

  • Ok so I think that the problem was either in the way I did my bindings in view model or that I didn't add in the conentView.xaml the

    BindingContext="{x:Reference this}"
    

    on the root element in the contentView in my case grid

    <ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 x:Name="this"
                 x:Class="MedMark.Components.PagingFunctionality">
    
        <Grid BindingContext="{x:Reference this}" ColumnDefinitions="50, auto, 50" Grid.Column="1" HorizontalOptions="End">
    

    you can see that this is the actual content view and contrary to what I read that you should bind to the binding context of the content view I still did it and it works as expected ContentView.xaml.cs

    public partial class PagingFunctionality : ContentView
    {
        public PagingFunctionality()
        {
            InitializeComponent();
            Content.BindingContext = this;
        }
    }
    

    Ok now I'll just paste some code to work as kinda of a template for others (or my future self :D)

    ContentView.xaml.cs

    public partial class PagingFunctionality : ContentView
    {
        public PagingFunctionality()
        {
            InitializeComponent();
            Content.BindingContext = this;
        }
    
        public static readonly BindableProperty TotalPagesProperty = BindableProperty.Create(nameof(TotalPages), typeof(int), typeof(PagingFunctionality), 0,
            BindingMode.OneWay, propertyChanged: TotalPagesChanged);
    
        public static readonly BindableProperty CurrentPageProperty = BindableProperty.Create(nameof(CurrentPage), typeof(int), typeof(PagingFunctionality), 0,
            BindingMode.TwoWay, propertyChanged: CurrentPageChanged);
    
    
        private static void TotalPagesChanged(BindableObject bindable, object oldValue, object newValue)
        {
            var view = bindable as PagingFunctionality;
            view.NotifyPagingDetails();
        }
    
        private static void CurrentPageChanged(BindableObject bindable, object oldValue, object newValue)
        {
            var view = bindable as PagingFunctionality;
            view.NotifyPagingDetails();
        }
    
    
        public int TotalPages 
        { 
            get => (int)GetValue(TotalPagesProperty);
            set => SetValue(TotalPagesProperty, value);
        }
    
        public int CurrentPage
        {
            get => (int)GetValue(CurrentPageProperty);
            set => SetValue(CurrentPageProperty, value);
        }
    
        public bool CanGoBack { get => this.CurrentPage > 1; }
        
        public bool CanGoNext { get => this.CurrentPage < this.TotalPages; }
    
        private void NotifyPagingDetails()
        {
            OnPropertyChanged(nameof(CanGoNext));
            OnPropertyChanged(nameof(CanGoBack));
        }
    
        private void GoBack(object sender, TappedEventArgs e)
        {
            if (CanGoBack)
            {
                this.CurrentPage--;
            }
        }
    
        private void GoNext(object sender, TappedEventArgs e)
        {
            if (CanGoNext)
            {
                this.CurrentPage++;
            }
        }
    }
    

    ContentView.xaml

    <ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 x:Name="this"
                 x:Class="MedMark.Components.PagingFunctionality">
    
        <Grid BindingContext="{x:Reference this}" ColumnDefinitions="50, auto, 50" Grid.Column="1" HorizontalOptions="End">
            <Image 
                Grid.Column="0"
                IsVisible="{Binding CanGoBack}" 
                Source="left_arrow.png" HeightRequest="30">
                <Image.GestureRecognizers>
                    <TapGestureRecognizer Tapped="GoBack"/>
                </Image.GestureRecognizers>
            </Image>
            <HorizontalStackLayout Margin="10, 0" Grid.Column="1">
                <Entry x:Name="CurrentPageLabel" Text="{Binding CurrentPage}" FontSize="20"/>
                <Label x:Name="TotalPageLabel" Text="{Binding TotalPages, StringFormat=' / {0}'}" FontSize="20" VerticalOptions="Center"/>
            </HorizontalStackLayout>
            <Image 
                Grid.Column="2" 
                IsVisible="{Binding CanGoNext}"
                Source="right_arrow.png" HeightRequest="30">
                <Image.GestureRecognizers>
                    <TapGestureRecognizer Tapped="GoNext"/>
                </Image.GestureRecognizers>
            </Image>
        </Grid>
    </ContentView>
    

    JobsView.xaml

                <components:PagingFunctionality
                    Grid.Column="1"
                    TotalPages="{Binding TotalPages}"
                    CurrentPage="{Binding CurrentPage}"/>
    

    JobsViewModel.cs

        [ObservableProperty]
        [NotifyPropertyChangedFor(nameof(TotalPages))]
        private int totalItems;
    
        [ObservableProperty]
        private int currentPage = 1;
    
        public int TotalPages { get => this.TotalItems % this.Pagination.PageSize == 0 ? this.TotalItems / this.Pagination.PageSize : this.TotalItems / this.Pagination.PageSize + 1; }
    
        partial void OnCurrentPageChanged(int oldValue, int newValue)
        {
            this.Pagination.CurrentPageNo = newValue;
    
            Task.Run(async () => await LoadJobs(false));
        }
    

    As CurrentPageProperty in ContentView.xaml.cs has a BindingMode = TwoWay, here in the viewModel, I listen for changes in CurrentPage to load the according items.

    Note that the logic for going to the next page is validated and triggered in the ContentView by the properties CanGoNext and CanGoBack and methods (that are bound to the tap event) GoNext and GoBack