Inside Story: How I migrated a WinForm control library to a Blazor library using the HTML canvas tag
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:
- Loops
for
,foreach
,do/while
,while
- Conditions
if/else
- Functions/Methods
- 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:
- Triangle
- Green star
- Spiral
- Color wheel
- Sierpiński triangle (a Triforce)
- 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
In Visual Studio, right-click on the solution and choose
Add
->New Project
.Choose
Razor Class Library
Give it a name.
Make sure to select
.NET 5.0
.- 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.
- Make sure that the
Click on
Create
.
Cleaning up and adding the required asset
Delete the
wwwroot\background.png
image.Rename the
wwwroot\exampleJsInterop.js
file towwwroot\TurtleJsInterop.js
.Rename the
ExampleJsInterop.cs
file toTurtleJsInterop.cs
.Delete the
Component1.razor
component.Create the
wwwroot\Turtle.png
image.
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
- 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.
Init
will register our callback method.Step
, our callback method that will ask theTurtle
component to repaint.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
.
OnAfterRenderAsync
will register our callback method using the injectedTurtleJsInterop
instance.Step
which is our callback method will redraw the turtle then callStateHasChanged
.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:
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.