├── .editorconfig
├── .gitignore
├── AsyncSearchDemo
├── App.xaml
├── App.xaml.cs
├── AssemblyInfo.cs
├── AsyncSearchDemo.csproj
├── AsyncSearchDemo.csproj.user
├── Commands
│ └── SearchCatFactsCommand.cs
├── MainWindow.xaml
├── MainWindow.xaml.cs
├── Models
│ └── CatFact.cs
├── Queries
│ └── CatFactsQuery.cs
└── ViewModels
│ └── MainViewModel.cs
├── CountdownDemo
├── CountdownDemo.csproj
└── Program.cs
├── HttpRequestDemo
├── HttpRequestDemo.csproj
├── Models
│ └── CatFact.cs
├── Program.cs
└── Queries
│ └── CatFactQuery.cs
├── PaginationDemo
├── App.xaml
├── App.xaml.cs
├── AppShell.xaml
├── AppShell.xaml.cs
├── CatFacts
│ ├── CatFact.cs
│ ├── CatFactListing.cs
│ ├── CatFactsQuery.cs
│ ├── CatFactsView.xaml
│ ├── CatFactsView.xaml.cs
│ └── CatFactsViewModel.cs
├── MauiProgram.cs
├── PaginationDemo.csproj
├── PaginationDemo.csproj.user
├── Platforms
│ ├── Android
│ │ ├── AndroidManifest.xml
│ │ ├── MainActivity.cs
│ │ ├── MainApplication.cs
│ │ └── Resources
│ │ │ └── values
│ │ │ └── colors.xml
│ ├── MacCatalyst
│ │ ├── AppDelegate.cs
│ │ ├── Info.plist
│ │ └── Program.cs
│ ├── Tizen
│ │ ├── Main.cs
│ │ └── tizen-manifest.xml
│ ├── Windows
│ │ ├── App.xaml
│ │ ├── App.xaml.cs
│ │ ├── Package.appxmanifest
│ │ └── app.manifest
│ └── iOS
│ │ ├── AppDelegate.cs
│ │ ├── Info.plist
│ │ └── Program.cs
├── Properties
│ └── launchSettings.json
├── Resources
│ ├── AppIcon
│ │ ├── appicon.svg
│ │ └── appiconfg.svg
│ ├── Fonts
│ │ ├── OpenSans-Regular.ttf
│ │ └── OpenSans-Semibold.ttf
│ ├── Images
│ │ └── dotnet_bot.svg
│ ├── Raw
│ │ └── AboutAssets.txt
│ ├── Splash
│ │ └── splash.svg
│ └── Styles
│ │ ├── Colors.xaml
│ │ └── Styles.xaml
└── Utilities
│ └── RelayCommand.cs
├── ParallelDemo
├── App.xaml
├── App.xaml.cs
├── AppShell.xaml
├── AppShell.xaml.cs
├── CatFacts
│ ├── CatFact.cs
│ ├── CatFactsObservable.cs
│ ├── CatFactsQuery.cs
│ ├── CatFactsView.xaml
│ ├── CatFactsView.xaml.cs
│ ├── CatFactsViewModel.cs
│ └── DailyCatFactQuery.cs
├── MauiProgram.cs
├── ParallelDemo.csproj
├── ParallelDemo.csproj.user
├── Platforms
│ ├── Android
│ │ ├── AndroidManifest.xml
│ │ ├── MainActivity.cs
│ │ ├── MainApplication.cs
│ │ └── Resources
│ │ │ └── values
│ │ │ └── colors.xml
│ ├── MacCatalyst
│ │ ├── AppDelegate.cs
│ │ ├── Info.plist
│ │ └── Program.cs
│ ├── Tizen
│ │ ├── Main.cs
│ │ └── tizen-manifest.xml
│ ├── Windows
│ │ ├── App.xaml
│ │ ├── App.xaml.cs
│ │ ├── Package.appxmanifest
│ │ └── app.manifest
│ └── iOS
│ │ ├── AppDelegate.cs
│ │ ├── Info.plist
│ │ └── Program.cs
├── Properties
│ └── launchSettings.json
├── Resources
│ ├── AppIcon
│ │ ├── appicon.svg
│ │ └── appiconfg.svg
│ ├── Fonts
│ │ ├── OpenSans-Regular.ttf
│ │ └── OpenSans-Semibold.ttf
│ ├── Images
│ │ └── dotnet_bot.svg
│ ├── Raw
│ │ └── AboutAssets.txt
│ ├── Splash
│ │ └── splash.svg
│ └── Styles
│ │ ├── Colors.xaml
│ │ └── Styles.xaml
└── todo.txt
├── README.md
├── StoreDemo
├── App.xaml
├── App.xaml.cs
├── AssemblyInfo.cs
├── Commands
│ └── AddGroceryListItemCommand.cs
├── MainWindow.xaml
├── MainWindow.xaml.cs
├── StoreDemo.csproj
├── StoreDemo.csproj.user
├── Stores
│ └── GroceryListStore.cs
├── ViewModels
│ ├── AddGroceryListItemViewModel.cs
│ ├── GroceryListViewModel.cs
│ └── GroceryViewModel.cs
└── Views
│ ├── AddGroceryListItemView.xaml
│ ├── AddGroceryListItemView.xaml.cs
│ ├── GroceryListView.xaml
│ ├── GroceryListView.xaml.cs
│ ├── GroceryView.xaml
│ └── GroceryView.xaml.cs
└── SystemReactiveDemo.sln
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.cs]
2 |
3 | # CS8618: Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
4 | dotnet_diagnostic.CS8618.severity = none
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vs
2 | bin
3 | obj
4 |
--------------------------------------------------------------------------------
/AsyncSearchDemo/App.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/AsyncSearchDemo/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Configuration;
4 | using System.Data;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 | using System.Windows;
8 |
9 | namespace AsyncSearchDemo
10 | {
11 | ///
12 | /// Interaction logic for App.xaml
13 | ///
14 | public partial class App : Application
15 | {
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/AsyncSearchDemo/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 |
3 | [assembly: ThemeInfo(
4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
5 | //(used if a resource is not found in the page,
6 | // or application resource dictionaries)
7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
8 | //(used if a resource is not found in the page,
9 | // app, or any theme specific resource dictionaries)
10 | )]
11 |
--------------------------------------------------------------------------------
/AsyncSearchDemo/AsyncSearchDemo.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WinExe
5 | net6.0-windows
6 | enable
7 | true
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/AsyncSearchDemo/AsyncSearchDemo.csproj.user:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Designer
7 |
8 |
9 |
10 |
11 | Designer
12 |
13 |
14 |
--------------------------------------------------------------------------------
/AsyncSearchDemo/Commands/SearchCatFactsCommand.cs:
--------------------------------------------------------------------------------
1 | using AsyncSearchDemo.Models;
2 | using AsyncSearchDemo.Queries;
3 | using AsyncSearchDemo.ViewModels;
4 | using MVVMEssentials.Commands;
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 | using System.Reactive.Linq;
9 | using System.Text;
10 | using System.Threading;
11 | using System.Threading.Tasks;
12 | using System.Windows;
13 |
14 | namespace AsyncSearchDemo.Commands
15 | {
16 | public class SearchCatFactsCommand : CommandBase
17 | {
18 | private readonly MainViewModel _viewModel;
19 | private readonly CatFactsQuery _query;
20 |
21 | private IDisposable _currentSearch;
22 |
23 | public SearchCatFactsCommand(MainViewModel viewModel, CatFactsQuery query)
24 | {
25 | _viewModel = viewModel;
26 | _query = query;
27 | }
28 |
29 | public override void Execute(object parameter)
30 | {
31 | _viewModel.IsLoading = true;
32 |
33 | _currentSearch?.Dispose();
34 | _currentSearch = Observable
35 | .FromAsync(() => _query.Execute(_viewModel.Search))
36 | .ObserveOn(SynchronizationContext.Current)
37 | .Subscribe((catFacts) =>
38 | {
39 | _viewModel.UpdateCatFacts(catFacts.Select(c => c.Content));
40 | },
41 | (error) =>
42 | {
43 | MessageBox.Show("Failed to load cat facts.", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
44 | },
45 | () =>
46 | {
47 | _viewModel.IsLoading = false;
48 | });
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/AsyncSearchDemo/MainWindow.xaml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
23 |
24 |
25 |
26 |
27 |
34 |
35 |
36 |
37 |
38 |
39 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/AsyncSearchDemo/MainWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using AsyncSearchDemo.ViewModels;
2 | using System.Windows;
3 |
4 | namespace AsyncSearchDemo
5 | {
6 | public partial class MainWindow : Window
7 | {
8 | public MainWindow()
9 | {
10 | InitializeComponent();
11 |
12 | DataContext = MainViewModel.LoadViewModel();
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/AsyncSearchDemo/Models/CatFact.cs:
--------------------------------------------------------------------------------
1 | namespace AsyncSearchDemo.Models
2 | {
3 | public class CatFact
4 | {
5 | public string Content { get; set; }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/AsyncSearchDemo/Queries/CatFactsQuery.cs:
--------------------------------------------------------------------------------
1 | using AsyncSearchDemo.Models;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Net.Http;
6 | using System.Net.Http.Json;
7 | using System.Threading.Tasks;
8 |
9 | namespace AsyncSearchDemo.Queries
10 | {
11 | public class CatFactsQuery
12 | {
13 | public async Task> Execute(string search = "")
14 | {
15 | using (HttpClient client = new HttpClient())
16 | {
17 | await Task.Delay(1000);
18 |
19 | CatFactListingResponse? response = await client.GetFromJsonAsync("https://catfact.ninja/facts?limit=332");
20 |
21 | if(response == null)
22 | {
23 | throw new Exception();
24 | }
25 |
26 | return response.Data
27 | .Select(c => new CatFact()
28 | {
29 | Content = c.Fact
30 | })
31 | .Where(c => c.Content.ToLower().Contains(search.ToLower()));
32 | }
33 | }
34 |
35 | private class CatFactListingResponse
36 | {
37 | public IEnumerable Data { get; set; }
38 | }
39 |
40 | private class CatFactResponse
41 | {
42 | public string Fact { get; set; }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/AsyncSearchDemo/ViewModels/MainViewModel.cs:
--------------------------------------------------------------------------------
1 | using AsyncSearchDemo.Commands;
2 | using AsyncSearchDemo.Queries;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Collections.ObjectModel;
6 | using System.ComponentModel;
7 | using System.Linq;
8 | using System.Reactive.Linq;
9 | using System.Threading;
10 | using System.Windows.Input;
11 |
12 | namespace AsyncSearchDemo.ViewModels
13 | {
14 | public class MainViewModel : INotifyPropertyChanged
15 | {
16 | private readonly IDisposable _disposeSearchObservable;
17 |
18 | private readonly ObservableCollection _catFacts;
19 | public IEnumerable CatFacts => _catFacts;
20 |
21 | private string _search = string.Empty;
22 | public string Search
23 | {
24 | get
25 | {
26 | return _search;
27 | }
28 | set
29 | {
30 | _search = value;
31 | OnPropertyChanged(nameof(Search));
32 | }
33 | }
34 |
35 | private bool _isLoading;
36 | public bool IsLoading
37 | {
38 | get
39 | {
40 | return _isLoading;
41 | }
42 | set
43 | {
44 | _isLoading = value;
45 | OnPropertyChanged(nameof(IsLoading));
46 | }
47 | }
48 |
49 | public ICommand SearchCatFactsCommand { get; }
50 |
51 | public event PropertyChangedEventHandler? PropertyChanged;
52 |
53 | public MainViewModel()
54 | {
55 | _catFacts = new ObservableCollection();
56 | SearchCatFactsCommand = new SearchCatFactsCommand(this, new CatFactsQuery());
57 |
58 | _disposeSearchObservable = Observable
59 | .FromEventPattern(
60 | h => PropertyChanged += h,
61 | h => PropertyChanged -= h)
62 | .Where(e => e.EventArgs.PropertyName == nameof(Search))
63 | .Throttle(TimeSpan.FromSeconds(1))
64 | .ObserveOn(SynchronizationContext.Current)
65 | .Subscribe((e) =>
66 | {
67 | SearchCatFactsCommand.Execute(null);
68 | });
69 | }
70 |
71 | public static MainViewModel LoadViewModel()
72 | {
73 | MainViewModel viewModel = new MainViewModel();
74 |
75 | viewModel.SearchCatFactsCommand.Execute(null);
76 |
77 | return viewModel;
78 | }
79 |
80 | public void UpdateCatFacts(IEnumerable catFacts)
81 | {
82 | _catFacts.Clear();
83 |
84 | foreach (string catFact in catFacts)
85 | {
86 | _catFacts.Add(catFact);
87 | }
88 | }
89 |
90 | private void OnPropertyChanged(string propertyName)
91 | {
92 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/CountdownDemo/CountdownDemo.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 | enable
7 | enable
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/CountdownDemo/Program.cs:
--------------------------------------------------------------------------------
1 | using System.Reactive.Linq;
2 |
3 | void Countdown(int seconds)
4 | {
5 | Observable
6 | .Timer(DateTimeOffset.UtcNow, TimeSpan.FromSeconds(1))
7 | .Select(currentSeconds => seconds - currentSeconds)
8 | .TakeWhile(currentSeconds => currentSeconds > 0)
9 | .Subscribe((currentSeconds) =>
10 | {
11 | Console.WriteLine(currentSeconds);
12 | },
13 | () =>
14 | {
15 | Console.WriteLine("Blast off!");
16 | });
17 | }
18 |
19 | Countdown(5);
20 |
21 | Console.ReadLine();
--------------------------------------------------------------------------------
/HttpRequestDemo/HttpRequestDemo.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 | enable
7 | enable
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/HttpRequestDemo/Models/CatFact.cs:
--------------------------------------------------------------------------------
1 | namespace HttpRequestDemo.Models
2 | {
3 | public class CatFact
4 | {
5 | public string Content { get; set; }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/HttpRequestDemo/Program.cs:
--------------------------------------------------------------------------------
1 | using HttpRequestDemo.Models;
2 | using HttpRequestDemo.Queries;
3 | using System.Reactive.Linq;
4 |
5 | CatFactQuery catFactQuery = new CatFactQuery();
6 |
7 | Observable
8 | .FromAsync(() => catFactQuery.Execute())
9 | .ValidateCatFactLength(80)
10 | .Retry(3)
11 | .Catch(Observable.Return(new CatFact() { Content = "Cats are cool." }))
12 | .Subscribe((catFact) =>
13 | {
14 | Console.WriteLine(catFact.Content);
15 | });
16 |
17 | Console.ReadLine();
18 |
19 | static class CatFactObservableExtensions
20 | {
21 | public static IObservable ValidateCatFactLength(this IObservable observable, int maxLength)
22 | {
23 | return observable
24 | .Select(catFact =>
25 | {
26 | if (catFact.Content.Length > maxLength)
27 | {
28 | return Observable.Throw(new Exception("Cat fact was too long."));
29 | }
30 |
31 | return Observable.Return(catFact);
32 | })
33 | .Switch();
34 | }
35 | };
--------------------------------------------------------------------------------
/HttpRequestDemo/Queries/CatFactQuery.cs:
--------------------------------------------------------------------------------
1 | using HttpRequestDemo.Models;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Net.Http.Json;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 |
9 | namespace HttpRequestDemo.Queries
10 | {
11 | public class CatFactQuery
12 | {
13 | public async Task Execute()
14 | {
15 | using(HttpClient client = new HttpClient())
16 | {
17 | CatFactResponse? response = await client.GetFromJsonAsync("https://catfact.ninja/fact");
18 |
19 | if(response == null)
20 | {
21 | throw new Exception();
22 | }
23 |
24 | return new CatFact()
25 | {
26 | Content = response.Fact
27 | };
28 | }
29 | }
30 |
31 | private class CatFactResponse
32 | {
33 | public string Fact { get; set; }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/PaginationDemo/App.xaml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/PaginationDemo/App.xaml.cs:
--------------------------------------------------------------------------------
1 | namespace PaginationDemo
2 | {
3 | public partial class App : Application
4 | {
5 | public App()
6 | {
7 | InitializeComponent();
8 |
9 | MainPage = new AppShell();
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/PaginationDemo/AppShell.xaml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/PaginationDemo/AppShell.xaml.cs:
--------------------------------------------------------------------------------
1 | namespace PaginationDemo
2 | {
3 | public partial class AppShell : Shell
4 | {
5 | public AppShell()
6 | {
7 | InitializeComponent();
8 | }
9 | }
10 | }
--------------------------------------------------------------------------------
/PaginationDemo/CatFacts/CatFact.cs:
--------------------------------------------------------------------------------
1 | namespace PaginationDemo.CatFacts
2 | {
3 | public class CatFact
4 | {
5 | public string Content { get; set; }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/PaginationDemo/CatFacts/CatFactListing.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace PaginationDemo.CatFacts
8 | {
9 | public class CatFactListing
10 | {
11 | public IEnumerable CatFacts { get; set; }
12 | public int Total { get; set; }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/PaginationDemo/CatFacts/CatFactsQuery.cs:
--------------------------------------------------------------------------------
1 | using System.Net.Http.Json;
2 |
3 | namespace PaginationDemo.CatFacts
4 | {
5 | public class CatFactsQuery
6 | {
7 | public async Task Execute(int offset = 0, int limit = 50, CancellationToken cancellationToken = default)
8 | {
9 | using (HttpClient client = new HttpClient())
10 | {
11 | CatFactListingResponse response = await client.GetFromJsonAsync("https://catfact.ninja/facts?limit=45", cancellationToken);
12 |
13 | if (response == null)
14 | {
15 | throw new Exception();
16 | }
17 |
18 | IEnumerable catFacts = response.Data
19 | .Skip(offset)
20 | .Take(limit)
21 | .Select(c => new CatFact()
22 | {
23 | Content = c.Fact
24 | });
25 |
26 | return new CatFactListing()
27 | {
28 | CatFacts = catFacts,
29 | Total = response.Data.Count()
30 | };
31 | }
32 | }
33 | private class CatFactListingResponse
34 | {
35 | public IEnumerable Data { get; set; }
36 | }
37 |
38 | private class CatFactResponse
39 | {
40 | public string Fact { get; set; }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/PaginationDemo/CatFacts/CatFactsView.xaml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 |
29 |
37 |
45 |
46 |
47 |
53 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/PaginationDemo/CatFacts/CatFactsView.xaml.cs:
--------------------------------------------------------------------------------
1 | namespace PaginationDemo.CatFacts
2 | {
3 | public partial class CatFactsView : ContentPage
4 | {
5 | public CatFactsView()
6 | {
7 | InitializeComponent();
8 |
9 | BindingContext = new CatFactsViewModel();
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/PaginationDemo/CatFacts/CatFactsViewModel.cs:
--------------------------------------------------------------------------------
1 | using MVVMEssentials.ViewModels;
2 | using PaginationDemo.Utilities;
3 | using System.Collections.ObjectModel;
4 | using System.ComponentModel;
5 | using System.Reactive.Linq;
6 | using System.Runtime.CompilerServices;
7 | using System.Windows;
8 | using System.Windows.Input;
9 |
10 | namespace PaginationDemo.CatFacts
11 | {
12 | public class CatFactsViewModel : ViewModelBase
13 | {
14 | private readonly CatFactsQuery _catFactsQuery;
15 |
16 | public ObservableCollection CatFacts { get; }
17 |
18 | private int _currentPage = 0;
19 | public int CurrentPage
20 | {
21 | get
22 | {
23 | return _currentPage;
24 | }
25 | set
26 | {
27 | _currentPage = value;
28 | OnPropertyChanged(nameof(CurrentPage));
29 |
30 | OnPropertyChanged(nameof(HasPreviousPage));
31 | OnPropertyChanged(nameof(HasNextPage));
32 | }
33 | }
34 |
35 | private int _currentItemsPerPage = 10;
36 | public int CurrentItemsPerPage
37 | {
38 | get
39 | {
40 | return _currentItemsPerPage;
41 | }
42 | set
43 | {
44 | _currentItemsPerPage = value;
45 | OnPropertyChanged(nameof(CurrentItemsPerPage));
46 |
47 | CurrentPage = 0;
48 | }
49 | }
50 |
51 | private int _totalItems;
52 | public int TotalItems
53 | {
54 | get
55 | {
56 | return _totalItems;
57 | }
58 | set
59 | {
60 | _totalItems = value;
61 | OnPropertyChanged(nameof(TotalItems));
62 |
63 | OnPropertyChanged(nameof(HasNextPage));
64 | }
65 | }
66 |
67 | public double TotalPages => Math.Ceiling((double)TotalItems / CurrentItemsPerPage);
68 |
69 | public bool HasPreviousPage => CurrentPage != 0;
70 | public bool HasNextPage => CurrentPage + 1 < TotalPages;
71 |
72 | public ICommand PreviousPageCommand
73 | {
74 | get => new RelayCommand