Inside Story: How I migrated a WinForm control library to a Blazor library using the HTML canvas tag

Pier-Luc Bonneville
Pier-Luc Bonneville
Being technical is important at the leadership level in a world where IT is eating the world.
Dec 6, 2020 9 min read
thumbnail for this post

Introduction

The Turtle graphics libraries, first introduced in the Logo programming language are useful tools to teach programming principals to children and novice programmers. With it, you can explore these four concepts:

  1. Loops for, foreach, do/while, while
  2. Conditions if/else
  3. Functions/Methods
  4. Recursion

In this post, we’ll be porting a .NET Framework 2.0 WinForm control library using the System.Drawing.Graphics class to .NET 5.0 for use in a Blazor application.

The original source we’ll be copying over can be found here on GitHub Nakov.TurtleGraphics.

Here is the final product running as a Blazor WebAssembly application

You can play with these few examples:

  1. Triangle
  2. Green star
  3. Spiral
  4. Color wheel
  5. Sierpiński triangle (a Triforce)
  6. Circle

Creating the Blazor component library

As of this writting, the dotnet CLI templates are only avaible for netstandard2.0 and haven’t been updated to .NET 5.0.

dotnet new -i Microsoft.AspNetCore.Blazor.Templates
dotnet new blazorlib
  1. In Visual Studio, right-click on the solution and choose Add -> New Project.

    Create a new project

  2. Choose Razor Class Library

    Choose Razor Class Library

  3. Give it a name.

  4. Make sure to select .NET 5.0.

    1. Make sure that the Support pages and views option is not selected as we are not going to create a Razor Pages or an MVC library but a Blazor library.
  5. Click on Create.

    Default Razor Class Library project structure

Cleaning up and adding the required asset

  1. Delete the wwwroot\background.png image.

  2. Rename the wwwroot\exampleJsInterop.js file to wwwroot\TurtleJsInterop.js.

  3. Rename the ExampleJsInterop.cs file to TurtleJsInterop.cs.

  4. Delete the Component1.razor component.

  5. Create the wwwroot\Turtle.png image.

    Turtle head

Our project is now in the desired state for us to start off our conversion. We are now ready to start our migration.

NuGet package reference

  1. Add the Blazor.Extensions.Canvas NuGet package reference.

Porting the code to .NET 5.0 with C# 9.0

Let’s start by create a type to represent the turtle. For this we’ll leverage the C# 9.0 record types with init only properties to create an immutable value type.

The TurtleHead doesn’t have any logic apart from the Angle validation as the Angle is always kept in the range of 0 to 360.

using System.Drawing;

namespace TurtleGraphics.BlazorCanvas
{
    public record TurtleHead
    {
        public float X { get; init; } = 0;

        public float Y { get; init; } = 0;

        private float _angle;
        public float Angle
        {
            get => _angle;
            init
            {
                _angle = value % 360;
                if (_angle < 0)
                {
                    _angle += 360;
                }
            }
        }

        public bool ShowTurtle { get; init; } = true;

        public bool PenVisible { get; init; } = true;

        public int Delay { get; init; } = 0;

        public static readonly Color DefaultColor = Color.Blue;
        public Color PenColor { get; init; } = DefaultColor;

        public const int DefaultPenSize = 7;
        public float PenSize { get; init; } = DefaultPenSize;

        public int Left { get; init; }

        public int Top { get; init; }

        public int Width { get; init; }

        public int Height { get; init; }
    }
}

Then, we’ll create the front end Turtle.razor component. There isn’t much code in this file as we’ll be putting it in a code-behind file. As you can see, we need to apply inline CSS transformation to the turtle icon to move and rotate the image while drawing.

@using Blazor.Extensions.Canvas

<div>
    @if (ShowTurtle)
    {
        <img style="
                display: flex;
                position: absolute;
                z-index: 1;
                -webkit-transform: @RotateStyle;
                -moz-transform:    @RotateStyle;
                -ms-transform:     @RotateStyle;
                -o-transform:      @RotateStyle;
                transform:         @RotateStyle;
                margin-left: @($"{_turtle.Left}px");
                margin-top:  @($"{_turtle.Top}px");"
             src="_content/TurtleGraphics.BlazorCanvas/Turtle.png"
             width="35"
             height="35" />
    }

    <div id="canvasContainer"
         style="width: 100%; height: 100%; opacity: 1;">
        <BECanvas @ref="_canvas" Height="@Height" Width="@Width"></BECanvas>
    </div>
</div>

After which we’ll create the Turtle.razor.cs back-end partial class.

Notice that the Turtle class is a wrapper for the TurtleHead record and that we are making the modification to the TurtleHead using the with expression syntax always creating a new instance of our TurtleHead record.

Here is the complete code for the Turtle class.

using System;
using Microsoft.AspNetCore.Components;
using Blazor.Extensions;
using Blazor.Extensions.Canvas;
using Blazor.Extensions.Canvas.Canvas2D;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.JSInterop;
using System.Drawing;
using System.Collections.Generic;

namespace TurtleGraphics.BlazorCanvas
{
    public partial class Turtle : IDisposable
    {
        private Canvas2DContext? _context;
        private BECanvasComponent? _canvas;
        private TurtleHead _turtle = new() { Width = 35, Height = 35 };

        [Parameter]
        public long Height { get; set; }

        [Parameter]
        public long Width { get; set; }

        [Inject]
        public TurtleJsInterop? TurtleJsInterop { get; set; }

        public float X
        {
            get => _turtle.X;
            set => _turtle = _turtle with { X = value };
        }

        public float Y
        {
            get => _turtle.Y;
            set => _turtle = _turtle with { Y = value };
        }

        public float Angle
        {
            get => _turtle.Angle;
            set => _turtle = _turtle with { Angle = value };
        }

        public bool ShowTurtle
        {
            get => _turtle.ShowTurtle;
            set => _turtle = _turtle with { ShowTurtle = value };
        }

        public bool PenVisible
        {
            get => _turtle.PenVisible;
            set => _turtle = _turtle with { PenVisible = value };
        }

        public Color PenColor
        {
            get => _turtle.PenColor;
            set => _turtle = _turtle with { PenColor = value };
        }

        public float PenSize
        {
            get => _turtle.PenSize;
            set => _turtle = _turtle with { PenSize = value };
        }

        public int Delay
        {
            get => _turtle.Delay;
            set => _turtle = _turtle with { Delay = value };
        }

        private string RotateStyle => $"rotate({_turtle.Angle}deg)";

        protected override async Task OnAfterRenderAsync(bool firstRender)
        {
            if (!firstRender)
                return;

            await TurtleJsInterop.Init(this);
            _context = await _canvas.CreateCanvas2DAsync();
        }

        public async ValueTask Step()
        {
            await DrawTurtle();

            StateHasChanged();
        }

        public async ValueTask OnResize(int width, int height)
        {
            Width = width;
            Height = height;

            await Reset();
        }

        public async Task Reset()
        {
            // Clear the surface area
            await _context.ClearRectAsync(0, 0, Width, Height);

            _turtle = _turtle with
            {
                // Initialize the pen size and color
                PenSize = TurtleHead.DefaultPenSize,
                PenColor = TurtleHead.DefaultColor,

                // Initialize the turtle position and otehr settings
                X = 0,
                Y = 0,
                Angle = 0,
                PenVisible = true,
                ShowTurtle = true,

                // Intentionally preserve the "Delay" settings
                //Delay = 0
            };

            StateHasChanged();
        }

        public async Task Forward(float distance = 10)
        {
            var angleRadians = _turtle.Angle * Math.PI / 180;
            var newX = _turtle.X + (float)(distance * Math.Sin(angleRadians));
            var newY = _turtle.Y + (float)(distance * Math.Cos(angleRadians));
            await MoveTo(newX, newY);
        }

        public async Task Backward(float distance = 10)
        {
            await Forward(-distance);
        }

        public async Task MoveTo(float newX, float newY)
        {
            var fromX = Width / 2 + _turtle.X;
            var fromY = Height / 2 - _turtle.Y;
            _turtle = _turtle with { X = newX, Y = newY };
            if (_turtle.PenVisible)
            {
                var toX = Width / 2 + _turtle.X;
                var toY = Height / 2 - _turtle.Y;

                await _context.SetLineCapAsync(LineCap.Round);
                await _context.BeginPathAsync();
                await _context.SetLineWidthAsync(_turtle.PenSize);
                await _context.SetStrokeStyleAsync(ColorTranslator.ToHtml(_turtle.PenColor));
                await _context.MoveToAsync(fromX, fromY);
                await _context.LineToAsync(toX, toY);
                await _context.StrokeAsync();
                await _context.ClosePathAsync();
            }

            await DrawTurtle();
        }

        public async Task Rotate(float angleDelta)
        {
            _turtle = _turtle with { Angle = _turtle.Angle + angleDelta };
            await DrawTurtle();
        }

        public async Task RotateTo(float newAngle)
        {
            _turtle = _turtle with { Angle = newAngle };
            await DrawTurtle();
        }

        public void PenUp()
        {
            _turtle = _turtle with { PenVisible = false };
        }

        public void PenDown()
        {
            _turtle = _turtle with { PenVisible = true };
        }

        private async Task DrawTurtle()
        {
            await Task.Delay(_turtle.Delay);

            var turtleX = 1 + Width / 2 + _turtle.X - _turtle.Width / 2;
            var turtleY = 1 + Height / 2 - _turtle.Y - _turtle.Height / 2;

            _turtle = _turtle with { Left = (int)Math.Round(turtleX) };
            _turtle = _turtle with { Top = (int)Math.Round(turtleY) };
        }

        #region IDisposable
        private bool _disposedValue;

        protected virtual void Dispose(bool disposing)
        {
            if (!_disposedValue)
            {
                if (disposing)
                {
                    // TODO: dispose managed state (managed objects)
                    _context?.Dispose();
                }

                // TODO: free unmanaged resources (unmanaged objects) and override finalizer
                // TODO: set large fields to null
                _disposedValue = true;
            }
        }

        // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
        // ~Turtle()
        // {
        //     // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
        //     Dispose(disposing: false);
        // }

        public void Dispose()
        {
            // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }

        #endregion // IDisposable
    }
}

Rendering

To have smooth rendering, we’ll be using the js window.requestAnimationFrame method to let the browser know that we want to update the screen before the next repaint.

In our TurtleJsInterop.cs file, we’ll add three methods.

  1. Init will register our callback method.
  2. Step, our callback method that will ask the Turtle component to repaint.
  3. OnResize which will reposition the turtle head when the window is resized.
using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;

namespace TurtleGraphics.BlazorCanvas
{
    // This class provides an example of how JavaScript functionality can be wrapped
    // in a .NET class for easy consumption. The associated JavaScript module is
    // loaded on demand when first needed.
    //
    // This class can be registered as scoped DI service and then injected into Blazor
    // components for use.

    public class TurtleJsInterop : IAsyncDisposable
    {
        private readonly Lazy<Task<IJSObjectReference>> moduleTask;
        private Turtle? _turtleInstance;

        public TurtleJsInterop(IJSRuntime jsRuntime)
        {
            moduleTask = new(() => jsRuntime.InvokeAsync<IJSObjectReference>(
               "import", "./_content/TurtleGraphics.BlazorCanvas/turtleJsInterop.js").AsTask());
        }

        public async ValueTask Init(Turtle instance)
        {
            _turtleInstance = instance;
            var module = await moduleTask.Value;
            await module.InvokeVoidAsync("init", DotNetObjectReference.Create(this));
        }

        [JSInvokable]
        public async ValueTask OnResize(int width, int height)
        {
            await _turtleInstance.OnResize(width, height);
        }

        [JSInvokable]
        public async ValueTask Step(float timeStamp)
        {
            await _turtleInstance.Step();
        }

        public async ValueTask DisposeAsync()
        {
            if (moduleTask.IsValueCreated)
            {
                var module = await moduleTask.Value;
                await module.DisposeAsync();
            }
        }
    }
}

Then, in the wwwroot\turtleJsInterop.js file we’ll create the JavaScript module invoked by our TurtleJsInterop C# class.

// This is a JavaScript module that is loaded on demand. It can export any number of
// functions, and may import other JavaScript modules if required.

export let canvasContainer = null;
export let canvas = null;
export let instance = null;

export function step(timeStamp) {
    window.requestAnimationFrame(step);
    instance.invokeMethodAsync('Step', timeStamp);
}

export function onResize() {
    if (!canvas || !canvasContainer) {
        return;
    }

    instance.invokeMethodAsync('OnResize', canvasContainer.clientWidth, canvas.height);
}

export function init(dotNetObject) {
    canvasContainer = document.getElementById('canvasContainer');
    let canvases = canvasContainer.getElementsByTagName('canvas') || [];

    instance = dotNetObject;
    canvas = canvases.length ? canvases[0] : null;
    window.requestAnimationFrame(step);

    onResize();
    window.addEventListener("resize", onResize);
}

We can now use the TurtleJsInterop class in Turtle.razor.cs.

  1. OnAfterRenderAsync will register our callback method using the injected TurtleJsInterop instance.
  2. Step which is our callback method will redraw the turtle then call StateHasChanged.
  3. OnResize will reposition the turtle head when the window is resized.
public partial class Turtle : IDisposable
{
	// ...

	[Inject]
    public TurtleJsInterop? TurtleJsInterop { get; set; }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender)
            return;

        await TurtleJsInterop.Init(this);
        _context = await _canvas.CreateCanvas2DAsync();
        await _context.SetLineCapAsync(LineCap.Round);
    }

    public async ValueTask OnResize(int width, int height)
    {
        Width = width;
        Height = height;

        await Reset();
    }

    public async ValueTask Step()
    {
        await DrawTurtle();

		StateHasChanged();
    }

	// ...
}

Wrapping up

That it for the library code. We are now ready to use our library on a Blazor application.

The code covered in this blog post is available here:

TurtleGraphics.BlazorCanvas

For a sample usage of this library, see Turtle graphics library in Blazor using the HTML canvas tag.

One might argue that the C# asynchronous peculiarities to achieve a non-blocking UI when declaring methods with async Task and when invoking them await _turtle.{method}() are a bit complex for introduction level courses to programming.

References

  1. Call .NET methods from JavaScript functions in ASP.NET Core Blazor
  2. Canvas scripting API
  3. Window.requestAnimationFrame()