doc/articles/guides/xf-migration/custom-drawn-controls.md
This guide explores how to migrate custom-drawn controls from Xamarin.Forms to Uno Platform. Custom-drawn controls use low-level graphics APIs to render custom visuals, and the good news is that the most common approach – using SkiaSharp – works seamlessly with Uno Platform.
While there are many ways to create custom-drawn controls, the main implementation you'll encounter in multi-platform Xamarin.Forms controls is SkiaSharp. SkiaSharp is a cross-platform .NET API based on the Skia 2D graphics library. Skia was created by Google but is available on all mainstream platforms, allowing you to create drawing code that works across devices regardless of their specific implementation.
When building a custom control for Xamarin.Forms, you use the SKCanvasView. This provides a view that contains the OnPaintSurface method, which you implement in your derived class to draw to the screen. The method arguments provide access to the SKSurface, which represents the raw surface you can draw to.
The SKCanvasView handles resizing and calls your OnPaintSurface method to redraw as required. Your code considers the size of the surface, as you should expect this to vary between devices or at runtime due to screen rotation or moveable UI elements.
The great news is that the SkiaSharp project already has support for Uno Platform through the SkiaSharp.Views.Uno NuGet package.
The SKXamlCanvas control is the equivalent to the SKCanvasView from Xamarin.Forms, but it inherits from the WinUI Canvas class. This control:
OnPaintSurface method where you can draw to the surfaceBecause of this, you probably don't need to change any of the SkiaSharp drawing code from an existing control – you simply:
SKXamlCanvas[!NOTE] While
SKXamlCanvasis the most straightforward option for migrating existing Xamarin.Forms controls (because its API closely matchesSKCanvasView), the Uno Platform documentation recommends usingSKCanvasElementon Skia-based targets when possible.SKCanvasElementis designed for Uno's Skia renderers, providing hardware acceleration and avoiding additional buffer copies, which can improve performance on platforms such as WebAssembly, Skia GTK, and Skia WPF.
Microcharts is a simple open-source charting library written in C# that utilizes SkiaSharp to draw a wide range of chart types. It had implementations for Xamarin.Forms, .NET MAUI, UWP, and others but initially had no Uno support.
The great thing about having UWP support is that the code was already written for the Windows version of SKCanvasView and could be used with minimal changes in an Uno library.
To add Uno support, a new project was created: Microcharts.Uno.WinUI. This is created as a .NET Standard library, so it has no platform dependencies itself. The required steps:
SkiaSharp.Views.Uno.WinUIUno.WinUIChartView.cs fileBecause the SkiaSharp.Views libraries depend on the base SkiaSharp library, you don't need to explicitly add it separately.
The only necessary changes in the code were to support WinUI 3.0 (and hence the Uno.WinUI implementation) by changing the using statements. This is handled using an #if directive so that the single file can be retained in multiple projects:
#if WINUI
using Microsoft.UI.Xaml;
using SkiaSharp.Views.Windows;
#else
using Windows.UI.Xaml;
using SkiaSharp.Views.UWP;
#endif
As described above, the drawing is carried out by the OnPaintCanvas method:
private void OnPaintCanvas(object sender, SKPaintSurfaceEventArgs e)
{
if (this.chart != null)
{
this.chart.Draw(e.Surface.Canvas, e.Info.Width, e.Info.Height);
}
else
{
e.Surface.Canvas.Clear(SKColors.Transparent);
}
}
The chart object is an instance of Microcharts.Chart and is the same across all implementations of Microcharts. It contains the Draw method, which takes the Skia canvas and size and does all the drawing based on the properties set on it and the data source.
This means that on all platforms, you can access the Chart property via XAML or code and have the same API regardless of platform.
Add the SkiaSharp.Views.Uno.WinUI NuGet package to your project:
<PackageReference Include="SkiaSharp.Views.Uno.WinUI" Version="2.88.0" />
Create a class that inherits from SKXamlCanvas:
using SkiaSharp;
using SkiaSharp.Views.Windows;
public class MyCustomControl : SKXamlCanvas
{
public MyCustomControl()
{
PaintSurface += OnPaintSurface;
}
private void OnPaintSurface(object sender, SKPaintSurfaceEventArgs e)
{
var canvas = e.Surface.Canvas;
var info = e.Info;
// Clear the canvas
canvas.Clear(SKColors.White);
// Your custom drawing code here
using (var paint = new SKPaint())
{
paint.Color = SKColors.Blue;
paint.IsAntialias = true;
paint.Style = SKPaintStyle.Fill;
// Example: Draw a circle in the center
var centerX = info.Width / 2f;
var centerY = info.Height / 2f;
var radius = Math.Min(info.Width, info.Height) / 4f;
canvas.DrawCircle(centerX, centerY, radius, paint);
}
}
}
<Page x:Class="MyApp.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:MyApp">
<Grid>
<local:MyCustomControl Width="400" Height="400"/>
</Grid>
</Page>
To make your custom control more flexible, add dependency properties that can be data-bound:
public class MyCustomControl : SKXamlCanvas
{
public static readonly DependencyProperty FillColorProperty =
DependencyProperty.Register(
nameof(FillColor),
typeof(Color),
typeof(MyCustomControl),
new PropertyMetadata(Colors.Blue, OnPropertyChanged));
public Color FillColor
{
get => (Color)GetValue(FillColorProperty);
set => SetValue(FillColorProperty, value);
}
private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// Invalidate the control to trigger a redraw
if (d is MyCustomControl control)
{
control.Invalidate();
}
}
private void OnPaintSurface(object sender, SKPaintSurfaceEventArgs e)
{
var canvas = e.Surface.Canvas;
var info = e.Info;
canvas.Clear(SKColors.White);
using (var paint = new SKPaint())
{
// Convert WinUI Color to SKColor
var color = FillColor;
paint.Color = new SKColor(color.R, color.G, color.B, color.A);
paint.IsAntialias = true;
paint.Style = SKPaintStyle.Fill;
var centerX = info.Width / 2f;
var centerY = info.Height / 2f;
var radius = Math.Min(info.Width, info.Height) / 4f;
canvas.DrawCircle(centerX, centerY, radius, paint);
}
}
}
Now you can bind or set the color from XAML:
<local:MyCustomControl Width="400"
Height="400"
FillColor="Red"/>
Call Invalidate() on your control when you need to trigger a redraw. This is typically done in property change handlers.
For complex drawings that don't change often, consider caching rendered content:
private SKBitmap cachedBitmap;
private void OnPaintSurface(object sender, SKPaintSurfaceEventArgs e)
{
if (cachedBitmap == null || needsRedraw)
{
// Render to cached bitmap
cachedBitmap = new SKBitmap(info.Width, info.Height);
using (var canvas = new SKCanvas(cachedBitmap))
{
// Draw your complex graphics here
}
needsRedraw = false;
}
// Draw the cached bitmap to the surface
e.Surface.Canvas.DrawBitmap(cachedBitmap, 0, 0);
}
SkiaSharp automatically uses hardware acceleration when available. Ensure your drawing code is efficient:
SKPaint objects when possible// Rectangle
canvas.DrawRect(x, y, width, height, paint);
// Circle
canvas.DrawCircle(centerX, centerY, radius, paint);
// Oval
canvas.DrawOval(new SKRect(left, top, right, bottom), paint);
// Line
canvas.DrawLine(x1, y1, x2, y2, paint);
// Path
using (var path = new SKPath())
{
path.MoveTo(x1, y1);
path.LineTo(x2, y2);
path.LineTo(x3, y3);
path.Close();
canvas.DrawPath(path, paint);
}
using (var paint = new SKPaint())
{
paint.Color = SKColors.Black;
paint.TextSize = 24;
paint.IsAntialias = true;
paint.TextAlign = SKTextAlign.Center;
canvas.DrawText("Hello, Uno!", x, y, paint);
}
using (var stream = Assembly.GetExecutingAssembly()
.GetManifestResourceStream("MyApp.Assets.image.png"))
using (var bitmap = SKBitmap.Decode(stream))
{
canvas.DrawBitmap(bitmap, x, y);
}
using (var paint = new SKPaint())
{
paint.Color = SKColors.Blue;
// Shadow
paint.ImageFilter = SKImageFilter.CreateDropShadow(2, 2, 4, 4, SKColors.Black);
// Blur
paint.ImageFilter = SKImageFilter.CreateBlur(5, 5);
canvas.DrawCircle(centerX, centerY, radius, paint);
}
The Microcharts.Samples.Uno project demonstrates how to use Microcharts in an Uno Platform app. The project structure:
This shows how easy it is to share or port code between different .NET platforms, as all the logic is platform-agnostic.
When migrating custom-drawn controls from Xamarin.Forms to Uno Platform:
SKCanvasViewSkiaSharp.Views.Uno.WinUI NuGet package to your Uno projectSKCanvasView to SKXamlCanvasSkiaSharp.Views.WindowsOnPaintSurface event handler if neededBindableProperty to DependencyProperty for any custom propertiesMigrating custom-drawn controls from Xamarin.Forms to Uno Platform is straightforward when using SkiaSharp:
SKCanvasViewThe key difference is changing from SKCanvasView to SKXamlCanvas and updating namespaces – the actual drawing code remains the same.
The complete Microcharts code, including the Uno libraries and sample app, is available in the Microcharts GitHub repository. The Microcharts.Uno package is also available on NuGet for easy integration into your own app.