MauiAppSample

This repo hosts a Maui Application sample that uses some specific (opinionated) design patterns - MVVM (usual pattern), Observer pattern (using Reactive Extensions).

MIT License

Stars
6
Committers
1

#+TITLE: MAUI Sample Application

  • Introduction
    :PROPERTIES:
    :CUSTOM_ID: introduction
    :END:
    ,This repository contains a starting point for all your MAUI
    applications.

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.

  • What does this sample do?
    :PROPERTIES:
    :CUSTOM_ID: what-does-this-sample-do
    :END:
    Well, the sample application is really very simple - it just shows up a
    button, which when clicked, throws up some numbers on a ListView that is
    just above the button. Everytime the button is clicked, some numbers are
    added to this ListView.

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.

  • Who can use this sample?
    :PROPERTIES:
    :CUSTOM_ID: who-can-use-this-sample
    :END:
    Everyone who develops mobile applications use
    [[https://dotnet.microsoft.com/en-us/apps/maui][MAUI]]

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.

  • Generic flow of any MAUI App
    :PROPERTIES:
    :CUSTOM_ID: generic-flow-of-any-maui-app
    :END:
    Before we get into how our sample works, we'll first understand how a generic MAUI application flow is:

[[./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 both the XAML and the code-behind in the
    [[file:MauiAppSample/Pages][Pages]] folder. Make sure to derive your
    Page from [[file:MauiAppSample/Pages/BasePage.cs][BasePage]].
  • Add a ViewModel for this page in the
    [[file:MauiAppSample/ViewModels][ViewModels]] folder, with properties
    and commands that you want to bind to in the page above. The ViewModel
    must be derived from
    [[file:MauiAppSample/ViewModels/BaseViewModel.cs][BaseViewModel]].
  • Register the ViewModel for the Page in
    [[file:MauiAppSample/AppBootstrapper.cs][AppBootstrapper.cs]]
  • Add code in the code-behind of your page to do the binding (see below)
    of properties and commands to the ViewModel

** ...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:

  • All views and their code-behinds should be in the
    [[file:MauiAppSample/Views][Views]] folder
  • All views must be derived from either
    [[file:MauiAppSample/Views/BaseView.cs][BaseView]] (if top-level UI
    control) or [[file:MauiAppSample/Views/BaseViewCell.cs][BaseViewCell]]
    (if the view is for a single cell in the ListView, Table, etc.)

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:

  • Add any data models your service will consume / generate in the
    [[file:MauiAppSample/Models][Models]] folder
  • Create a base-service (as an abstract class or an interface) for the
    service in [[file:MauiAppSample/Services/Base][Services/Base]]. Make
    sure to derive this from
    [[file:MauiAppSample/Services/BaseService.cs][BaseService]]
  • Add a "mock" for the service so that you can test your service in
    [[file:MauiAppSample/Services/Mock][Services/Mock]]. Make sure to
    derive this mock service from your own base-service interface /
    abstract class created above.
  • Add the real implementation of the service directly in the
    [[file:MauiAppSample/Services][Services]] folder. Make sure to also
    derive this real service from your base service abstract class /
    interface created above
  • Register your service in
    [[file:MauiAppSample/AppConfig.cs][AppConfig.cs]]:
    • Use =RegisterConstant()= (in Splat) for registering your service
      (mock or real) as a singleton
    • Create a property to hold a global instance of your service
    • Assign your service instance to the above property using
      =GetService()= (in Splat)
  • Architecture of the sample
    :PROPERTIES:
    :CUSTOM_ID: architecture-of-the-sample
    :END:
    The sample has a simple MVVM architecture as shown below. All the Views
    are coded in GREEN, ViewModels in LIGHT BLUE, and Models in BROWN. We
    additionally have "Services" that are coded in ORANGE.

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

  • Configures all services (i.e., registers a concrete implementation of
    a service with a service interface - using the
    [[https://github.com/reactiveui/splat][Splat]] dependency-injection
    framework)
  • Registers ViewModels for all Views in the application (connecting
    pages to their view models and other custom-ui controls with their
    view models)

** 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).

  • Data Streams
    :PROPERTIES:
    :CUSTOM_ID: data-streams
    :END:
    The data generated from (or consumed by) the services are in the form of
    =IObservable<IChangeSet>=, where =T= is the type of data Model
    generated (in our case, this =T= is =Location=).

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.

  • Concurrency
    :PROPERTIES:
    :CUSTOM_ID: concurrency
    :END:
    To understand threading and concurrency issues that can crop up, go back
    to how Views, ViewModels and the Services that generate the Models work.

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:

  • One once side, Views need to respond to user-interactions almost
    real-time: when a UI control initiates an action to be performed by a
    service, it should not keep the application hanging until that action
    is complete (this will make the application unresponsive when a
    long-running service action is initiated)
  • On the other side, Services typically access external systems
    (database systems, network systems or hardware) which may take time to
    respond to the service

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.

  • Folders and files
    :PROPERTIES:
    :CUSTOM_ID: folders-and-files
    :END:
    The sample has the following folders and files (apart from the usual
    Visual Studio files):

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

  • Contact
    :PROPERTIES:
    :CUSTOM_ID: contact
    :END:
    If you liked this sample, or want to feedback,
    [[https://twitter.com/arvindd][contact me on twitter]].