.net MAUI

.NET MAUI – Exploring Overlays

This is post #3 in a series called ‘.NET MAUI Source of Truth’.

About Source Of Truth – As any developer knows, source code is the purest form of truth in working software. So I’ve decided the best way to get deep into .NET MAUI is to look at the source code.

In my last posts, we explored the .NET MAUI codebase learning about the new Windows functionality, and then managed to get a demo of them working in Preview11.

In my previous research, I noticed a new method on Window called AddOverlay. In this post I’m going to explore WindowOverlays, what are they? How is it implemented natively? How does it maintain state, including during page navigation? When should I use them? How is it related to the traditional navigation style and Shell? What can we use it for? (login screens/flows) How do we animate it?

Going back to the roots of ‘Source of Truth’ this post is going to focus on the .NET MAUI source code and look at how things are implemented.

To do this post I’ve downloaded the .NET MAUI source code from github and opened it in Visual Studio for Mac Preview.

If we start at the source we can see that IWindow has 2 methods and a property in relation to Overlays.

/// <summary>
/// Provides the ability to create, configure, show, and manage Windows.
/// </summary>
public interface IWindow : ITitledElement
{
	/// <summary>
	/// Gets the read only collection of Window Overlays on top of the Window.
	/// </summary>
	IReadOnlyCollection<IWindowOverlay> Overlays { get; }

	/// <summary>
	/// Adds a Window Overlay to the current Window.
	/// </summary>
	/// <param name="overlay"><see cref="IWindowOverlay"/>.</param>
	/// <returns>Boolean if the window overlay was added.</returns>
	bool AddOverlay(IWindowOverlay overlay);

	/// <summary>
	/// Removes a Window Overlay to the current Window.
	/// </summary>
	/// <param name="overlay"><see cref="IWindowOverlay"/>.</param>
	/// <returns>Boolean if the window overlay was removed.</returns>
	bool RemoveOverlay(IWindowOverlay overlay);

As my next step I thought I would take a look in the codebase looking to see where AddOverlay is used, (hoping we had a sample). In the search, I found a test and a sample. Using the overlay methods seems simple enough.

void TestAddOverlayWindow(object sender, EventArgs e)
{
	var window = GetParentWindow();
	overlay ??= new TestWindowOverlay(window);
	window.AddOverlay(overlay);
}

void TestRemoveOverlayWindow(object sender, EventArgs e)
{
	if (overlay is not null)
	{
		GetParentWindow().RemoveOverlay(overlay);
		overlay = null;
	}
}

Next, I’ve taken a look at how the AddOverlay method is implemented in the Window(.NET MAUI Window), Window.Impl.cs.

/// <inheritdoc/>
public bool AddOverlay(IWindowOverlay overlay)
{
	if (overlay is IVisualDiagnosticsOverlay)
		return false;

	// Add the overlay. If it's added, 
	// Initalize the native layer if it wasn't already,
	// and call invalidate so it will be drawn.
	var result = _overlays.Add(overlay);
	if (result)
	{
		overlay.Initialize();
		overlay.Invalidate();
	}

	return result;
}

It’s a simple method, the overlay gets added to the list, then initialised and invalidated. In this method, it doesn’t show how these overlays are rendered onto the screen. In order to find this out, we will need to do a bit more digging.

At this point in time, you might ask the question has this functionality even been implemented? I had the same question so I’ve taken the sample code from my previous blog, you can find here, and added some code to test. I added and overlayed the following:

public class TestWindowOverlay : WindowOverlay
{
	IWindowOverlayElement _testWindowDrawable;

	public TestWindowOverlay(Window window)
		: base(window)
	{
		_testWindowDrawable = new TestOverlayElement(this);

		AddWindowElement(_testWindowDrawable);

		EnableDrawableTouchHandling = true;
		Tapped += OnTapped;
	}

	async void OnTapped(object sender, WindowOverlayTappedEventArgs e)
	{
		if (!e.WindowOverlayElements.Contains(_testWindowDrawable))
			return;

		var window = Application.Current.Windows.FirstOrDefault(w => w == Window);

		System.Diagnostics.Debug.WriteLine($"Tapped the test overlay button.");

		var result = await window.Page.DisplayActionSheet(
			"Greetings from Visual Studio Client Experiences!",
			"Goodbye!",
			null,
			"Do something", "Do something else", "Do something... with feeling.");

		System.Diagnostics.Debug.WriteLine(result);
	}

	class TestOverlayElement : IWindowOverlayElement
	{
		readonly WindowOverlay _overlay;
		Circle _circle = new Circle(0, 0, 0);

		public TestOverlayElement(WindowOverlay overlay)
		{
			_overlay = overlay;
		}

		public void Draw(ICanvas canvas, RectangleF dirtyRect)
		{
			canvas.FillColor = Color.FromRgba(255, 0, 0, 225);
			canvas.StrokeColor = Color.FromRgba(225, 0, 0, 225);
			canvas.FontColor = Colors.Orange;
			canvas.FontSize = 40f;

			var centerX = dirtyRect.Width - 50;
			var centerY = dirtyRect.Height - 50;
			_circle = new Circle(centerX, centerY, 40);

			canvas.FillCircle(centerX, centerY, 40);
			canvas.DrawString("🔥", centerX, centerY + 10, HorizontalAlignment.Center);
		}

		public bool Contains(Point point) =>
			_circle.ContainsPoint(new Point(point.X / _overlay.Density, point.Y / _overlay.Density));

		struct Circle
		{
			public float Radius;
			public PointF Center;

			public Circle(float x, float y, float r)
			{
				Radius = r;
				Center = new PointF(x, y);
			}

			public bool ContainsPoint(Point p) =>
				p.X <= Center.X + Radius &&
				p.X >= Center.X - Radius &&
				p.Y <= Center.Y + Radius &&
				p.Y >= Center.Y - Radius;
		}
	}
}

On my iPad simulator, I was able to see the drawing. The tapped method was not working for me.

Now we know the drawing code is working but not how it is all linked together, the clues might just be IWindowOverlayElement and Draw.

If we then take a look in the MAUI codebase we can see that the Draw method also exists on the WindowOverlay, it loops over the elements and draws them. I assume (hope) there’s some type of visual invalidation hierarchy in this window overlaying.

/// <inheritdoc/>
public void Draw(ICanvas canvas, RectangleF dirtyRect)
{
	if (!IsVisible)
		return;
	foreach (var drawable in _windowElements)
		drawable.Draw(canvas, dirtyRect);
}

Also noting that I’ve discovered WindowsOverlay is a partial class and there’s a native implementation in each.

Now I need to find out how/when/why the Draw method is called.

After a bit of digging, I found some answers inside the native implementations of the WindowOverlay. What happens is that the WindowOverlay grabs the native Layer and then draws itself on top using the NativeGraphicsView to map the drawing in the Overlay to a view that can be seen natively. Below is the iOS version.

// Create a passthrough view for holding the canvas and other diagnostics tools.
_passthroughView = new PassthroughView(this, nativeWindow.RootViewController.View.Frame);

// the native graphics view calls the draw methods
_graphicsView = new NativeGraphicsView(_passthroughView.Frame, this, new DirectRenderer());
_graphicsView.AutoresizingMask = UIViewAutoresizing.All;

_passthroughView.AddSubview(_graphicsView);

...

// Any time the frame gets a new value, we need to update and invalidate the canvas.
_frameObserver = nativeLayer.AddObserver("frame", Foundation.NSKeyValueObservingOptions.New, FrameAction);
// Disable the graphics view from user input.
// This will be handled by the passthrough view.
_graphicsView.UserInteractionEnabled = false;

// Make the canvas view transparent.
_graphicsView.BackgroundColor = UIColor.FromWhiteAlpha(1, 0.0f);

// Add the passthrough view to the front of the stack.
nativeWindow.RootViewController.View.AddSubview(_passthroughView);
nativeWindow.RootViewController.View.BringSubviewToFront(_passthroughView);

// Any time the passthrough view is touched, handle it.
_passthroughView.OnTouch += UIViewOnTouch;
IsNativeViewInitialized = true;
return IsNativeViewInitialized;

It looks like the only way to use overlays is through canvas drawing, this means none of the .NET MAUI Controls or functionality will work with overlays. Some initial problems that I can see at this point, what about transitions and animations?.

As a basic requirement we need to update the UI based on state changes, I was able to do this by using the Invalidate method on the Overlay.

Device.StartTimer(TimeSpan.FromMilliseconds(500), () =>
{
	_size += 10;
	_overlay.Invalidate();
	return true;
});

So we’ve coved the basic questions, what are they and how is it implemented natively. What about the other questions?

How does it maintain state, including during page navigation? I have not tested this specifically but based on the architecture I would suggest it does maintain the state.

Do gestures pass-through to underlying views? This worked well, when I tapped where the drawing was then gestures did not pass-through but when I tapped outside of the drawing the gestures did pass-through.

How is it related to the traditional navigation style and Shell? This functionality sits completely outside of navigation and shell.

What can we use it for? In the future I would like to see it used to build outstanding user experiences, eg popovers, toast messages, alerts and slideovers.

How do we animate/transition it? Not sure exactly, I don’t think we can use any of the the animation libraries available in .NET MAUI (.NET MAUI Animations, Lottie or Native APIs).

Summary

  • Uses the Drawing capability of .NET MAUI
  • We can have multiple overlays and multiple overlay elements.
  • We can add and remove overlays
  • Overlays provide a canvas and draw method which allows you to draw on the screen.
  • No support for Controls, Animations or Transitions
  • Has the ability to invalidate and redraw

My thoughts

I think that overlays are a great idea and something that I could get excited about but in my current knowledge it seems that overlays provide a fairly limited functionality. Considering that the overlay feature needs to be supported and maintained if built, then ideally it should be a useful feature.

I think that with overlays you should be able to create something like slideoverkit and popups with animations/transitions etc, ideally it would support all the functionality create these easily or allow the extensibilty to create these.

I would suggest it will need to be able to support controls, animations and transitions.

We know that .NET MAUI is still early days so I hope that we can see this functionality developed further.

.NET MAUI Source Of Truth – Exploring IWindow

This is post #1 in a series called ‘.NET MAUI Source of Truth’.

About Source Of Truth – As any developer knows, source code is the purest form of truth in working software. So I’ve decided the best way to get deep into .NET MAUI is to look at the source code.

Exploring IWindow

Recently I was exploring the .NET MAUI codebase and came across something that seemed interesting, IWindow. ‘Windows in MAUI? that doesn’t make sense’ because Xamarin.Forms never had windows. I set about to find out what IWindow was.

A Xamarin.Forms Recap

Before we take a look at Windows in .NET MAUI let’s refresh ourselves on view hierarchies in Xamarin.Forms. In Xamarin.Forms we have an Application class with a MainPage property which takes a Page. You’re able to set the MainPage to either a single page or a navigation page.

So our hierarchies looks something like this:
Application->Page(Navigation/Page)->Page->Content

Windows in MAUI

Important Note: In this article whenever I make reference to Window/Windows most of the time it’s going to be Windows concept from .NET MAUI, not the Windows platform support of .NET MAUI.

If I look into any samples of MAUI applications then they follow the same hierarchy we find in Xamarin.Forms. So where’s the Windows?

Initially looking at the latest preview10 of .NET MAUI it seems the functionality of Windows is limited. So in order to see what Windows are going to look like in the future of .NET MAUI then we need to look at the latest source code.

If we remember that we’ve always started in Forms applications using MainPage,

eg MainPage = new ContentPage();

then let’s start with that MainPage property on the application class, FYI this is code from the MAUI github repository. If we dig into that method we can see that the MainPage still is part of the hierarchy but now Window is a parent of that page.

public Page? MainPage
{
	get
	{
		...
	}
	set
	{
		if (MainPage == value)
			return;

		OnPropertyChanging();

		if (Windows.Count == 0)
		{
			_pendingMainPage = value;
		}
		else
		{
			Windows[0].Page = value;
		}

		OnPropertyChanged();
	}
}

Now we can see that Window is a parent of Page, and Window has a property called Page.

If we look further into the implementation of the application class then we can see a new method that creates a new Window. I guess with the variable _pendingMainPage that we require some type of lazy loading.

IWindow IApplication.CreateWindow(IActivationState? activationState)
{
	Window? window = null;

	// try get the window that is pending
	if (activationState?.State?.TryGetValue(MauiWindowIdKey, out var requestedWindowId) ?? false)
	{
		if (requestedWindowId != null && _requestedWindows.TryGetValue(requestedWindowId, out var w))
			window = w;
	}

	// create a new one if there is no pending windows
	if (window == null)
	{
		window = CreateWindow(activationState);

		if (_pendingMainPage != null && window.Page != null && window.Page != _pendingMainPage)
			throw new InvalidOperationException($"Both {nameof(MainPage)} was set and {nameof(Application.CreateWindow)} was overridden to provide a page.");

		// clear out the pending main page as this will never be used again
		_pendingMainPage = null;
	}

	// make sure it is added to the windows list
	if (!_windows.Contains(window))
		AddWindow(window);

	return window;
}

Even more, is revealed if we take a look at IApplication. It looks like we will be able to OpenWindow, CloseWindow, and CreateWindow.

/// <summary>
/// Class that represents a cross-platform .NET MAUI application.
/// </summary>
public interface IApplication : IElement
{
	/// <summary>
	/// Gets the instantiated windows in an application.
	/// </summary>
	IReadOnlyList<IWindow> Windows { get; }

	/// <summary>
	/// Instantiate a new window.
	/// </summary>
	/// <param name="activationState">Argument containing specific information on each platform.</param>
	/// <returns>The created window.</returns>
	IWindow CreateWindow(IActivationState? activationState);

	void OpenWindow(IWindow window);

	/// <summary>
	/// Requests that the application closes the window.
	/// </summary>
	/// <param name="window">The window to close.</param>
	void CloseWindow(IWindow window);

	/// <summary>
	/// Notify a theme change.
	/// </summary>
	void ThemeChanged();
}

Here we can see one of the commits which is an initial implementation of Windows support: https://github.com/dotnet/maui/commit/6743036c67c4c19263ca180deb7523e0750b4820

From within .NET MAUI codebase, we can see a sample of how to use multiple windows in the .NET MAUI samples projects, look for MultiWindowPage.

public partial class MultiWindowPage : BasePage
{
	static int windowCounter = 1;

	public MultiWindowPage()
	{
		InitializeComponent();

		label.Text = "Window Count: " + (windowCounter++).ToString();
	}

	void OnNewWindowClicked(object sender, EventArgs e)
	{
		Application.Current.OpenWindow(new Window(new MultiWindowPage()));
	}

	void OnCloseWindowClicked(object sender, EventArgs e)
	{
		var window = this.GetParentWindow();
		if (window is not null)
			Application.Current.CloseWindow(window);
	}
}

It’s awesome to see that we have multiple windows going on, this is good for multiple reasons including support for Desktop Applications and IPad. This Window functionality will probably become more useful in future iOS/Android OS releases if the native OS build out the Windowing functionality further.

The current preview of .NET MAUI does not have the code changes for IWindow support so you will either need to download the .NET MAUI source or wait until we get our next preview release.

Here are the windows events all explained: https://github.com/dotnet/maui/issues/1720

Summary

  • Windows is a new concept built in .NET MAUI
  • Windows will allow you to open and close Windows
  • It looks like it will be very useful for Desktop applications

This is only a brief look at IWindow in .NET MAUI, there will be a lot more info to come as I discover more and the .NET team builds out the functionality further. I’m looking forward to sharing the knowledge. If you have any questions or need some .NET MAUI Consulting and XAM is here to help.

A First Look with FreshMvvm.Maui

Is FreshMvvm going to support .NET MAUI? FreshMvvm has over half a million downloads and it’s an extremely loved MVVM framework for Xamarin, so I think that it’s important that we put in the time to ensure FreshMvvm had a .NET MAUI version. This is a port of the FreshMvvm code base, it’s a new project. Moving forward both FreshMvvm and FreshMvvm.Maui will be supported. I must also thank Vlad Antohi who helped with the port of the codebase.

So What’s Changed?

FreshMvvm.Maui is still the FreshMvvm that you know and love. Generally, the upgrade process will be smooth and easy because from a consumer’s perspective not much has changed. We’ve made some changes internally, with the biggest change being to support the .NET dependency injection infrastructure. This means that TinyIOC has been removed and by default we use the Microsoft default dependency injection. The biggest advantage of this is you can bring in any container that supports the Microsoft Dependency Injection.

How do I get started?

1. First you’ll need to install the .NET MAUI preview.

https://xam.com.au/installing-net-maui-preview/

2. Create a new .NET Maui project from the template

dotnet new maui -n HelloFreshMauiPreview

Once you create the project, you can then open it up in Visual Studio. I’m currently on my mac and running Visual Studio 2022 Preview.

3. Add FreshMvvm.Maui from nuget

4. Add your Service, Pages and View Models.

In this case I’ve added a Database service that Mocks a database, a small quote list and quote edit. You can see all the files in the sample repo here.

5. Setup a simple navigation container for the application.

public partial class App : Application
{
 public App()
 {
  InitializeComponent();

  var page = FreshPageModelResolver.ResolvePageModel<QuoteListPageModel>();
  var basicNavContainer = new FreshNavigationContainer(page);
  MainPage = basicNavContainer;
 }
}

6. Configure the app

public static MauiApp CreateMauiApp()
{
	var builder = MauiApp.CreateBuilder();

    builder
        .UseMauiApp<App>()
        .ConfigureFonts(fonts =>
        {
            fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
        });

    builder.Services.Add(ServiceDescriptor.Singleton<IDatabaseService, DatabaseService>());

    builder.Services.Add(ServiceDescriptor.Transient<QuoteListPage, QuoteListPage>());
    builder.Services.Add(ServiceDescriptor.Transient<QuotePage, QuotePage>());

    builder.Services.Add(ServiceDescriptor.Transient<QuoteListPageModel, QuoteListPageModel>());
    builder.Services.Add(ServiceDescriptor.Transient<QuotePageModel, QuotePageModel>());

    MauiApp mauiApp = builder.Build();

    mauiApp.UseFreshMvvm();

    return mauiApp;
}

It’s that easy, we have our first FreshMvvm.Maui app running.

You can find the this simple FreshMvvm.Maui app here: https://github.com/rid00z/HelloFreshMauiPreview/