MAUI–Decoupled applications using MvpVm

GitHub Project: Dotnet-maui-workshop (section 7)


Note: For this fictional application everything can be reused between Monkey and Inventory with the exception of the Population in the detail view - inventory does not have a population.

MAUI and MvpVm

The Model-View-ViewModel (Mvvm) pattern is a great reusable widget pattern,  but when it comes to building a framework/application you’ll want to use a more extensible pattern such as the Model-View-Presenter-ViewModel (MvpVm) pattern.

To understand why MvpVm is preferred, you’ll need to know some history, you’ll find that Mvvm has already been a traveled path (with MVC) requiring a new pattern to meet its short comings; thus it evolved to MVP.

Model View Controller (MVC)

The Model View Controller pattern essentially became obsolete with the emergence of smart controls; the "Controller" in MVC was replaced with controls that automatically updated the model via data binding (reference Twisting the Triad articles below)

It wasn't only the lack of a "Controller" that sparked the need for a new pattern, it was also the issues that MVC presented.   These were documented in the Taligent paper and Twisting the Triad (below). 

Model View Presenter (MVP)

A key problem, that I trust all of us have encountered with the MVVM pattern, is the inability to easily reuse a ViewModel; components become tightly coupled.   This, along with other issues, are not new; in Martin Fowlers article on GUI Architectures he addresses the Presentation Model - which is essentially MVVM.  Per GUI Architectures, the Presentation Model and Application Model are evolved MVC patterns with access to the View being the distinguishing difference.  MVP solved the issues presented by these MVC patterns (read Twisting the Triad and the Taligent paper links above for more info).

Decoupled applications

Microsoft Practices and Patterns Architecture Guide 2.0 covers, in great detail, best practices and patterns for application design.   The emphasis is on MVP as it lends itself to decoupled applications, i.e., the views and view models can truly be reusable.  AppArchGuide2.0.pdf (3.09 mb)

overall_patternR1

MvpVm?

MVP as a pattern is the tried and true pattern for applications, established by early architects and emphasized in Microsoft's best practices and patterns.  However, the emergence of WPF (and now MAUI) didn't quite fit as-is.  The pattern still applies, the only difference is there is now a ViewModel that the View observes.  As a result I coined the phrase MvpVm in my MSDN article (click image below).

In MvpVm the View declares the presenter, and the presenter is responsible for populating the view-model and managing the business logic layer.   I should emphasize that the presentation layer never accesses the data access layer directly; it only is aware of the business logic layer which in turn is only aware of data layer interfaces.  Because of these interfaces and IOC, the application can easily swap out implementation as business logic dictates.  For example, in my GitHub  Dotnet-maui-workshop (section 7) project, I swap out the offline and online implementation of IDataService; an event driven process that automatically updates if internet is disconnected.  This is demonstrated by the animated gif at the top of this article.

Decoupled applications can be difficult to follow if you are not familiar with the practices and patterns of the application framework in question.   A knowledge of Inversion of control (IOC) aka Dependency Injection (DI) is a prerequisite.   I will cover the basics of this applications pattern below.

Tip: you can look at the MvpVm pattern as a collection of puzzle pieces (reusable components) that the presenter is responsible for managing.  It has the primary responsibility for updating the view-model, you'll find that via the PresenterBase class, it invokes commands that can update the view-model as well.   This permits view-models and commands to remain decoupled (and reusable).

Note: currently I have not established a database.   When I do there will be entity business logic classes that will manage those entities.  

MvpVm_Overview

A picture says a thousand words

Below I show the two view models that are shared between the Monkey and Inventory views.   At a glance you can see why they can be easily reused.  For the DetailViewModel you'll note there is an IsPopulationVisible flag, the Inventory view does not require the population information; this flag drives its visibility (I point to this in the animated gif at the top of this article).

If we look at our MonkeyPresenter below, used by the MainPage, you'll see that the SetSupportedButtons() method defines the commands (buttons) that we want to display (pointed to below, order counts).  Ideally, we don't want to use "magic strings", however since I do not have a reference to the "GotoInventoryCommand" (which resides in a separate DLL) I have to use one.  The framework displays the buttons as shown in bottom left pane.

Each command represents a clear separation of concerns with the logic being fully encapsulated within it.  Where the buttons "can be" specified in the SetSupportedButtons() method, it is not required, just convenient. In the Inventory view you'll note (in animated gif above) that there is a button with a question mark - this button is set in XAML (there is no command for it). Above I show that the label is handling clicks and they are handled in the OnButtonClickedHandler (green arrow above).

The MainPage view shown below


Note: the flexLayout control handles the display of all commands that are supported by this presenter, e.g., Find Closest, Get data, and Inventory; they are handled by the framework in PresenterBase.

Note in the FindClosestCommand that the "ButtonText property is set to "Find Closest" which is displayed in the first button.

Pointed to above you’ll see the command has access to the view-model, it not only has access to it but also the presenter and view(s).   The ButtonEventArgs is set [by the framework] with the following:

This effectively gives the command access to everything the presenter has access to.  

With this basic understanding of the application's framework you should be able to quickly navigate its code by going to the presenter and/or command for the applicable process.  

The Monkey and Inventory apps share most, if not all, of the currently available components (commands and view-models).  We can easily swap out data access layers (offline and online) for both Monkey and Inventory using these reusable components.   

With only the presenters being tightly coupled to available components, view-models as well as views can be easily reused.

Apple M1 an "ARM-powered" device

After giving my IMac to my son I purchased the newer Mac Mini with an M1 chip so that I can deploy IPhone and Android applications.   I installed parallels and was surprised with the following message after "having" to install Windows 11 insider preview (for ARM).

So it looks like I won't be able to have my x64 / x86 Windows development environment within my Apple environment :(  This was a disappointment...

However, the power and performance of the M1 was impressive, not what I was accustomed to.  I'll be downloading Visual Studio 2022 for Mac as well as Visual Studio Code to see how far it will let me go in developing my full-stack application.     

The above link (Visual Studio on ARM-powered devices) follows:

 

No Internet–have network access

After having to crash out of DotTrace profiler I had no internet access, however I could access the network.

I found the following three steps resolved the issue:

1. Go to Settings

2. Type in Internet Options

3. On advanced tab click “Reset” button

image

EF Code First: Adding columns to table

1. Make the change to the class, e.g., below I added SortOrder property

2. Open Package Manager Console and type:
     PM> add-migration AddSortOrderFieldToTriples – Context TripleDbContext

image

This will result in the auto-generation of the AddSortOrderFieldToTriples class.   Note below that since my TripleDbContext resides in a separate assembly (not the web application) I have to set the “Default Project” to its location.   Since there are two context (ApplicationDbContext and TripleDbContext) in that assembly I have to also provide the –Context parameter.

image

Once the classes have been updated you will type in the following:

PM> update-database –verbose –Context TripleDbContext

This will apply the change to the database table as shown below:

image

ASP.NET Core–adding a second EF database context [code-first] to external project

The process is pretty much straight forward with some caveats that I’ll explain below.

First I moved the Data/Migrations and Models folder out of my web app into to my new project (figure 1).  I got an unexpected compile error - it was a rather obscure error from the _LoginPartial.cshtml.gs.s file – which is the compiled file.   To resolve this I had to update the _LoginPartial.cshtml manually since intellisense was not working in this file (figure 2).  Once I did that, and updated namespaces for the existing ApplicationUser references, my web app was compiling and running again.

SNAGHTML2a91d3df
Figure 1

image
Figure 2

I created my new TripleDbContext as well as the Triple table (see figure 1).  I then added the code pointed to below so that my new TripleDbContext will have a connection string when needed.

image

Figure 3

If you don’t have the dotnet –ef tool installed you can go to the root of your web app [command prompt] and type the following:

dotnet tool install --global dotnet-ef

After installed you’ll need need to switch to the “project that contains your xxxDbContext” and use the following command line.  Since we have more than one context, e.g., ApplicationDbContext (security) and TripleDbContext (new) we have to provide the –context or it will complain, likewise we have to specify the DbContextOptions<TripleDbContext> in the constructor for options (see figure 4).

dotnet ef migrations add "Initial" -o "Data/Migrations" --context TripleDbContext

dotnet ef database update --context TripleDbContext

Note that I provide the connection string for the OnConfiguring() function from the environment – this will only be needed when configuring the database, e.g., doing the two commands above.

image

Figure 4

With that you’ll find your database has the new database table.

image

Figure 5

ASP.NET CORE - Uncaught Syntax Error: Unexpected token ‘<’

http causes error when deployed

After getting past the 500 error when deployed to Azure successfully I thought I was out of the woods, however when I deployed to my ISP I got the unexpected token when trying to login; I was able to register without issue.

My goal, which I achieved, was to be able to deploy the same code to both Azure and my ISP.  Once deployed to my ISP I would login and it would loop back to the login screen, emptying out my login/password – no error to let me know something was wrong.  I hit F12 and see I have a 302 error with a “samesite=none”.

Disclaimer: this effort is simply to allow me to deploy and test my code.  When it comes time that I have SSL on both sites I will update the code as required to fully implement security – this is for development purposes.

image
Figure 1.


I then configured my web application (ref highlighted in figure 3) so that it emulated a deployed application by setting the environement to “Production” and disable SSL.  Then when I ran the app locally I saw more details in my Visual Studio output window.  It displays more on the “SameSite=None”.  Note: if I click on enable SSL (figure 3) I could run locally – it runs locally in SSL.  The problem is when you are running with http.

image
Figure 2


image
Figure 3.


To resolve this issue I implemented the extension from the following website:
https://www.thinktecture.com/en/identity/samesite/prepare-your-identityserver/

image
Figure 4.


NOTE: that I have one exception to their code [on line 135 below] and that is that I return true.  When false I still had the problem.

image

The extension code follows:

namespace MyWebApp.Extensions
{
using global::Microsoft.AspNetCore.Builder;
using global::Microsoft.AspNetCore.Http;
using global::Microsoft.Extensions.DependencyInjection;

namespace Microsoft.Extensions.DependencyInjection
{
public static class SameSiteCookiesServiceCollectionExtensions
{
/// <summary>
/// -1 defines the unspecified value, which tells ASPNET Core to NOT
/// send the SameSite attribute. With ASPNET Core 3.1 the
/// <seealso cref="SameSiteMode" /> enum will have a definition for
/// Unspecified.
/// </summary>
private const SameSiteMode Unspecified = (SameSiteMode)(-1);

/// <summary>
/// Configures a cookie policy to properly set the SameSite attribute
/// for Browsers that handle unknown values as Strict. Ensure that you
/// add the <seealso cref="Microsoft.AspNetCore.CookiePolicy.CookiePolicyMiddleware" />
/// into the pipeline before sending any cookies!
/// </summary>
/// <remarks>
/// Minimum ASPNET Core Version required for this code:
/// - 2.1.14
/// - 2.2.8
/// - 3.0.1
/// - 3.1.0-preview1
/// Starting with version 80 of Chrome (to be released in February 2020)
/// cookies with NO SameSite attribute are treated as SameSite=Lax.
/// In order to always get the cookies send they need to be set to
/// SameSite=None. But since the current standard only defines Lax and
/// Strict as valid values there are some browsers that treat invalid
/// values as SameSite=Strict. We therefore need to check the browser
/// and either send SameSite=None or prevent the sending of SameSite=None.
/// Relevant links:
/// - https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1
/// - https://tools.ietf.org/html/draft-west-cookie-incrementalism-00
/// - https://www.chromium.org/updates/same-site
/// - https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/
/// - https://bugs.webkit.org/show_bug.cgi?id=198181
/// </remarks>
/// <param name="services">The service collection to register <see cref="CookiePolicyOptions" /> into.</param>
/// <returns>The modified <see cref="IServiceCollection" />.</returns>
public static IServiceCollection ConfigureNonBreakingSameSiteCookies(this IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.MinimumSameSitePolicy = Unspecified;
options.OnAppendCookie = cookieContext =>
CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
options.OnDeleteCookie = cookieContext =>
CheckSameSite(cookieContext.Context, cookieContext.CookieOptions);
});

return services;
}

private static void CheckSameSite(HttpContext httpContext, CookieOptions options)
{
if (options.SameSite == SameSiteMode.None)
{
var userAgent = httpContext.Request.Headers["User-Agent"].ToString();

if (DisallowsSameSiteNone(userAgent))
{
options.SameSite = Unspecified;
}
}
}

/// <summary>
/// Checks if the UserAgent is known to interpret an unknown value as Strict.
/// For those the <see cref="CookieOptions.SameSite" /> property should be
/// set to <see cref="Unspecified" />.
/// </summary>
/// <remarks>
/// This code is taken from Microsoft:
/// https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/
/// </remarks>
/// <param name="userAgent">The user agent string to check.</param>
/// <returns>Whether the specified user agent (browser) accepts SameSite=None or not.</returns>
private static bool DisallowsSameSiteNone(string userAgent)
{
// Cover all iOS based browsers here. This includes:
// - Safari on iOS 12 for iPhone, iPod Touch, iPad
// - WkWebview on iOS 12 for iPhone, iPod Touch, iPad
// - Chrome on iOS 12 for iPhone, iPod Touch, iPad
// All of which are broken by SameSite=None, because they use the
// iOS networking stack.
// Notes from Thinktecture:
// Regarding https://caniuse.com/#search=samesite iOS versions lower
// than 12 are not supporting SameSite at all. Starting with version 13
// unknown values are NOT treated as strict anymore. Therefore we only
// need to check version 12.
if (userAgent.Contains("CPU iPhone OS 12")
|| userAgent.Contains("iPad; CPU OS 12"))
{
return true;
}

// Cover Mac OS X based browsers that use the Mac OS networking stack.
// This includes:
// - Safari on Mac OS X.
// This does not include:
// - Chrome on Mac OS X
// because they do not use the Mac OS networking stack.
// Notes from Thinktecture:
// Regarding https://caniuse.com/#search=samesite MacOS X versions lower
// than 10.14 are not supporting SameSite at all. Starting with version
// 10.15 unknown values are NOT treated as strict anymore. Therefore we
// only need to check version 10.14.
if (userAgent.Contains("Safari")
&& userAgent.Contains("Macintosh; Intel Mac OS X 10_14")
&& userAgent.Contains("Version/"))
{
return true;
}

// Cover Chrome 50-69, because some versions are broken by SameSite=None
// and none in this range require it.
// Note: this covers some pre-Chromium Edge versions,
// but pre-Chromium Edge does not require SameSite=None.
// Notes from Thinktecture:
// We can not validate this assumption, but we trust Microsofts
// evaluation. And overall not sending a SameSite value equals to the same
// behavior as SameSite=None for these old versions anyways.
if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6"))
{
return true;
}

return true;
}
}
}
}

BlogEngine.net: Startindex cannot be less than zero

Adventures on the edge

Behind the scenes the “default.aspx” is being searched for; if you put a /default.aspx at the end of the URL you will find that the page will load.

The fix is to go into the site settings and set the “Default Doc” to default.aspx.   In my case I had to manually cut the default.aspx from the last position (figure 2) and then paste it above index.aspx.  Once I did this the site started working as expected


image
Figure 1.


image
Figure 2.

ASP.NET Core - Unexpected character encountered while parsing number:

IdentityServer4.Startup: Information: Starting IdentityServer4 version 4.1.0+5a4433f83e8c6fca7d8979141fa5a92684ad56f6
Exception thrown: 'Newtonsoft.Json.JsonReaderException' in Newtonsoft.Json.dll
Unexpected character encountered while parsing number: �. Path '', line 1, position 1.

You’ll see this error if you are configured with an IdentityServer type=File (figure 1) and your environment is setup to run in “Development”; the fix is to set the ASPNETCORE_ENVIRONMENT=Production (figure 2).  Likewise you can leave the setting at “Development” but will have to copy the appsettings.json “IdentityServer” segment to the appsettings.Development.json file (removing the Key type = Development).

In figure 3 and 4 you can see that it gets confused thinking it is in Development expecting JsonConvert to deserialize the contents when in fact the path is our .pfx file.  By setting it to Production it ignores the appsettings.Development.json (more closely emulating a production deployed environment which can aid with debugging issues).

image
Figure 1.

image
Figure 2.

image
Figure 3.

image
Figure 4.