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
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