Friday, 29 July 2022

Display a map with .NET MAUI

Many apps, whether mobile or desktop, require the ability to show a map. However, .NET MAUI doesn’t currently have a view (control) capable of displaying a map. That’s frustrating because the underlying platforms that .NET MAUI supports all largely have native map views. Android has the MapView. iOS/MacCatalyst has MKMapView. WinUI has…nothing.

So, most of the platforms .NET MAUI runs on can display a map, but .NET MAUI itself lacks a cross-platform view to display a map. Therefore I’ve created a very simple cross-platform Map view. It was written primarily for me to understand how to write handlers, rather than being a fully featured control. It displays a map, lets you scroll it, zoom it, show your location, show traffic data, and change the map imagery (street/satellite/hybrid). There’s plenty it doesn’t do - no initialising the map to a specific location, no map pins, no routes, no drawing on the map surface etc. A fully featured Map view is beyond the scope of what I was attempting to do, and besides, there will be one appearing in .NET MAUI in the not too distant future.

As it’s a simpler example than the Video view I published yesterday, I thought I’d share it.

Handler architecture

.NET MAUI has an extension mechanism, known as handlers, that you can use to customise existing .NET MAUI controls, and write your own cross-platform views whose implementations are provided by native views.

Each .NET MAUI view has an interface representation, that abstracts a cross-platform view. Cross-platform views that implement these interfaces are known as virtual views. Handlers map these virtual views to native views on each platform, and are responsible for creating the underlying native view, and mapping their API to the cross-platform control.

Handlers are accessed through their view-specific interface. This avoids the cross-platform view having to reference its handler, and the handler having to reference the cross-platform view. Each handler typically provides a property mapper, and sometimes a command mapper, that maps the cross-platform view API to the native view API.

The following diagram shows the handler architecture for the Map view:

The Map view implements the IMap interface. On iOS/MacCatalyst, the MapHandler class maps the cross-platform Map view to an iOS/MacCatalyst MKMapView. On Android, the Map view is mapped to a MapView that's provided by the Xamarin.GooglePlayServices.Maps NuGet package. There’s no Windows implementation, due to the lack of a map control on WinUI.

The PropertyMapper in the MapHandler class maps the cross-platform view properties to native view APIs via mapper methods. Each platform then provides implementations of the mapper methods, which manipulate the native view API as appropriate. The overall effect is that when a property is set on the cross-platform view, the underlying native view is updated as required.

Handler implementations on each platform must override the CreatePlatformView method, and optionally the ConnectHandler and DisconnectHandler methods. The CreatePlatformView method should return the native view that implements the cross-platform view. The ConnectHandler method should perform any required native view setup, and the DisconnectHandler method should perform any required native view cleanup. Note that the DisconnectHandler override is intentionally not invoked by .NET MAUI - you have to invoke it yourself from a suitable place in your app’s lifecycle.

Code

I’m not going to provide a walkthrough of the code. But you can download it, and step through it yourself, by cloning the repo. However, I will give you some pointers to working through the code.

The important files in the solution are highlighted below:

  • Controls - the cross-platform view implementation. IMap abstracts the Map view and exposes members that the handler needs to be able to access, and derives from .NET MAUI’s View. The Map class, which derives from .NET MAUI’s View class, provides the cross-platform implementation, and is simply a collection of BindableProperty objects.
  • Handlers - The IMapHandler interface, which derives from .NET MAUI’s IViewHandler, specifies VirtualView and PlatformView properties. The VirtualViewproperty is used to access the cross-platform view from the handler, and the PlatformView property is used to access the native view that implements the Map view. The MapHandler class is a partial class, whose platform-specific implementations are in the MapHandler.Android.cs, MapHandler.iOS.cs and MapHandler.Windows.cs files.

A handler must be registered against its cross-platform view, and this takes place in MauiProgram.cs with the ConfigureMauiHandler/AddHandler methods.

On Android you’ll need to insert your Google Map API key into the Android Manifest for the map to appear.

I hope this ends up being useful to folks on their journey to understand how to write custom controls backed by .NET MAUI handlers.

Thursday, 28 July 2022

Playing video with .NET MAUI

Many apps, whether mobile or desktop, require the ability to play video. That video may be remote, stored in the app bundle, or be chosen from the user’s device. However, .NET MAUI currently doesn’t have a view (control) capable of playing video. That’s frustrating because the underlying platforms that .NET MAUI supports all largely have native views for playing video. Android has the VideoView. iOS/MacCatalyst has AVPlayer. WinUI has…nothing yet, but I understand it’s coming soon.

So, most of the platforms .NET MAUI runs on can play video, but .NET MAUI itself lacks a cross-platform view to play video. Therefore I’ve created a cross-platform Video view. It changed name as I iterated over it. It started out as VideoPlayer, changed to VideoView, then I settled on Video, purely because .NET MAUI’s image view is called Image. Obviously the Video view plays video. Specifically, it plays video from URLs, from videos embedded in your app package (and hence embedded in your single project), and files chosen by the user on your device. As well as using the in-built transport controls to control video playback, you can provide your own transport controls. Does it play audio? Potentially but I’ve not tried it. It will most likely require some work to turn it into a Media view.

Handler architecture

.NET MAUI has an extension mechanism, known as handlers, that you can use to customise existing .NET MAUI controls, and write your own cross-platform views whose implementations are provided by native views.

Each .NET MAUI view has an interface representation, that abstracts a cross-platform view. Cross-platform views that implement these interfaces are known as virtual views. Handlers map these virtual views to native views on each platform, and are responsible for creating the underlying native view, and mapping their API to the cross-platform control.

Handlers are accessed through their view-specific interface. This avoids the cross-platform view having to reference its handler, and the handler having to reference the cross-platform view. Each handler typically provides a property mapper, and potentially a command mapper, that maps the cross-platform view API to the native view API.

The following diagram shows the handler architecture for the Video view:

The Video view implements the IVideo interface. On iOS/MacCatalyst, the VideoHandler class maps the cross-platform Video view to an iOS/MacCatalyst AVPlayer. On Android, the Video view is mapped to a VideoView. There’s currently no WinUI implementation (due to the lack of a MediaElement control on WinUI) but I’ll add one once WinUI supports playing video.

The PropertyMapper in the VideoHandler class maps the cross-platform view properties to native view APIs via mapper methods. Each platform then provides implementations of the mapper methods, which manipulate the native view API as appropriate. The overall effect is that when a property is set on the cross-platform view, the underlying native view is updated as required.

The CommandMapper in the VideoHandler class maps cross-platform view commands to native view APIs via mapper methods. Command mappers provide a way for cross-platform controls to send commands to native views on each platform. They’re similar to property mappers, but allow for additional data to be passed. Note that commands, in this context, doesn’t mean ICommand implementations. In this context, a command is just a way of invoking some functionality on a native control. For example, the ScrollView in .NET MAUI uses a command mapper so that the ScrollView asks its handler to instruct the native views to scroll to a specific location, passing along the scroll arguments (such as the position or element it wants to scroll to). The ScrollView handler on each platform unpacks the scroll arguments and invokes native view functionality to perform the desired scroll. This was analogous in Xamarin.Forms to having an event on the cross-platform view, with the renderer subscribing to the event. The advantage of the command mapper approach is that it decouples the native view from the cross-platform view, and avoids the need to unsubscribe from events. It also allows for easy customisation - the command mapper can be modified by consumers without subclassing.

Handler implementations on each platform must override the CreatePlatformView method, and optionally the ConnectHandler and DisconnectHandler methods. The CreatePlatformView method should return the native view that implements the cross-platform view. The ConnectHandler method should perform any required native view setup, and the DisconnectHandler method should perform any required native view cleanup. Note that the DisconnectHandler override is intentionally not invoked by .NET MAUI - you have to invoke it yourself from a suitable place in your app's lifecycle.

Code

I’m not going to provide a walkthrough of the code. But you can download it, and step through it yourself, by cloning the repo. However, I will give you some pointers to working through the code.

The solution is structured as follows:

The important folders in the solution are:

  • Controls - the cross-platform view implementation.

    IVideo abstracts the Video view and exposes members that the handler needs to be able to access, and derives from .NET MAUI’s IView. The Video class, which derives from .NET MAUI’s View class, provides the cross-platform implementation, and is a collection of BindableProperty objects, events, and public methods.

  • Handlers - the handler implementation.

    The IVideoHandler interface, which derives from .NET MAUI’s IViewHandler, specifies VirtualView and PlatformView properties. The VirtualView property is used to access the cross-platform view from the handler/native view layer, and the PlatformView property is used to access the native view that implements the Video view. The VideoHandler class is a partial class, whose platform-specific implementations are in the VideoHandler.Android.cs, VideoHandler.iOS.cs and VideoHandler.Windows.cs files.

  • Platforms - the native view implementations.

    Rather than implement the native views directly in the handler, I’ve split them out into native view implementations called MauiVideoPlayer. On Android, MauiVideoPlayer derives from RelativeLayout (for positioning the video on the page) and uses a VideoView to play videos (along with a MediaController for the transport controls). On Android there’s also a VideoProvider class, which is a content provider that retrieves the embedded video files from the assets folder of its bundle. On iOS/MacCatalyst, MauiVideoPlayer derives from UIView and uses an AVPlayer to play videos (along with an AVPlayerViewController for the transport controls).

  • Resources/Raw - three embedded video files.

    The video files have a build action of MauiAsset.

  • Views - pages that exercise the Video view. An event handler for the Unloaded event on each page invokes the DisconnectHandler override of the VideoHandler.

A handler must be registered against its cross-platform view, and this takes place in MauiProgram.cs with the ConfigureMauiHandler/AddHandler methods.

Next steps

There are currently two bugs I’m aware of in the implementation. Firstly, on Android the video is meant to be centred on the page, but it insists on aligning itself to the top of the page. I have suspicions for why this is happening. Secondly, on iOS, when using a Slider as a custom positioning bar, the scale of the Slider isn’t updated at runtime when setting its Maximum property. This makes it impossible right now to use a Slider to control the video’s position. I’ve pin pointed this to a bug in .NET MAUI on iOS, and logged it.

I’m planning on turning this into an official sample during August, and writing official docs on how to create custom controls using .NET MAUI handlers.