⚠️ Warning: This website is still being developed. Code examples, videos, and links may be broken while I continue to work on it. -- Scott, May 10, 2020
Resources for visualizing data using C# and the .NET platform

Isolating the Renderer

By isolating the renderer into its own library (independent of both the graphics model and the GUI projects) we can create a single render method that can be used across GUI platforms.

In this article we will refactor the constellation project to isolate the render method into its own library which does not contain any GUI code. The renderer library will simply contain one static class with one static method to render a Field onto a Bitmap.

To simplify this example the SkiaSharp/OpenGL rendering system was omitted, but the concepts here could also be applied to that graphics library.

Rationale

On one hand design could be criticized for overly fragmenting the application into multiple small projects. On the other hand this design offers three compelling advantages:

  • Rendering is isolated from the GUI code. You can modify one and be guaranteed not to affect the other. This is consistent with the single responsibility principle (the S in solid), and the separation of concerns (SoC), and the dependency inversion principle (the D in SOLID).

  • The renderer is testable. You can run the graphics model and render it however you like it from another GUI, Console Application, or MSTest/NUnit/XUnit Test Project.

  • Rendering is platform-independent. Since the rending method targets .NET Standard it can be called from Windows Forms, WPF, UDP, Xamarin, WinUI, or whatever Microsoft invents next! The renderer is not coupled to a UI framework.

System.Drawing Implementation

Renderer (.NET Standard Library)

  • This was mostly a copy/paste job right out of Form1.cs

  • Note that the System.Drawing.Common NuGet package must be installed to provide System.Drawing support for .NET Standard.

public static void Render(Field field, Bitmap bmp)
{
    using (Graphics gfx = Graphics.FromImage(bmp))
    using (Brush brush = new SolidBrush(Color.White))
    using (Pen pen = new Pen(Color.White))
    {

        gfx.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

        // clear background
        gfx.Clear(ColorTranslator.FromHtml("#003366"));

        // draw stars
        float starRadius = 3;
        foreach (var star in field.Stars)
        {
            var rect = new RectangleF(
                    x: star.X - starRadius,
                    y: star.Y - starRadius,
                    width: starRadius * 2,
                    height: starRadius * 2
                );

            gfx.FillEllipse(brush, rect);
        }

        // draw lines connecting close stars
        double connectDistance = 100;
        foreach (var star1 in field.Stars)
        {
            foreach (var star2 in field.Stars)
            {
                double dX = Math.Abs(star1.X - star2.X);
                double dY = Math.Abs(star1.Y - star2.Y);
                if (dX > connectDistance || dY > connectDistance)
                    continue;
                double distance = Math.Sqrt(dX * dX + dY * dY);
                int alpha = (int)(255 - distance / connectDistance * 255);
                alpha = Math.Min(alpha, 255);
                alpha = Math.Max(alpha, 0);
                pen.Color = Color.FromArgb(alpha, Color.White);
                if (distance < connectDistance)
                    gfx.DrawLine(pen, star1.X, star1.Y, star2.X, star2.Y);
            }
        }
    }
}

GUI (Windows Forms Application)

The GUI code becomes very simple, which is an excellent sign that we are on the right track. Well-written GUI code does not contain business logic, and vise versa.

A Timer initiates a render every 1ms which creates a Bitmap the size of a Picturebox, renders onto it, then it applies the Image to the Picturebox.

private void timer1_Tick(object sender, EventArgs e)
{
    stopwatch.Restart();
    using (Bitmap bmp = new Bitmap(pictureBox1.Width, pictureBox1.Height))
    {
        RenderSystemDrawing.Renderer.Render(field, bmp);
        pictureBox1.Image?.Dispose();
        pictureBox1.Image = (Bitmap)bmp.Clone();
    }
}

💡 You must install System.Drawing.Common in your Windows Forms Application to facilitate communication with the renderer which is using it.

Source Code

GitHub: /examples/drawing/constellation/isolated