This repo hosts a Maui Application sample that uses some specific (opinionated) design patterns - MVVM (usual pattern), Observer pattern (using Reactive Extensions).
MIT License
#+TITLE: MAUI Sample Application
The best way to use this is to open the solution in your visual studio, and export the project MauiAppSample as a "Visual Studio Template" (Project -> Export Template) after selecting the project.
This button is simply labelled "Track Location" and the numbers that show up on the ListView are the latitude and longitude values of a location.
AND
Uses [[https://www.reactiveui.net/][Reactive Extensions]] (with all it's niceties such as Observables, Dynamic Data, etc.).
If either of these are NOT a target of your application's design, this sample is NOT for you.
The sample is heavily influenced by a superb sample in the ReactiveUI - [[https://github.com/reactiveui/ReactiveUI.Samples/tree/main/Xamarin/Cinephile][Cinephile]]. Cinephile is done for Xamarin, while this sample is for MAUI.
[[./img/maui-flow.drawio.svg]]
Our Sample :PROPERTIES: :CUSTOM_ID: our-sample :END: Our sample follows exactly the same flow - just that we use Reactive versions of Shell, ShellContent and ContentPage. All our classes are derived from these reactive versions.
How to... :PROPERTIES: :CUSTOM_ID: how-to :END: ** ...Add a new page? :PROPERTIES: :CUSTOM_ID: add-a-new-page :END:
** ...Add a new custom-control or a ViewCell? :PROPERTIES: :CUSTOM_ID: add-a-new-custom-control-or-a-viewcell :END: Follow the same steps as above, except:
Other steps (registration of views and their view models, binding, etc.) follows the same steps as for Pages.
** ...Add a service? :PROPERTIES: :CUSTOM_ID: add-a-service :END:
[[file:img/arch.svg]]
** Application bootstrapping :PROPERTIES: :CUSTOM_ID: application-bootstrapping :END: When the application [[file:MauiAppSample/App.xaml.cs][starts]], it [[file:MauiAppSample/AppBootstrapper.cs][bootstraps]] and then creates the [[file:MauiAppSample/Pages/MainPage.xaml][MainPage]].
Before creating the =MainPage=, it:
** Application Pages :PROPERTIES: :CUSTOM_ID: application-pages :END: The =MainPage= (and all other pages that are added to this application) derives from the [[file:MauiAppSample/Pages/BasePage.cs][BasePage]] so as to have a consistent feature access (such as logging, ViewModel associations, etc.) across all pages. As with any XAML application, =MainPage= comes with both [[file:MauiAppSample/Pages/MainPage.xaml][XAML]] and a [[file:MauiAppSample/Pages/MainPage.xaml.cs][code-behind]].Both the XAML and its code-behind form a part of the "View" in the MVVM pattern. For ease of discovery, all pages (although are also views) are placed under a dedicated folder [[file:MauiAppSample/Pages][Pages]].
** View Models :PROPERTIES: :CUSTOM_ID: view-models :END: Each page has a corresponding ViewModel with a naming scheme =ViewModel.cs=. All ViewModels are placed in the folder [[file:MauiAppSample/ViewModels][ViewModel]].The ViewModel corresponding to =MainPage= is [[file:MauiAppSample/ViewModels/MainPageViewModel.cs][MainPageViewModel]].
Similarly, a page may contain additional UI custom controls - just for keeping the [[file:MauiAppSample/Pages][Pages]] folder uncluttered, these are all added in the [[file:MauiAppSample/Views][Views]] folder. This =Views= folder too contains the custom-control's XAML file and its code-behind.
** View <-> ViewModel binding :PROPERTIES: :CUSTOM_ID: view---viewmodel-binding :END: The UI controls in the pages are bound to properties in the ViewModels, and this binding is done in the pages' code-behind. For custom-controls, this binding happens in the controls' code-behind file.
This binding uses simple Reactive Extension pattern. For example, the =MainPage= has this in the code-behind:
#+begin_example ... this.WhenActivated(disposable => { this.OneWayBind(ViewModel, vm => vm.LocationList, v => v.LstLocations.ItemsSource) .DisposeWith(disposable);
this.BindCommand(ViewModel, vm => vm.StartReadingCommand, v => v.BtnStart) .DisposeWith(disposable);
this.WhenAnyValue(vm => vm.ViewModel.StartReadingCommand) .Subscribe(); }); ... #+end_example
What you see is that specific properties in the ViewModel are bound to specific UI properties in the View using the Reactive Extensions =WhenActiviated=, =WhenAnyValue=, =OneWayBind=, and =BindCommand=. For editable UI controls, =Bind= can be used for two-way binds.
While =OneWayBind= and =Bind= are for binding with properties, =BindCommand= is for binding UI control-actions to services that perform that action. You can see above that a button in the view is bound to an action to start reading from a sensor. So:
/Views are bound to ViewModels using the Reactive Extensions in the View's code-behind./
** Services and data Model :PROPERTIES: :CUSTOM_ID: services-and-data-model :END: Services are those that generate data for (or consumes data from) ViewModels. This data that services generate or consume form the "Model" of MVVM.
There are various forms of services - those that perform a specific duty (for example, fetch weather information from a remote weather service - in this case the data Model that this service generates is the weather data), controls a car sensor (in this case, the service consumes control information from the ViewModel and uses that data to control a car-sensor).
In our case, the [[file:MauiAppSample/ViewModels/MainPageViewModel.cs][MainPageViewModel]] uses the [[file:MauiAppSample/Services/Base/LocationSensor.cs][LocationSensor]] service that generates [[file:MauiAppSample/Models/Location.cs][Location]] data (Model).
When services generate =IObservable=, it is easy to respond to data on the UI because the ViewModel can simply =Subscribe= to this =Observable= and since ViewModels are also bound to the Views, the data generated by the services is simply reflected on the Views without any more intermediate code in the ViewModel.
Also, an =IObservable<IChangeSet>= makes this even more interesting, as we now have all the [[https://www.reactiveui.net/docs/handbook/collections/][Dynamic Data]] operators at our disposal.
All operators of the Reactive Extensions [[https://reactivex.io/documentation/operators.html][can be seen here]]. These operators help in transforming data, replacing data and many other interesting data operations easy.
ViewModels basically are a link between Views and the Services that they offer to the Views. Typically, these services are either CPU-bound services (eg: calculations, data-crunching) or IO-bound (eg: reading sensor values, data transfers on network, etc.) This makes ViewModel's job tricky:
So, basically, ViewModel will have to run different parts of the data stream at different speeds. Thankfully, Reactive Extensions come with a solution to exactly this problem: it makes use of schedulers.
ViewModels use this pattern for handling this (see this code in MainPageViewModel):
#+begin_example StartReadingCommand // <-- Running on a TaskpoolScheduler .SubscribeOn(RxApp.TaskpoolScheduler) // <-- Running on a TaskpoolScheduler .ObserveOn(RxApp.TaskpoolScheduler) // <-- Running on a TaskpoolScheduler .Transform(x => new LocationViewModel(x)) // <-- Running on a TaskpoolScheduler .DisposeMany() // <-- Running on a TaskpoolScheduler .ObserveOn(RxApp.MainThreadScheduler) // <-- Running on a TaskpoolScheduler .Bind(out _locationList) // <-- Running on the main (GUI) thread .Subscribe(); // <-- Running on the main (GUI) thread #+end_example
As you can see above, once the UI has initiated an action to read, the command kick-starts a service action that responds with an =IObservable<IChangeSet>=. The actions run by ViewModel on the service (i.e., the action that =StartReadingCommand= initiates in the service =LocationSensor=) does not run in the main thread (which runs the GUI) - it runs from one of the threads in the thread-pool, so that the UI thread (main thread) is free to respond to any user-actions.
Howevever, once the data is generated by the service-thread, that data needs to be updated (i.e., bound to) a UI-element - and hence we use =.ObserveOn(RxApp.MainThreadScheduler)= to switch the context to the main-thread for data updation.
| Folder/File | Contents | |---------------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [[file:MauiAppSample/App.xaml][App.xaml]] | Application front-end | | [[file:MauiAppSample/App.xaml.cs][App.xaml.cs]] | Application front-end code-behind, our starting point | | [[file:MauiAppSample/AppBootstrapper.cs][AppBootstrapper.cs]] | Bootstrapping code that initialises the logging system, and registers various services using the =AppConfig= (below). It also connects ViewModels a Views (registers an =IViewFor=) | | [[file:MauiAppSample/AppConfig.cs][AppConfig.cs]] | Application configuration. It also "injects" a concrete implementation for services. | | [[file:MauiAppSample/Pages][Pages]] | Folder that contains both the XAML and code-behind of all the application pages. All pages derive from the =BasePage= (below). | | [[file:MauiAppSample/Pages/BasePage.cs][Pages/BasePage.cs]] | Base class for all application pages, that forces a template for using the logging system in all pages, and also connecting a page with its ViewModel | | [[file:MauiAppSample/Views][Views]] | Folder containing custom-control's XAML and their code-behind. | | [[file:MauiAppSample/Views/BaseView.cs][Views/BaseView.cs]] | All custom-control views derive from this, similar to the =BasePage=. | | [[file:MauiAppSample/Views/BaseViewCell.cs][Views/BaseViewCell.cs]] | All ViewCells (eg: data template items inside a =ListView=, etc.) derive from this | | [[file:MauiAppSample/ViewModels][ViewModels]] | Folder containing all the ViewModels of the Views and Pages. | | [[file:MauiAppSample/ViewModels/BaseViewModel.cs][ViewModels/BaseViewModel.cs]] | All ViewModels derive from this class | | [[file:MauiAppSample/Services][Services]] | Folder containing all services. | | [[file:MauiAppSample/Services/Mock][Services/Mock]] | Since services can be complex, they also need an ability to "mock" by generating fake data during the development time. All such "mock" services go here. | | [[file:MauiAppSample/Services/Base][Services/Base]] | All base-classes of individual services go here. Both the real service and the mock services derive from the base-service defined here. | | [[file:MauiAppSample/Services/BaseService.cs][Services/BaseService.cs]] | All base-services (in the [[file:MauiAppSample/Services/Base][Services/Base]] folder) derive from this class. This enables logging for all services. |