4 minute read

SkiaSharp is a 2D graphics system for .NET and C#, powered by Google’s Skia graphics engine. It’s available as a NuGet package, and can easily be added to any .NET project, particularly .NET MAUI projects.

Back in the Xamarin.Forms days we had quite extensive documentation for using SkiaSharp in Xamarin.Forms. However, while these docs are still live they aren’t easily found as all the Xamarin docs have been forcibly de-indexed by search engines. Due to resource constraints, and the size of the SkiaSharp user base, the docs were never ported to .NET MAUI. However, the Xamarin.Forms SkiaSharp documentation still largely applies to .NET MAUI. It’s just that sometimes APIs have been deprecated.

I’m not going to re-iterate all the basics of using SkiaSharp - you can find all that out in the old Xamarin.Forms docs. What I am going to discuss here is how to use SkiaSharp with MVVM in a .NET MAUI app.

Using SkiaSharp in .NET MAUI usually involves adding an SKCanvasView to your UI, and then handling it’s PaintSurface event to draw graphical objects or images. The traditional way of doing this is by adding an SKCanvasView to a XAML page:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skiasharp="clr-namespace:SkiaSharp.Views.Maui.Controls;assembly=SkiaSharp.Views.Maui.Controls"
             x:Class="Imaging.Views.MainPage">            
    <skia:SKCanvasView PaintSurface="OnCanvasViewPaintSurface" />
</ContentPage>

Then, in the code-behind you have an event handler for the PaintSurface event:

SKBitmap bitmap;

void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
    SKImageInfo info = args.Info;
    SKCanvas canvas = surface.Canvas;

    canvas.Clear();
    canvas.DrawBitmap(bitmap, info.Rect, BitmapStretch.Uniform);
}

In the example above the OnCanvasViewPaintSurface event handler draws an SKBitmap on the SKCanvasView.

A common question I’ve seen asked is how to move the code that draws on the SKCanvasView (the PaintSurface event handler) from the view to a viewmodel? There are different approaches that could be used, but my preferred approach is to sub-class the SKCanvasView control and add a BindableProperty to it that specifies a service that’s called to draw on the canvas. This service then needs implementing, injecting into a view model, and a method providing in the service that can be called from the view model to force the canvas to be re-drawn. While this approach adds considerable glue code, compared to the code-behind approach, the advantage is that it moves the code that draws on an SKCanvasView from the view to a service consumed by a viewmodel. This enables you to keep the code-behind for views largely empty, if that’s what you desire.

Sub-class the SKCanvasView

The first step is to sub-class the SKCanvasView and add a BindableProperty to it that specifies a service that’s called to draw on the canvas:

using SkiaSharp.Views.Maui.Controls;
using SkiaSharp.Views.Maui;
using Imaging.Services;

namespace Imaging.Controls;

public class MySKCanvasView : SKCanvasView
{
    public static readonly BindableProperty CanvasRendererProperty =
        BindableProperty.Create(nameof(CanvasRenderer), typeof(IBitmapRendererService), typeof(MySKCanvasView), null, defaultBindingMode: BindingMode.TwoWay,
        propertyChanged: (bindable, oldValue, newValue) =>
        {
            ((MySKCanvasView)bindable).CanvasRendererChanged((IBitmapRendererService)oldValue, (IBitmapRendererService)newValue);
        });

    public IBitmapRendererService CanvasRenderer
    {
        get { return (IBitmapRendererService)GetValue(CanvasRendererProperty); }
        set { SetValue(CanvasRendererProperty, value); }
    }

    void CanvasRendererChanged(IBitmapRendererService currentRenderer, IBitmapRendererService newRenderer)
    {
        if (currentRenderer != newRenderer)
        {
            if (currentRenderer != null)
                currentRenderer.InvalidateSurfaceRequest -= CanvasRendererInvalidateSurfaceRequest;

            if (newRenderer != null)
                newRenderer.InvalidateSurfaceRequest += CanvasRendererInvalidateSurfaceRequest;

            InvalidateSurface();
        }
    }

    void CanvasRendererInvalidateSurfaceRequest(object sender, EventArgs e)
    {
        InvalidateSurface();
    }

    protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
    {
        CanvasRenderer.PaintSurface(e.Surface, e.Info);
    }
}

In this example, the BindableProperty is called CanvasRenderer and is of type IBitmapRendererService which is a interface that specifies the operations the service will provide. When the CanvasRenderer bindable property is set to an IBitmapRendererService implementation, an event handler for the InvalidateSurfaceRequest event is registered that simply calls the SKCanvasView InvalidateSurface method to force the canvas to be re-drawn. The sub-classed control also overrides the OnPaintSurface method, which in turn calls a PaintSurface method that’s specified in the IBitmapRendererService interface.

Define the service interface

The IBitmapRendererService interface simply defines the operations that will be supported by the service that implements it:

using SkiaSharp;

namespace Imaging.Services;

public interface IBitmapRendererService
{
    SKBitmap Bitmap { get; set; }
    void PaintSurface(SKSurface surface, SKImageInfo info);
    void InvalidateSurface();
    event EventHandler InvalidateSurfaceRequest;
}

Implement the service

The BitmapRendererService implements the IBitmapRendererService interface, providing the PaintSurface implementation:

using SkiaSharp;
using Imaging.Extensions;
using CommunityToolkit.Mvvm.ComponentModel;

namespace Imaging.Services;

public class BitmapRendererService : ObservableObject, IBitmapRendererService
{
    SKBitmap _bitmap = null;
    public SKBitmap Bitmap
    {
        get => _bitmap;
        set
        {
            SetProperty(ref _bitmap, value);
            InvalidateSurfaceRequest?.Invoke(this, EventArgs.Empty);
        }
    }

    public void PaintSurface(SKSurface surface, SKImageInfo info)
    {
        SKCanvas canvas = surface.Canvas;
        canvas.Clear();

        if (_bitmap != null)
            canvas.DrawBitmap(_bitmap, info.Rect, ImageStretch.Uniform);
    }

    public void InvalidateSurface()
    {
        InvalidateSurfaceRequest?.Invoke(this, EventArgs.Empty);
    }

    public event EventHandler InvalidateSurfaceRequest;
}

The PaintSurface method is used here to draw an SKBitmap to the canvas, and uses similar code to the earlier code for the PaintSurface event handler.

Inject the service into the viewmodel

Provided that the BitmapRendererService type is registered with the MauiAppBuilder object (see the example), it can automatically be injected into viewmodels:

public partial class MainPageViewModel : ObservableObject
{
    readonly IBitmapRendererService _bitmapService;
    public IBitmapRendererService BitmapRenderer
    {
        get => _bitmapService;
    }

    public MainPageViewModel(IBitmapRendererService bitmapService)
    {
        _bitmapService = bitmapService;
    }
    ...
}

Consume the sub-classed SKCanvasView

The sub-classed SKCanvasView object can then be consumed from a page:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:controls="clr-namespace:Imaging.Controls"                
             xmlns:viewmodels="clr-namespace:Imaging.ViewModels"
             x:Class="Imaging.Views.MainPage"
             x:DataType="viewmodels:MainPageViewModel">
    <controls:MySKCanvasView CanvasRenderer="{Binding BitmapRenderer}" />
</ContentPage>

In this example, the CanvasRenderer bindable property is set to the BitmapRenderer property from MainPageViewModel, with the BindingContext of the page being set to the viewmodel in code-behind.

Then, when the Bitmap property in the BitmapRendererService object is set to an SKBitmap it will be drawn to the sub-classed SKCanvasView. In addition, in the viewmodel whenever you want to force the sub-classed SKCanvasView to be re-drawn simply call the InvalidateSurface method on the BitmapRendererService object:

_bitmapService.InvalidateSurface();

If you want to see this approach in action in a .NET MAUI app, go and check out this repo. It contains a prototype that shows how to perform 2D image processing with SkiaSharp, hosted in a .NET MAUI app that targets Mac Catalyst.

If you want to see other examples of using SkiaSharp in .NET MAUI, check out the SkiaSharp samples that I wrote while I was still at Microsoft.

Updated: