Building a 2D Game with WinUI and Win2D

For a while I've been working building a little collection of classic arcade games for Windows to better understand Win2D and game development, and to see what is possible with WinUI.. You can see the games in action at games.xaml.dev or grab it right from the store. In this post, I'll break down how the rendering comes together and build a small example: a keyboard-controlled ship flying over a layered starfield.

You really only need very little graphics infrastructure to get started. A few lines, circles, translucent colors, and a managing some drawing orders can take you a long way.

Bringing XAML and immediate-mode rendering together

Space Arcade is a WinUI application. For this tutorial, we'll combine two systems to build a similar little interactive app:

System What it does
XAML Layout, menus, buttons, score displays, and other normal application UI
Win2D Draws the playfield: backgrounds, ships, terrain, projectiles, and effects


Win2D
is a hardware-accelerated 2D drawing API built on Direct2D. Instead of creating a XAML element for every projectile or star, I draw them into a CanvasControl.

That is an immediate-mode approach: each draw callback describes the scene again. The application keeps the world state, but it doesn't keep a separate XAML visual for every object. Immediate-mode rendering is very typical in game development where you have very frequent changes to what is on the screen.

XAML, by contrast, is retained-mode UI. A score label remains a label; a button remains a button and you don't have to manage redrawing or updating it after declaring it. It keeps its layout, keyboard navigation, and accessibility behavior without my renderer having to recreate those features.

The beauty is that these systems can coexist in the same Grid:

<Grid>
    <canvas:CanvasControl Draw="OnDraw" />
    <StackPanel Margin="24" IsHitTestVisible="False">
        <TextBlock Text="SCORE" />
        <TextBlock Text="000000" />
    </StackPanel>
</Grid>

Here, canvas maps to using:Microsoft.Graphics.Canvas.UI.Xaml. Children declared after appear above earlier children, so the text sits above the playfield. Since this is a decorative overlay, I use IsHitTestVisible="False" so it doesn't intercept pointer input in case I want to use the mouse to interact with the game area. However real buttons should remain interactive, so don't disable hit tests on those or the user can't click them.

The important lesson here is: not everything animated on screen needs to become part of the game renderer. Use XAML where it makes sense.

The other separation: input, update, and draw

Inside the game, there are three responsibilities:

Keyboard events ----> held-key state
                            |
Rendering notification ---> update scene using elapsed time
                            |
                            +---> request canvas redraw
                                       |
Canvas Draw callback ----------------> draw current scene

XAML controls remain above the canvas.

The XAML page owns input and lifecycle. A scene or world owns positions, movement, and effects. The renderer reads that state and converts it into drawing commands, and we update that state every frame and based on user-inputs. Some of the drawing code lives directly in its page; more involved rendering can live in a dedicated renderer.

For the tutorial, we'll use three small pieces:

File Responsibility
GamePage.xaml and .xaml.cs Canvas hosting, frame timing, keyboard input, pause, and cleanup
Scene.cs Ship movement, star positions, and particle lifetimes - ie. the game state.
SceneRenderer.cs Drawing, transforms, colors, and layer order


The complete sample project (ZIP) accompanies this post. The snippets below introduce the pieces in order; they are excerpts or intermediate versions, not extra code to paste on top of the final sample.

1. Start with one shape

In a WinUI project with the Microsoft.Graphics.Win2D package installed, a CanvasControl gives us a CanvasDrawingSession in its Draw event that is called at some point after a draw has been requested.

The first renderer can be this small:

private void OnDraw(CanvasControl sender, CanvasDrawEventArgs e)
{
    CanvasDrawingSession ds = e.DrawingSession;
    ds.Clear(Color.FromArgb(255, 5, 11, 25));
    ds.FillCircle(200, 150, 12, Color.FromArgb(255, 140, 239, 242));
}

Those types come from Microsoft.Graphics.Canvas, Microsoft.Graphics.Canvas.UI.Xaml, and Windows.UI.

Clear the canvas, then draw a circle. Pretty basic:

A single cyan circle at control coordinates 200, 150 on a dark canvas.

2. Choose a coordinate system before adding movement

Drawing directly in the control's coordinates is fine for the first dot. For a game, I find it easier to choose a fixed logical playfield.

Our sample uses 960 by 540 logical units, independent of the window size:

ds.Clear(Color.FromArgb(255, 5, 11, 25));

float scale = MathF.Min(ActualWidth / Scene.Width, ActualHeight / Scene.Height);
Vector2 offset = new(
    (width - Scene.Width * scale) / 2,
    (height - Scene.Height * scale) / 2); // Center

ds.Transform =
    Matrix3x2.CreateScale(scale) *
    Matrix3x2.CreateTranslation(offset);

ds.FillCircle(Scene.Width / 2, Scene.Height / 2, 12,
    Color.FromArgb(255, 140, 239, 242));
width and height are the canvas's ActualWidth and ActualHeight, converted to float.

The transform maps the fixed 960 × 540 game coordinates onto the actual canvas. The circle's position (480, 270) is the center of the logical world, not necessarily the center of the control.

For example, on a 1200 x 800 canvas:

  • Scale is 1.25, making the playfield 1200 x 675.
  • Translation adds 62.5 units of padding at the top.
  • The circle's center becomes (600, 400) -the canvas center- and its radius scales from 12 to 15.

Without the transform, it would remain at (480, 270) with radius 12, regardless of window size.

For just one centered circle, we could calculate its screen coordinates directly. The transform becomes useful when drawing an entire scene: every shape, position, and stroke scales together, while movement and gameplay stay in the same logical coordinate system.

Using the smaller scale preserves the aspect ratio. The remaining space becomes letterboxing. A ship at (480, 270) stays in the center, and its movement speed stays in logical units per second. If you don't want letterboxing, that's fine too, but requires more testing to ensure what you want on the screen doesn't get cropped.

Note that the control dimensions are device-independent pixels, not physical monitor pixels. Win2D handles the control's DPI so we don't have to; we aren't multiplying the scale by the display's DPI again.

Clear the full canvas before applying the world transform. The final sample also clips drawing to the logical playfield using CreateLayer, so particles and glow cannot spill into the letterbox margins. Restore the previous transform afterward.

If you later support aiming with the mouse, invert this same transform to convert pointer positions back into world coordinates. Don't maintain a second, slightly different scaling calculation for input.

The cyan circle placed at the center of the 960 by 540 logical playfield.

3. Make the dot move

The ship's position belongs to the scene stored as a 2D vector. For example:

public Vector2 Player { get; private set; } = new(Width / 2, Height / 2);

Key-down and key-up events maintain a set of held keys. On each update, the page converts that set into a direction:

Vector2 direction = new(
    (Held(VirtualKey.Right, VirtualKey.D) ? 1 : 0) -
    (Held(VirtualKey.Left, VirtualKey.A) ? 1 : 0),
    (Held(VirtualKey.Down, VirtualKey.S) ? 1 : 0) -
    (Held(VirtualKey.Up, VirtualKey.W) ? 1 : 0));

The scene then applies movement and multiplies it by the amount of time passed since the last frame (dt) to make the speed constant:

if (direction.LengthSquared() > 1)
    direction = Vector2.Normalize(direction);

Velocity = direction * PlayerSpeed;
Player = Vector2.Clamp(
    Player + Velocity * dt,
    new Vector2(PlayerRadius),
    new Vector2(Width - PlayerRadius, Height - PlayerRadius));

Here, PlayerSpeed is 240 logical units per second. Normalizing the diagonal direction stops diagonal movement from being faster than horizontal movement. Opposite keys cancel out.

For timing, the sample uses CompositionTarget.Rendering as a UI-thread frame notification. Although its event argument is declared as object, we can cast it to WinUI's Microsoft.UI.Xaml.Media.RenderingEventArgs and use its RenderingTime timestamp to calculate the time passed since the previous frame:

TimeSpan now = ((RenderingEventArgs)e).RenderingTime;
if (_lastFrame is not TimeSpan previous)
{
    _lastFrame = now;
    return;
}
float dt = (float)Math.Clamp((now - previous).TotalSeconds, 0, 0.05);
_lastFrame = now;

if (_paused) return;
_scene.Update(dt, direction);
_canvas.Invalidate();

RenderingTime is a frame timestamp, not the duration of the current frame. Subtract the previous timestamp to get elapsed time. _lastFrame is a nullable TimeSpan? field, initially null; the first notification establishes a baseline without advancing the scene. Reset it to null when the page loads or pause changes so resuming doesn't include time spent paused.

The page calls Invalidate() to request drawing; that isn't a synchronous call to Draw. Requests can be coalesced and called from multiple events within a single frame and still only trigger a single draw.

The 50 ms cap is a simple defense against a giant movement jump after a stall. It deliberately discards excess elapsed time. This is a variable-step tutorial, not a deterministic physics engine (For a collision-heavy simulation, consider a fixed-step accumulator with bounded catch-up work).

Win2D also has CanvasAnimatedControl, with its own animation loop and game-loop-thread model. This example uses CanvasControl to keep input, simulation, drawing, and XAML updates on the same UI thread. That's a simplicity tradeoff, not a claim that it's inherently better for games: a busy UI thread also delays the game. CanvasAnimatedControl can isolate the game loop from that work, but requires safely transferring input and UI state between threads.

Step 3: just the dot moving, cropped around its circular path.

The clips in steps 3-5 use scripted directions through the same Scene.Update method, with Win2D-rendered frames encoded at 30 frames per second. Each shows only the drawing introduced so far, cropped to the movement area without magnification.

4. Replace the dot with a tiny ship

Instead of a circle, let's make a more recognizable character to look like a simple spaceship. We'll draw a triangle in local coordinates:

Vector2 nose = new(16, 0);
Vector2 upper = new(-11, -10);
Vector2 lower = new(-11, 10);

ds.DrawLine(nose, upper, Cyan, 1.5f);
ds.DrawLine(upper, lower, Cyan, 1.5f);
ds.DrawLine(lower, nose, Cyan, 1.5f);

These points describe a ship centered near the origin, facing right. To draw it at the player's position, compose its rotation and translation with the existing world-to-canvas transform:

Matrix3x2 previous = ds.Transform;
ds.Transform =
    Matrix3x2.CreateRotation(scene.Heading) *
    Matrix3x2.CreateTranslation(scene.Player) *
    previous;

In System.Numerics, that order applies local rotation first, then placement in the world, then the world-to-canvas transform. Reversing it can rotate the ship's position around the origin instead of rotating the ship itself.

While moving, the scene updates Heading using MathF.Atan2(Velocity.Y, Velocity.X). When movement stops, it keeps the last heading.

Restore the previous transform in a finally block after drawing the ship. Transforms are shared drawing-session state: forgetting to restore one can make the next planet or particle rotate with the player.

Step 4: the same movement now drives a small, direction-facing ship. No starfield or glow yet.

5. Build a starfield that feels like several layers

Create collecgion of stars once, each with a position, depth, and twinkle phase. The sample uses 150 stars split between three depths:

float depth = (i % 3) switch
{
    0 => 0.25f,
    1 => 0.6f,
    _ => 1
};

Randomize the initial positions and phases, but don't regenerate them in Draw. Otherwise, stars teleport between frames and the background becomes visual noise.

During updates, move them at different speeds:

Vector2 drift = new Vector2(-18, 6) - Velocity * 0.15f;
star.Position += drift * star.Depth * dt;

The constant drift keeps the background alive while the ship is stationary. The velocity term nudges it opposite the ship's movement. Larger depth values move farther, creating a simple parallax cue simulating different distances to the stars.

We'll also wrap stars when they leave the playfield, so the enter on the other side:

private static float Wrap(float value, float extent) =>
    (value % extent + extent) % extent;

C#'s remainder can be negative, so the extra normalization matters for stars moving left or up.

In the renderer, we can also vary brightness smoothly to give a twinkling effect:

float twinkle = 0.7f + 0.3f * (float)Math.Sin(
    scene.Time * (1 + star.Depth) + star.Phase);
byte alpha = (byte)(twinkle * (60 + 150 * star.Depth));

ds.FillCircle(
    star.Position,
    0.5f + star.Depth,
    Color.FromArgb(alpha, 185, 220, 245));

Depth now controls speed, size, and brightness. Each is a small signal; together they make a flat collection of dots feel more spatial.

Twinkle is a function of scene time. Drawing the same state twice produces the same appearance instead of advancing an animation twice.

Step 5: stars drift at different speeds and change brightness smoothly while the ship moves.

6. Give drawing order a name

There doesn't have to be a layer object or a separate render target for every visual layer. But it can be useful to create a method that renders each object to more logically understand the draw order.

DrawHaze(ds);
DrawStars(ds, scene);
DrawPlanet(ds);
DrawParticles(ds, scene);
DrawPlayer(ds, scene);
DrawForeground(ds);

Our haze is a handful of large, faint circles. The planet is an opaque disk with a thin illuminated outline and an elliptical ring. Its opaque body covers stars drawn behind it. The exhaust is behind the ship (see step 8), and faint scanlines are drawn last.

The XAML heading and instructions sit above all of these Win2D passes. We don't redraw that text in the canvas.

Step 6: drawing order adds haze, an opaque planet, and a foreground treatment, with XAML text above the canvas. The particle pass is still empty; we'll fill it in step 8.

7. Adding a simple neon-like glow effect

Much of Space Arcade's neon appearance comes from drawing the same primitive several times with slightly different tints:

private static void GlowLine(
    CanvasDrawingSession ds, Vector2 a, Vector2 b, Color color)
{
    ds.DrawLine(a, b, Tint(color, 0.06f), 11);
    ds.DrawLine(a, b, Tint(color, 0.2f), 5);
    ds.DrawLine(a, b, color, 1.5f);
}

Tint returns the same RGB color with an alpha derived from the supplied opacity.

The broad, faint stroke suggests scattered light. The narrower stroke builds a halo. The crisp center preserves the shape. Replacing the ship's three ordinary lines with GlowLine immediately changes its character to have a slight glow.

This is an approximation of glow, not a blur, bloom shader, or simulation of emitted light. The sample uses normal alpha compositing, not actual additive blending.

For small line-based scenes, the approximation is simple and effective. It also has a cost: three strokes instead of one, with overlapping translucent pixels. More glow isn't automatically better.

If an effect really needs a blurred silhouette, Win2D supports offscreen render targets and effects such as GaussianBlurEffect. That adds resource management, intermediate surfaces, and fill cost, and wasn't something I really needed.

Step 7, shown close-up: broad translucent strokes add a halo without replacing the crisp ship outline. This crop is magnified 2x to make the layered strokes easier to see, but the effect is still quite subtle.

8. Add a particle layer

Particles are another small simulation that we can use for exhaust that we can define with a record:

internal record struct Particle(
    Vector2 Position, Vector2 Velocity, float Life);

During updates, reduce their remaining life and advance their position. Remove expired particles while iterating backward through the list.

While the ship is moving, the sample emits exhaust behind its local forward direction at 45 particles per second. An elapsed-time accumulator determines how many to create, rather than emitting one particle per frame. This keeps the intended emission rate independent of the display's refresh rate.

Drawing is simpler than updating:

float fade = Math.Clamp(particle.Life / Particle.Lifetime, 0, 1);
ds.FillCircle(particle.Position, 5 * fade, Tint(Amber, 0.1f * fade));
ds.FillCircle(particle.Position, 1.8f * fade, Tint(Amber, fade));

Again, one faint halo and one brighter center. Shrinking and fading together makes the exhaust dissipate without any texture animation.

The tutorial emits from the latest ship position. For extremely fast motion or longer frame gaps, distributing emissions along the traveled segment produces a smoother trail.

Step 8: time-based emission adds a fading exhaust trail behind the ship, completing the sample.

The less glamorous part: focus and lifetime

A renderer that looks good for ten seconds isn't enough. It has to behave when you click, resize, switch apps, and leave the page.

The complete sample handles the essentials:

  • On Loaded, create the canvas, subscribe to drawing and frame notifications, reset the frame-time baseline, and focus the page.
  • On Unloaded, unsubscribe from frame and window events, detach drawing, and call RemoveFromVisualTree() on the canvas. A later load creates a new canvas.
  • On focus loss, clear held keys so a missed key-up event doesn't leave the ship moving.
  • On window deactivation, pause and clear input. Returning to the window does not resume automatically; The user can press P to unpause and resume the game.

CompositionTarget.Rendering is a static event. Leaving a page subscribed can keep it alive and updating after it is no longer visible. Treat subscription and cleanup as a pair. This event in general you should be careful with and always unsubscribe once you don't need it, as it causes the WinUI rendering loop to constantly run with no pauses.

Download the complete example

Download the sample source (ZIP), extract it, and open RenderingSample.csproj. The archive includes the complete packaged WinUI project, package references, manifest, and tutorial source; there's no need to scaffold a project or copy files manually.

You'll need Windows, the .NET 10 SDK, and Developer Mode. The included README has the run command for WinApp CLI 0.7 or later; you can also use Visual Studio and just launch it from there. Try moving diagonally, releasing the keys, pausing, resizing the window, and switching to another app.

How the same approach grows into different games

Once the frame timing, coordinate system, and drawing passes are in place, the visual vocabulary can change without replacing the whole application.

A scrolling terrain game adds a camera offset and draws terrain segments before ships and explosions. A circular arena builds shapes from angles and radii. A perspective-looking playfield can project logical lane and depth coordinates into 2D points before sending them to Win2D.

That last distinction is useful: a scene can look three-dimensional without being rendered by a full 3D engine. In Space Arcade, projected geometry can still be drawn as ordinary 2D lines. Logical positions remain the source of truth for gameplay.

The first useful renderer can be remarkably modest: one canvas, one scene, elapsed-time updates, and a handful of well-ordered drawing methods. The rest is merely choosing what to put in each layer.

Full demo

I would love for you to try out my game from the store now (it has a free fully playable trial!), and please leave a review in the store if you like it.

Further reading

Building a Windows Tray App by combining Microsoft.UI.Reactor and a Worker Project

I wanted a small Windows app that lives in the tray instead of the taskbar, and minimizes/closes to the tray. This can be useful for building small service apps, while having a UI you can occasionally access to control settings etc.

Here is the full process for setting that up step by step.

1. Start with a Worker project

I started with the standard worker template. In a new project folder, create a worker project using the worker template:

dotnet new worker

I liked the Worker template for this because a tray app is really a background app first. The process needs to stay alive even when the window is hidden.

2. Retarget the project for Windows

The next step was changing the target framework in the project file - we need this to be compatible with WinUI/Reactor and gives access to the APIs needed for windowing and tray behavior. Change the target framework in the project file to:

<TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>

3. Add the UI packages

After that I added the packages the app needs:

dotnet package add Microsoft.UI.Reactor --prerelease
dotnet package add WinUIEx

Microsoft.UI.Reactor gave me a clean C# way to define the window UI. WinUIEx helps with tray icon support and some of the window behavior.

4. Add the Windows app properties

Then I added a few properties in the project file needed to compile WinUI and Reactor apps:

<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<Platforms>ARM64;X64</Platforms>

5. Add the tray icon asset

The app needs a real .ico file for the tray. I added trayicon.ico file to the project and marked it as content:

<Content Include="trayicon.ico" />

6. Keep the app entry point simple

Program.cs stays very small:

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
host.Run();

7. Create the Settings Window:

Create a new SettingsWindow.cs file with the UI. We'll just use the simple Reactor sample window here as a starting point:

using Microsoft.UI.Reactor;
using Microsoft.UI.Reactor.Core;
using Microsoft.UI.Xaml.Controls;
using WinUIEx;
using static Microsoft.UI.Reactor.Factories;

namespace ReactorTrayWorker;

internal sealed class SettingsWindow : Component
{
    public override Element Render()
    {
        var (name, setName) = UseState("World");

        return 
            VStack(
            Heading($"Hello, {name}!"),
            TextBox(name, setName, placeholderText: "Your name")
                .AutomationName("NameInput")
        ).Padding(16);
    }
}

You of course want to change this to whatever content you want here.

8. Create a static UI Window creator for Reactor

Create an initialize method for WinUI/Reactor that takes an action for quitting (we'll use that later):

internal static void InitializeWinUIWindow(Action stopHost)
{
    ReactorApp.Run(_ =>
    {
        ReactorWindow? settingsWindow = null;
        settingsWindow = ReactorApp.OpenWindow(
            new WindowSpec
            {
                Title = "My Worker Settings",
                Width = 560,
                Height = 460,
                ActivateOnOpen = false,
            },
            () => new SettingsWindow(),
            configure: host =>
            {
                WindowManager manager = WindowManager.Get(host.Window);
                manager.WindowStateChanged += (_, state) =>
                {
                    // If the window is minimized, we don't want it to show in the Alt+Tab switcher + taskbar, so we set IsShownInSwitchers to false.
                    bool isVisible = state != WinUIEx.WindowState.Minimized;
                    manager.AppWindow.IsShownInSwitchers = isVisible;
                };
            });
    });
}

The app also watches WindowStateChanged through WindowManager.

When the window is minimized, the app updates IsShownInSwitchers so it does not stay visible in Alt+Tab or the taskbar. When it is restored, that visibility can come back.

This keeps the behavior consistent. Close hides it. Minimize hides it. The tray icon is the main way back in.

Inside Worker.cs, the background service starts the window setup by calling this method:

SettingsWindow.InitializeWinUIWindow(hostApplicationLifetime.StopApplication);

That shutdown callback matters. The tray menu needs a clean way to stop the whole host when the user actually wants to quit, and we'll use it in the Quit context menu further down. Also note that this call is blocking as long as the Reactor app runs, so wrap that call in a Task.Run you don't await.

9. Create the tray icon during window setup

Inside the window host configuration, let's add a TrayIcon that points to trayicon.ico:

string executablePath = new FileInfo(Assembly.GetExecutingAssembly().Location).DirectoryName!;
TrayIcon icon = new(0, Path.Combine(executablePath, @"trayicon.ico"), "My Worker") {
    IsVisible = true
};

At that point the app is available from the tray even if the window is hidden, and we just need to hook clicking it up to opening the window. The tray icon handles the Selected event. When the user clicks the tray icon, the app activates the main window, brings it to the front, and makes sure it can appear in the switcher again. Here I'm declaring a static action that I'll reuse for the context menu later as well.

var showWindow = static() => {
    ReactorApp.PrimaryWindow?.Activate(); // Activate the window
    ReactorApp.PrimaryWindow?.NativeWindow.SetForegroundWindow(); // Bring to front
    ReactorApp.PrimaryWindow?.AppWindow.IsShownInSwitchers = true; //Show in switcher
};
icon.Selected += (_, _) => showWindow();

 

10. Add the tray menu

The tray icon also handles the ContextMenu (right-click) event. We'll add two actions:

  1. Open
  2. Quit

Open brings the settings window back and does the same as left-click. Quit is the only path that fully exits the app and shuts down the worker.

icon.ContextMenu += (_, e) =>
{
    MenuFlyout flyout = new();
    flyout.Items.Add(new MenuFlyoutItem() { Text = "Open" });
    ((MenuFlyoutItem)flyout.Items[0]).Click += (_, _) => showWindow();
    flyout.Items.Add(new MenuFlyoutItem() { Text = "Quit" });
    ((MenuFlyoutItem)flyout.Items[1]).Click += (_, _) =>
    {
        isQuitting = true;
        ReactorApp.PrimaryWindow?.Close();
        icon.Dispose();
        stopHost.Invoke(); // Shuts down the worker thread
    };
    e.Flyout = flyout;
};

This is the core behavior of the whole project. The window is not supposed to control app lifetime like a normal WinUI app does. The tray icon does that. So to prevent uses from exiting the app, we'll handle the closing event as well:

host.Window.AppWindow.Closing += (_, e) =>
{
    // Prevent closing out the window and just hide to tray instead unless we're quitting
    e.Cancel = !isQuitting;
    settingsWindow?.Hide();
};

So now clicking the X button does not shut down the app. It just sends the window back to the tray, unless the quitting flag was set from the Quit context menu.

Here's what the entire initalize method now looks like:

internal static void InitializeWinUIWindow(Action stopHost)
{
    ReactorApp.Run(_ =>
    {
        bool isQuitting = false;
        ReactorWindow? settingsWindow = null;
        settingsWindow = ReactorApp.OpenWindow(
            new WindowSpec
            {
                Title = "My Worker Settings",
                Width = 560, Height = 460,
                ActivateOnOpen = false,
            },
            () => new SettingsWindow(),
            configure: host =>
            {
                // Configure Tray icon
                string executablePath = new FileInfo(System.Reflection.Assembly.GetExecutingAssembly().Location).DirectoryName!;
                TrayIcon icon = new(0, Path.Combine(executablePath, @"trayicon.ico"), "My Worker")
                {
                    IsVisible = true
                };
                var showWindow = static () => {
                    ReactorApp.PrimaryWindow?.Activate();
                    ReactorApp.PrimaryWindow?.NativeWindow.SetForegroundWindow();
                    ReactorApp.PrimaryWindow?.AppWindow.IsShownInSwitchers = true;
                };
                // Left click on the tray icon will show the window:
                icon.Selected += (_, _) => showWindow();

                // Context menu options:
                icon.ContextMenu += (_, e) =>
                {
                    MenuFlyout flyout = new();
                    flyout.Items.Add(new MenuFlyoutItem() { Text = "Open" });
                    ((MenuFlyoutItem)flyout.Items[0]).Click += (_, _) => showWindow();

                    flyout.Items.Add(new MenuFlyoutItem() { Text = "Quit" });
                    ((MenuFlyoutItem)flyout.Items[1]).Click += (_, _) =>
                    {
                        isQuitting = true;
                        ReactorApp.PrimaryWindow?.Close();
                        icon.Dispose();
                        stopHost.Invoke();
                    };
                    e.Flyout = flyout;
                };

                // Prevent closing out the window and just hide to tray instead unless we're quitting
                host.Window.AppWindow.Closing += (_, e) =>
                {
                    e.Cancel = !isQuitting;
                    settingsWindow?.Hide();
                };

                // If the window is minimized, we don't want it to show in the Alt+Tab switcher + taskbar,
                // so we set IsShownInSwitchers to false when minimized.
                WindowManager manager = WindowManager.Get(host.Window);
                manager.WindowStateChanged += (_, state) =>
                {
                    bool isVisible = state != WinUIEx.WindowState.Minimized;
                    manager.AppWindow.IsShownInSwitchers = isVisible;
                };
            });
    });
}

And that's it! We now have the framework for a tray-based WinUI/Reactor application.

You can find the full application code here: https://github.com/dotMorten/ReactorExperiments/tree/main/TrayApp

File-based WinUI apps with Microsoft.UI.Reactor

One of the things I've been wanting to try for a while was whether Reactor could work nicely with .NET file-based apps.

Turns out: it can.

The idea is pretty simple. Instead of creating a full project, you put everything in a single App.cs file, add a few #: directives at the top, and run it directly with `dotnet run`.

The smallest example

Here's a complete Reactor app in one file:

#:property TargetFramework=net10.0-windows10.0.22621.0
#:property WindowsAppSDKSelfContained=true
#:package Microsoft.UI.Reactor@0.1.0-*

using Microsoft.UI.Reactor;
using Microsoft.UI.Reactor.Core;
using static Microsoft.UI.Reactor.Factories;

ReactorApp.Run<App>("SingleFileReactor", width: 900, height: 600);

class App : Component
{
    public override Element Render()
    {
        var (name, setName) = UseState("World");

        return VStack(
            Heading($"Hello, {name}!"),
            TextBox(name, setName, placeholderText: "Your name")
                .AutomationName("NameInput")
        ).Padding(16);
    }
}

Save that as `App.cs`, then run: dotnet run App.cs -a x64

The -a x64 part matters here. Since the script uses WindowsAppSDKSelfContained=true, you need to tell the build which Windows architecture you want (you could also add #:property RuntimeIdentifier=win-x64 to the header instead)

And that's really it. You get a native desktop window with a heading and a text box, and as you type your name, the greeting updates live.

What the directives do

The three lines at the top do most of the magic:

#:property TargetFramework=net10.0-windows10.0.22621.0
#:property WindowsAppSDKSelfContained=true
#:package Microsoft.UI.Reactor@0.1.0-*

The target framework makes this a Windows app targeting WinUI.

WindowsAppSDKSelfContained=true makes sure the Windows App SDK bits are available the way the app expects.

And the package line is just a normal NuGet dependency, except declared inline for the file-based app model.

A normal Reactor app created from the project template is still the right place to start for a "real" app. You get a .csproj, a proper project structure, and something you can keep growing.

But file-based apps open up a different kind of scenario.

Sometimes you don't want to start a project. Sometimes you just want a little tool.

Maybe you want:

  • a quick internal utility
  • a tiny prototype
  • a one-off helper app
  • a script that sometimes needs a proper interactive UI

That's where this gets really fun.

Mixing scripting with a real UI

I often use file-based apps for running little scripts with C# instead of using Batch or Bash. This got me the idea that you can create a script that can either run in plain console mode, or pop a UI if you ask for interactive mode.

Here's a cleaned up version of that:

#:property TargetFramework=net10.0-windows10.0.22621.0
#:property WindowsAppSDKSelfContained=true
#:package Microsoft.UI.Reactor@0.1.0-*

using Microsoft.UI.Reactor;
using Microsoft.UI.Reactor.Core;
using Microsoft.UI.Xaml;
using static Microsoft.UI.Reactor.Factories;

if (args.Length > 0 && (args[0] == "-i" || args[0] == "--interactive"))
{
    Console.WriteLine("Waiting for user to enter their name...");
    ReactorApp.Run<EnterUsernameApp>("Username", width: 300, height: 130);
}
else
{
    Console.WriteLine("Enter your user name:");
    AppState.Username = Console.ReadLine() ?? string.Empty;
}

if (string.IsNullOrEmpty(AppState.Username))
{
    Console.WriteLine("No username entered.");
    return -1;
}

Console.WriteLine($"Hello {AppState.Username}!");
return 0;

class EnterUsernameApp : Component
{
    public override Element Render()
    {
        var (name, setName) = UseState("");

        return VStack(
            TextBox(name, setName, placeholderText: "Enter username")
                .AutomationName("UsernameInput"),
            Button("Accept", () =>
                {
                    AppState.Username = name;
                    ReactorApp.PrimaryWindow?.Close();
                })
                .IsEnabled(!string.IsNullOrEmpty(name))
                .HAlign(HorizontalAlignment.Stretch)
        ).Padding(10);
    }
}

static class AppState
{
    public static string Username { get; set; } = string.Empty;
}

Run it in console mode: dotnet run App.cs -a x64
Or launch the UI mode: dotnet run App.cs -a x64 -- -i

If you're writing automation or a developer tool, that can be incredibly useful. Most of the time maybe the script can stay headless and work in the terminal. But if the user needs to make a choice, enter a value, or confirm something in a friendlier way, you can just pop a real window. Another example could be that you can either provide filenames as arguments, or if you don't provide this as argument, a UI allowing you to browse to the required files instead.

Is this how you should build every app?

Nope! Once the app grows beyond a quick tool or prototype, a normal project structure is still going to be much easier to maintain.

But for small utilities? Demos? Quick experiments? Interactive scripts, this is really nice.

I especially like that it lowers the bar for building little native Windows helpers. If I can keep a whole app in one file and still get a proper WinUI window, I'm far more likely to reach for a native UI instead of settling for a clunky prompt loop.

And that's probably the biggest compliment I can give this setup: it makes a real Windows UI feel cheap enough to use for the small stuff too.

Your first Microsoft.UI.Reactor app

Now that Microsoft.UI.Reactor and the project templates are on NuGet.org, getting started is a LOT simpler than it used to be.

If you want the absolute shortest path to a running app, it's really just this (assuming you already installed the .NET SDK):

dotnet new install Microsoft.UI.Reactor.ProjectTemplates
dotnet new reactorapp
dotnet run -a x64

That's it!

So let's take a quick look at what those three commands actually do, and more importantly what kind of app Reactor generates for you.

Creating the app

The first command installs the template:

dotnet new install Microsoft.UI.Reactor.ProjectTemplates

You only need to do this the first time.

Next:

dotnet new reactorapp

If you run that in an empty folder, the template uses the folder name as the project name and drops the generated files right there.

And finally:

dotnet run -a x64

That builds and launches the app with the x64 architecture, which is generally the right thing to do for a WinUI desktop app (if you are on ARM64 you can use arm64 instead).

If you prefer creating the app in a named folder instead, you can also do:

dotnet new reactorapp -n MyFirstReactorApp
cd MyFirstReactorApp
dotnet run -a x64

Project Contents

One of the nice things about the template is that it stays very small. You're not dropped into a huge starter app with a lot of moving parts. The interesting bits are basically just:

- App.cs
- <ProjectName>.csproj

And honestly, that's a pretty good first impression for Reactor, because the whole point is that your UI is just C# code.

Let's start with App.cs

The generated app looks like this:

using System;
using Microsoft.UI.Reactor;
using Microsoft.UI.Reactor.Core;
using Microsoft.UI.Reactor.Layout;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using static Microsoft.UI.Reactor.Factories;

ReactorApp.Run<App>("MyFirstReactorApp", width: 900, height: 600);

class App : Component
{
  public override Element Render()
  {
    var (name, setName) = UseState("World");

    var titleBar = TitleBar("MyFirstReactorApp").Flex(shrink: 0);

    var body = Border(
      FlexColumn(
        Heading($"Hello, {name}!"),
        TextBox(name, setName, placeholderText: "Your name")
           .AutomationName("NameInput")
      ) with { RowGap = 16 }
    ).Padding(24).Flex(grow: 1, basis: 0);

    return FlexColumn(titleBar, body)
        .Backdrop(BackdropKind.Mica);
    }
}


This is pretty simple, but at the same time there's actually a lot going on here, so let's break down the basics.
The very first interesting line is this one:

ReactorApp.Run<App>("MyFirstReactorApp", width: 900, height: 600);

This is the app bootstrap. Reactor opens the window, hosts the app, and renders your root component. If you're used to XAML app startup plumbing, this is refreshingly simple.

Then we get to the App component itself:

class App : Component
{
    public override Element Render()

The Render methods are the key to Reactor. You don't create a window and start mutating controls. Instead you render an element tree that describes what the UI should look like right now, and it returns a light-weight description of the UI - not the UI objects themselves.

The next line is probably the most important one in the whole sample, and if you're coming from XAML probably the most unfamiliar one:

var (name, setName) = UseState("World");

UseState is one of many "hooks" you'll find in Reactor. You have some state. You render UI from that state. Events update the state. Reactor re-renders and patches the native WinUI controls in place.

The sample keeps this super simple: the initial value is `"World"`, so the heading renders as:

Heading($"Hello, {name}!")

which means the app starts out showing `Hello, World!` using a Textblock with the Heading style. The "Heading" is merely a simple short-cut for creating a TextBlock with the style preset, to keep your code simple and concise. There are a bunch of these short-hand statements like HStack and VStack for Horizontal and Vertical StackPanels.

Then the text box wires straight into that same state:

TextBox(name, setName, placeholderText: "Your name")

This is a nice little detail because it immediately shows the controlled-input model. As you type into the text box, `setName` gets called, the component renders again, and the heading updates live.

So type `Reactor`, and the heading becomes `Hello, Reactor!`.

That's a tiny app, sure, but it's also the whole Reactor loop in one screen.

The rest of the file is mostly layout and presentation.

var titleBar = TitleBar("MyFirstReactorApp").Flex(shrink: 0);

gives you a title bar, while:

FlexColumn(...)
Border(...).Padding(24)

build up the body using ordinary C# composition instead of XAML markup.

And finally:

.Backdrop(BackdropKind.Mica);

adds a Mica backdrop so the sample already feels like a proper Windows app.

That last part is worth calling out: Reactor isn't drawing some fake custom UI surface. It's building real WinUI UI under the covers.

The project file is small too

The generated project file is also pretty lean:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net10.0-windows10.0.22621.0</TargetFramework>
    <Platforms>ARM64;X86;X64</Platforms>
    <UseWinUI>true</UseWinUI>
    <WindowsPackageType>None</WindowsPackageType>
    <WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.WindowsAppSDK" Version="..." />
    <PackageReference Include="Microsoft.UI.Reactor" Version="..." />
  </ItemGroup>
</Project>

Again, nothing wild here, and that's a good thing.

UseWinUI makes this a WinUI project, WindowsPackageType is set to None so you start with an unpackaged desktop app, and Microsoft.UI.Reactor just comes in as a normal NuGet package.

That's really the big story here: Reactor now feels like a regular part of the .NET ecosystem. Install a template, create a project, run it. Edit it in Visual Studio, VS Code, or just Notepad for a bit of personal torture. Or have your favorite agent iterate on it. Because everything is code, you're more likely to get compile time errors instead of XAML runtime errors which really helps with both your and an agent's inner dev-loop.

What you get when it runs

When the app starts up, you get a small native desktop window with a title bar, a greeting, and a text box asking for your name.

Type in the box, and the heading updates immediately.

That might not sound like much, but it's actually a very good first sample because it teaches the right thing right away: the UI is a function of state.

No XAML. No view models. No binding setup. Just state in, UI out.

A lot of starter templates try too hard to impress you. They throw in navigation, settings pages, MVVM layers, mock data, half a dozen folders, and a bunch of code you're supposed to delete later.

This one doesn't.

It gives you one component, one piece of state, one layout, and one interaction. Just enough to understand the model, and not so much that you have to reverse engineer the template before you can start building your own app.

And that's probably exactly what a first Reactor app should be.

Where to next?

If you want learn more start with the Build 2026 presentation which covers a lot of the basics:

- Building WinUI Apps with C# First Patterns and AI Assisted Workflows

Next check out the official Reactor Documentation, but I'd like to call out a few key pages to study:

Next go study the samples. Here are a few I found interesting:

  • Reactor Gallery - Lots of great samples in one big demo app. Run this app and study each sample.
  • Validation Showcase - Great example of how to do even intricate input validation with very little code.
  • Reactor IDE - A Visual-Studio style app demonstrating docking/draggable windows.
  • Particle Storm - High-performance particle rendering using Win2D.

Automatically getting API difference diagrams in your .NET PRs

A while back I built a tool that could analyze .NET Source Files as well as assemblies, and generate an Object Model Diagram. I found having OMDs of the APIs you're working on is a really great way to quickly get an overview of what an API looks like and how you could work with it. The public API surface should generally not change once you ship it, so getting it right the first time is important. In addition it was able to also compare two assemblies or two sets of source folders and just give you the difference. This allowed you to review just what is getting added, and if there are any breaking changes that might not have been intended. The end goal was to one day have these OMDs in the PR itself. You can get this tool today as a dotnet tool by running the following command:

   dotnet tool install --global dotMorten.OmdGenerator

For a long time, I’d wanted one specific feature in my .NET Object Model Diagram Generator: the ability to compare API surface directly between git refs. This would simplify comparing against a PR.

As mentioned, the tool could already compare one set of source files against another, and it could generate a clean object model diff from that. But what I really wanted was something more natural for real-world development: compare the code in my working tree to a tag, compare one branch to another, or compare two commits from a remote repository without having to manually check out folders, create temp copies, or script around the tool.

It was one of those ideas that sat on my “I should really add this someday” list for years.

This time, I finally built it — mostly because with AI agents now it's so much faster to iterate on without having to do all the manual work of actually typing :-)  So with GitHub Copilot helping me work through the design, implementation, tests, and documentation, I was able to add the feature much faster than I would have otherwise. Copilot didn’t magically do the work for me, but it absolutely helped me turn a long-delayed idea into a finished feature.

What the new feature does

The generator can now compare source code against a specific commit, branch, or tag.

That means you can now:

  • compare your current checkout against a release tag
  • compare two branches
  • compare two commits
  • compare two refs from a remote git repository

The key new arguments are:

  • gitRepo — a local repository path or remote git URL
  • sourceRef — the git ref for the “new” side of the diff
  • compareRef — the git ref for the “old” side of the diff
  • source — the source path to analyze

The important detail is that source still tells the tool what part of the repository to analyze. The git ref options tell it which versions of that source to compare.

Comparing API changes between two refs

Here’s the simplest case: compare your current source tree against a tag from the same repository.

generateomd --source=C:\github\dotnet\runtime\src\libraries\System.Text.Json\src --compareRef=v8.0.0 --format=html

That says:

  • analyze the current contents of System.Text.Json
  • compare them against the v8.0.0 tag
  • emit html output (you can use md if you want markdown format)

If you want to compare two refs from a remote repository, you can do that too:

generateomd --source=src/libraries/System.Text.Json/src --gitRepo=https://github.com/dotnet/runtime.git --sourceRef=main --compareRef=v8.0.0 --format=md

In this case:

  • gitRepo points to the remote repo
  • source is now a repo-relative path
  • sourceRef is the “new” side
  • compareRef is the baseline

You can also compare two commits directly:

generateomd --source=src/MyLibrary --gitRepo=https://github.com/your-org/your-repo.git --sourceRef=9f4f4cf --compareRef=28630aaf8d27367248358ef064b57d6214b0f103 --format=md

That gives you a markdown API diff suitable for release notes, PR discussions, or review workflows.

Why this is useful

This ends up being much more practical than folder-to-folder comparisons.

Instead of manually exporting source trees or checking out branches into separate directories, you can compare API
shape straight from version control. That makes it far easier to answer questions like:

  • What public API changed in this pull request?
  • What changed between this release tag and main?
  • Did this commit actually alter the public surface area?
  • Are these changes additive, breaking, or just refactorings?

For a tool built around API visualization and change tracking, git-based comparison feels like the missing piece.

Using it in GitHub Actions

Once the tool can compare two git refs, the next obvious step is automation.

A really nice workflow is to generate an API diff for every pull request and post it as a PR comment. That gives reviewers a focused view of API changes without having to inspect every file manually.

The flow looks like this:

  1. trigger on pull_request
  2. compare the PR head SHA to the PR base SHA
  3. generate markdown output
  4. detect whether the markdown actually contains API changes
  5. create or update a PR comment with the result

At a high level, the command looks like this inside the workflow:

generateomd \
--source=src/MyLibrary \
--gitRepo=${{ github.server_url }}/${{ github.repository }} \
--sourceRef=${{ github.event.pull_request.head.sha }} \
--compareRef=${{ github.event.pull_request.base.sha }} \
--format=md \
--output=api-diff

This is a great fit for pull requests because GitHub already gives you the exact two SHAs you care about:

  • github.event.pull_request.head.sha
  • github.event.pull_request.base.sha

That means the comment always reflects the actual API delta introduced by the PR.

The important GitHub Action pieces

Here are the parts of the workflow that matter most.

 

1. Install the tool

- uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x

- run: dotnet tool install --global dotMorten.OmdGenerator

That makes generateomd available in the workflow.

2. Generate markdown output

- name: Generate API diff
run: |
generateomd \
--source=src/MyLibrary \
--gitRepo=${{ github.server_url }}/${{ github.repository }} \
--sourceRef=${{ github.event.pull_request.head.sha }} \
--compareRef=${{ github.event.pull_request.base.sha }} \
--format=md \
--output=api-diff

The output is written to api-diff.md.

3. Only comment when there’s an actual API diff

The workflow checks whether the generated markdown contains any namespace entries:

- name: Check whether API changes were found
id: api_diff
run: |
if grep -q '^namespace ' api-diff.md; then
echo "has_changes=true" >> "$GITHUB_OUTPUT"
else
echo "has_changes=false" >> "$GITHUB_OUTPUT"
fi

That means the bot won’t spam pull requests when nothing in the API surface actually changed.

Auto-updating the original PR comment

This is the part I especially like.

The workflow uses a hidden marker in the PR comment body, such as:

<!-- dotnet-omd-api-diff -->

When the action runs again, it looks for an existing bot comment containing that marker. If it finds one, it updates that comment instead of creating a new one.

That gives you two nice behaviors:

  • if new commits change the API diff, the original comment is refreshed.
  • if the API changes are later reverted, the existing comment can be updated to say that no API changes are detected any longer.

That second behavior matters a lot. It means the bot comment tracks the current truth of the PR, not just the first interesting state it saw.

In practice, the logic becomes:

  • if there are API changes and no prior comment, create one
  • if there are API changes and a prior comment exists, update it
  • if there are no API changes but a prior comment exists, update it to say so
  • if there are no API changes and no prior comment exists, do nothing

That keeps noise low while still making the automation feel smart and stateful.

Why this workflow is valuable

I like this approach because it adds useful review context without adding much maintenance overhead.

Reviewers get a dedicated summary of API changes. Authors get immediate feedback if they accidentally alter public surface area. And because the comment updates in place, the pull request stays clean instead of filling up with bot spam every time a new commit is pushed.

It also turns the tool from something you run manually into something that can enforce visibility around API changes as part of normal team workflow.

 

Closing

This git-based diff support makes the Object Model Diagram Generator much more useful in day-to-day development. Comparing API shape between commits, branches, and tags is now built in, and the GitHub Actions integration makes it easy to surface those changes directly in pull requests.

You can see the full github action documented here: https://github.com/dotMorten/DotNetOMDGenerator/tree/main#github-actions-comment-pr-api-changes 

And here's an example of one of my repos now using this action to validate PRs: https://github.com/dotMorten/WinUIEx/blob/main/.github/workflows/apidiff.yml 

Removing Memory Allocations in HTTP Requests Using ArrayPool<T>

I recently discovered, that when I did network requests to download data, often memory allocations would be up to over 4 times that of the download size, and thus I logged a bug in the .NET repo. This led to some interesting discussion and discovery. CPU Performance isn't a huge concern for network requests since the network itself would by far be the limiting factor, but if we allocate huge chunks of memory, we also put a lot of load on the Garbage Collector, so there is definitely some allocation overhead that could be improved. Thanks to the discussions in that issue, and pointers to Microsoft's own solutions, I wanted to summary the findings and solution here, to help anyone else wanting to improve their networking code. It's also a great example of how you can use ArrayPool<T> to optimize your allocations.

To set out benchmarking some code, I created a simple little webserver that serves out exactly 1Mb responses:

using var listener = new HttpListener();
listener.Prefixes.Add("http://localhost:8001/");
listener.Start();
CancellationTokenSource cts = new CancellationTokenSource();
_ = Task.Run(() =>
{
    byte[] buffer = new byte[1024*1024];
    cts.Token.Register(listener.Stop);
    while (!cts.IsCancellationRequested)
    {
        HttpListenerContext ctx = listener.GetContext();
        using HttpListenerResponse resp = ctx.Response;
        resp.StatusCode = (int)HttpStatusCode.OK;
        using var stream = resp.OutputStream;
        stream.Write(buffer, 0, buffer.Length);
        resp.Close();
    }
});

 Now next was set up a simple benchmark that measures allocations. I use BenchmarkDotNet, and since I'm focused on allocations and not the actual execution time, I'm using a ShortRunJob. Example:

[MemoryDiagnoser]
[ShortRunJob(RuntimeMoniker.Net80)]
public class HttpBenchmark
{
    readonly HttpClient client = new HttpClient();
    const string url = "http://localhost:8001/data.bin"; // 1,000,000 byte file
	
    [Benchmark]
    public async Task<byte[]> Client_Send_GetByteArrayAsync()
    {
        using var request = new HttpRequestMessage(HttpMethod.Get, url);
        using var response = await client.SendAsync(request).ConfigureAwait(false);
        var bytes = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
        return bytes;
    }
}


The obvious expectation here is I'm downloading 1Mb, so the allocation should likely be only slightly larger than that. However to my surprise the benchmark reported the following:

| Method                                    | Gen0      | Gen1      | Gen2      | Allocated  |
|------------------------------------------ |----------:|----------:|----------:|-----------:|
| Client_Send_GetByteArrayAsync             | 1031.2500 | 1015.6250 | 1000.0000 | 5067.52 KB |

That's almost 5mb (!!!) allocated just to download 1mb!

For good order, I also tried using the MUCH simpler HttpClient.GetByteArrayAsync method. This method is pretty basic, and doesn't really give you much control of the request or how to process the response, but I figured it would be worth comparing:

    [Benchmark]
    public async Task<byte[]> GetBytes()
    {
        return await client.GetByteArrayAsync(url).ConfigureAwait(false);
    }

This gave a MUCH better result, around the expected allocations:

| Method                                    | Gen0      | Gen1      | Gen2      | Allocated  |
|------------------------------------------ |----------:|----------:|----------:|-----------:|
| Client_Send_GetByteArrayAsync             | 1031.2500 | 1015.6250 | 1000.0000 | 5067.52 KB |
| GetByteArrayAsync                         |  179.6875 |  179.6875 |  179.6875 | 1030.59 KB |


So I guess that means, if you can use this method, use it. And we're done right? (Spoiler: Keep reading since we eventually will make this WAY better!)

First of all, I'm not able to use HttpClient.GetByteArrayAsync method. I needed the SendAsync method that provides the HttpRequestMessage overload to tailor the request beyond a simple GET request, and I also want to be able to work with the full HttpResponse even if the request fails with an HTTP Error code (a failed request can still have a body you can read, for example what you see on a 404 Page), or do early rejection of the request before the response has completed. So what's going on here? The more efficient method will probably also have clues to what it does that is different.

Next I changed my benchmark slightly, using the HttpCompletionOption ResponseHeadersRead, which allows me to get the HttpResponse before the actual content has been read, and only the headers have been parsed. My thinking is, I'll be able to get at the head of the stream before any major allocations are made and read through the data.

   [Benchmark]
   public async Task SendAsync_ResponseHeadersRead_ChunkedRead()
   {
       var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
       using var message = await client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
       using var stream = message.Content.ReadAsStream();
       byte[] buffer = new byte[4096];
       while (await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false) > 0)
       {
           // TODO: Process data chunked
       }
   }

The results now look MUCH more promising:

| Method                                    | Gen0      | Gen1      | Gen2      | Allocated  |
|------------------------------------------ |----------:|----------:|----------:|-----------:|
| Client_Send_GetByteArrayAsync             | 1031.2500 | 1015.6250 | 1000.0000 | 5067.52 KB |
| GetByteArrayAsync                         |  179.6875 |  179.6875 |  179.6875 | 1030.59 KB |
| SendAsync_ResponseHeadersRead_ChunkedRead |    7.8125 |         - |         - |   52.02 KB |


This tells us there is a way to process this data without a huge overhead, by simply copying out byte by byte the data as it's coming down the network. If we know the length of the response, we can just copy the content into a byte array and our allocation with be on-par with GetByteArrayAsync, and return the data then. In fact if we dig into how GetByteArray is implemented, we get the clue that that is what's going on: HttpClient.cs#L275-L285 

The code comment literally says if we have a content-length we can just allocate just what we need and return that - if we don't, we can use ArrayPools instead to reduce allocations while growing the array we want to return. It also refers to an internal `LimitArrayPoolWriteStream` that can be found here: HttpContent.cs#L923

It very cleverly uses ArrayPools to reuse a chunk of byte[] memory, meaning we don't have to constantly allocate memory for the garbage collector to clean up, but instead reuses the same chunk of memory over and over. Networking is a perfect place to use this sort of thing, because the number of simultaneous requests you got going out is limited by the network protocol, so you will never rent a large amount of byte arrays at the same time. So why don't we just take a copy of that LimitArrayPoolWriteStream and use it for our own benefit? The code copies out pretty much cleanly except for `BeginWrite` and `EndWrite`, and two exception helpers missing, but since I don't plan on using the two methods, I just removed those two, and replaced the call to the exception helpers with

throw new ArgumentOutOfRangeException("_maxBufferSize");

and code compiles fine.

This means we can now create a request that is able to access the entire byte array via its ArraySegment<byte> GetBuffer() method and work on it as a whole, before releasing it back to the pool. As long as you remember to dispose your LimitArrayPoolWriteStream, you'll be good, and we won't allocate ANY memory for the actual downloaded data. How cool is that?

    [Benchmark]
    public async Task SendAsync_ArrayPoolWriteStream()
    {
        var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
        var message = await client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);

        using var data = new LimitArrayPoolWriteStream(int.MaxValue, message.Content.Headers.ContentLength ?? 256);
        await message.Content.CopyToAsync(data).ConfigureAwait(false);
        var buffer = data.GetBuffer();
        foreach(byte item in buffer)
        {
            // Work on the entire byte buffer
        }
    }
| Method                                    | Gen0      | Gen1      | Gen2      | Allocated  |
|------------------------------------------ |----------:|----------:|----------:|-----------:|
| Client_Send_GetByteArrayAsync             | 1031.2500 | 1015.6250 | 1000.0000 | 5067.52 KB |
| GetByteArrayAsync                         |  179.6875 |  179.6875 |  179.6875 | 1030.59 KB |
| SendAsync_ResponseHeadersRead_ChunkedRead |    7.8125 |         - |         - |   52.02 KB |
| SendAsync_ArrayPoolWriteStream            |         - |         - |         - |    6.15 KB |

6.15kb!!! For a 1 megabyte download, and we still have full access to the entire 1 mb of data to operate on at once. Array pools really are magic and allows us to allocate memory without really allocating memory over and over again. ArrayPools are the good old Reduce, Reuse and Recycle mantra but for developers ♻️

Now, there is one thing we can do to improve the first method that allocates 5mb: We can get that down to about 2mb by simply setting the Content-Length header server-side, which allows the internals to be a bit more efficient. But 2mb is still incredibly wasteful though, compared to what is pretty much allocation-free, and you don't have to rely on the server always knowing the content-length it'll be returning. Having a known length, we can just use the ArrayPool right in the read method, and do something much simpler like this, which is just a simplified version of the LimitArrayPoolWriteStream without the auto-growing of the pool if no content-length is available:

    [Benchmark]
    public async Task KnownLength_UseArrayPool()
    {
        using var requestMessage = new HttpRequestMessage(HttpMethod.Get, url);
        using var message = await client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
        using var stream = message.Content.ReadAsStream();
        if (!message.Content.Headers.ContentLength.HasValue)
            throw new NotSupportedException();
        var buffer = ArrayPool<byte>.Shared.Rent((int)message.Content.Headers.ContentLength);
        await stream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
        foreach(var b in buffer)
        {
            // Process data chunked
        }
        ArrayPool<byte>.Shared.Return(buffer);
    }
| Method                                    | Gen0      | Gen1      | Gen2      | Allocated  |
|------------------------------------------ |----------:|----------:|----------:|-----------:|
| KnownLength_UseArrayPool                  |         - |         - |         - |    3.71 KB |

This is definitely very efficient and a simple way to read data, _if_ you know you got a content length in the header.

 I can't claim too much credit for this blogpost. I initially logged a bug with the huge amount of memory allocated - that led to some great discussion which ultimately led me to finding this almost-zero-allocation trick for dealing with network requests. You can read the discussion in the issue here https://github.com/dotnet/runtime/issues/81628. Special thanks to Stephen Toub and David Fowler for leading me to this discovery.

 

Here's also some extra resources to read more about Array Pools:

Adam Sitnit on performance and array pools: https://adamsitnik.com/Array-Pool/
Since that blogpost, the pools only got faster, and you can read more about that here: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6/#buffering

 

Building a side-kick display for your PC with .NET 5

Sidekick

I used to have a little 8" tablet next to my PC that would run a little simple app, and show calendar for today, weather and various other info. It was a neat little thing to have to glance at, and I rarely missed a meeting etc. because I would often glance and get reminded of what's on my to-do list today. However tablets don't like to be plugged in 24/7, so ultimately it died, and I've been missing having something like this for a while.

That's where I got the idea to just plug in a little external display to my PC and have a little app run on that. However, when you plug in another display, it becomes part of your desktop, and I didn't want it to be an area where other apps could go to, or where the app that is meant to launch on it, would go somewhere else, which got me wondering: Can I plug in one of those little IoT displays I had laying around into my PC and drive it with a little bit of code?

oled

These little 1.5" OLED displays are great, and could do the job. The only problem was, this was an SPI display, and my PC doesn't have an SPI port. A bit of searching and I found Adafruit has an FT232H USB-to-SPI board that can do the trick. A quick bit of soldering, and you're good to go (see diagram wiring below): If you want to create a 3D printed case for it, you can download my models from here: https://www.thingiverse.com/thing:4935630 (it's a tight fit so keep those wires short and tight)

ConnectionDiagram

Now I wanted to use .NET IoT since this was the easiest for me to write against, but unfortunately it turned out this specific Adafruit SPI device isn't supported. However the IoT team is awesome, and after reaching out, they couldn't help themselves adding this support (Thank you Laurent Ellerbach who did this on his vacation!). The display module is already supported by .NET IoT, so it was mostly a matter of figuring out what code to write to hook it up correctly. Luckily there are samples there that only needed slight tweaking. So let's get to the code.

Setting up the project

First open a command prompt / terminal, and start by creating the application worker process and add some dependencies we need:

> dotnet new worker
> dotnet add package Iot.Device.Bindings
> dotnet add package Microsoft.Extensions.Hosting.WindowsServices

You'll also want to change the target framework to 'net5.0-windows10.0.19041.0' since we'll be using the Windows APIs further down.

At the time of writing this, the FT232H PR hasn't been merged yet, so download that project from here and add it as a project reference. Hopefully by the time you read this, it'll already be part of the bindings package.

Initializing the display

The thing we'll do is to create the display driver that we need, so let's add a method for that to the Worker.cs file:

private static Iot.Device.Ssd1351.Ssd1351 CreateDisplay()
{
    var devices = Iot.Device.Ft232H.Ft232HDevice.GetFt232H();
    var device = new Iot.Device.Ft232H.Ft232HDevice(devices[0]);
    var driver = device.CreateGpioDriver();            
    var ftSpi = device.CreateSpiDevice(new System.Device.Spi.SpiConnectionSettings(0, 4) { Mode = System.Device.Spi.SpiMode.Mode3, DataBitLength = 8, ClockFrequency = 16_000_000 /* 16MHz */ });
    var controller = new System.Device.Gpio.GpioController(System.Device.Gpio.PinNumberingScheme.Logical, driver);
    Iot.Device.Ssd1351.Ssd1351 display = new(ftSpi, dataCommandPin: 5, resetPin: 6, gpioController: controller);
    return display;
}

 

Next we need to set a range of commands to the display to get it configured and "ready". We'll add another method for that here:

private static async Task InitDisplayAsync(Iot.Device.Ssd1351.Ssd1351 display)
{
    await display.ResetDisplayAsync();
    display.Unlock();
    display.MakeAccessible(); // Command A2,B1,B3,BB,BE,C1 accessible if in unlock state
    display.SetDisplayOff(); // Turn on sleep mode
    display.SetDisplayEnhancement(true);
    display.SetDisplayClockDivideRatioOscillatorFrequency(0x00, 0x0F); // 7:4 = Oscillator Frequency, 3:0 = CLK Div Ratio (A[3:0]+1 = 1..16)
    display.SetMultiplexRatio(); // Use all 128 common lines by default....
    display.SetSegmentReMapColorDepth(Iot.Device.Ssd1351.ColorDepth.ColourDepth262K, 
        Iot.Device.Ssd1351.CommonSplit.OddEven, Iot.Device.Ssd1351.Seg0Common.Column0,
        Iot.Device.Ssd1351.ColorSequence.RGB); // 0x74 Color Depth = 64K, Enable COM Split Odd Even, Scan from COM[N-1] to COM0. Where N is the Multiplex ratio., Color sequence is normal: B -> G -> R
    display.SetColumnAddress(); // Columns = 0 -> 127
    display.SetRowAddress(); // Rows = 0 -> 127
    display.SetDisplayStartLine(); // set startline to to 0
    display.SetDisplayOffset(0); // Set vertical scroll by Row to 0-127.
    display.SetGpio(Iot.Device.Ssd1351.GpioMode.Disabled, Iot.Device.Ssd1351.GpioMode.Disabled); // Set all GPIO to Input disabled
    display.SetVDDSource(); // Enable internal VDD regulator
    display.SetPreChargePeriods(2, 3); // Phase 1 period of 5 DCLKS,  Phase 2 period of 3 DCLKS
    display.SetPreChargeVoltageLevel(31);
    display.SetVcomhDeselectLevel(); // 0.82 x VCC
    display.SetNormalDisplay(); // Reset to Normal Display
    display.SetContrastABC(0xC8, 0x80, 0xC8); // Contrast A = 200, B = 128, C = 200
    display.SetMasterContrast(0x0a); // No Change = 15
    display.SetVSL(); // External VSL
    display.Set3rdPreChargePeriod(0x01); // Set Second Pre-charge Period = 1 DCLKS
    display.SetDisplayOn(); //--turn on oled panel
    display.ClearScreen();
}

This should be enough to start using our panel. Let's modify the ExecuteAsync method to initialize the display and have it start drawing something:

Iot.Device.Ssd1351.Ssd1351? display;

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    display = CreateDisplay();
    await InitDisplayAsync(display);
    try
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            display.FillRect(System.Drawing.Color.Red, 0, 0, 128, 128);
            await Task.Delay(1000, stoppingToken);
            display.FillRect(System.Drawing.Color.White, 0, 0, 128, 128);
            await Task.Delay(1000, stoppingToken);
        }
    }
    catch (OperationCanceledException)
    {
    }
    finally
    {
        display.SetDisplayOff();
    }
}

If you run your app now, you should see something like this:

Untitled Project

If not, check your wiring.

Note that the SetDisplayOff code is quite important. If you kill the app before that code executes, you'll notice that the display will be stuck on whatever last screen you were showing. This isn't good for OLEDs, as it can cause burn-in over time. This also happens if your PC goes into standby, so it's a good idea to turn the display off when standby occurs. It's also a good idea to ensure the display is turned off in the event of an exception, so it's good to have that code in a finally statement.
To detect standby, add a reference to the Windows SDKs by setting <UseWindowsForms>true</UseWindowsForms> in your project's PropertyGroup and it'll bring in the required reference. Then add the following code:

Microsoft.Win32.SystemEvents.PowerModeChanged += (s, e) =>
{
    if (e.Mode == Microsoft.Win32.PowerModes.Suspend)
        display?.SetDisplayOff();
    else if (e.Mode == Microsoft.Win32.PowerModes.Resume)
        display?.SetDisplayOn();
}

 

At this point, we're pretty much all set to make this little display more useful.

Drawing to the display

Let's create a bit of code that writes a message to the screen. We can just use its System.Drawing APIs to draw a bitmap and send it to the device. Let's create a small helper method first to do that:

private void DrawImage(Action<System.Drawing.Graphics, int, int> drawAction)
{
    using System.Drawing.Bitmap dotnetBM = new(128, 128);            
    using var g = System.Drawing.Graphics.FromImage(dotnetBM);
    g.Clear(System.Drawing.Color.Black);
    drawAction(g, 128, 128);
    dotnetBM.RotateFlip(System.Drawing.RotateFlipType.RotateNoneFlipX);
    display.SendBitmap(dotnetBM);
}

Note that we flip the image before sending it, so the display gets the pixels in the right expected order. Next lets modify our little drawing loop, to draw something instead:

DrawImage((g, w, h) =>
{
    g.DrawString("Hello World", new System.Drawing.Font("Arial", 12), System.Drawing.Brushes.White, new System.Drawing.PointF(0, h / 2 - 6));
});
await Task.Delay(1000, stoppingToken);
DrawImage((g, w, h) =>
{
    g.DrawString(DateTime.Now.ToString("HH:mm"), new System.Drawing.Font("Arial", 12), System.Drawing.Brushes.White, new System.Drawing.PointF(0, h / 2 - 6));
});
await Task.Delay(1000, stoppingToken);

 

You should now see the display flipping between showing "Hello World" and the current time of day. You can use any of the drawing APIs to draw graphics, bitmaps etc. For instance I have the worker process monitor my doorbell and cameras, and if there's a motion or doorbell event, it'll start sending snapshots from the camera to the display, and after a short while, switch back to the normal loop of pages.

Drawing a calendar

Let's make the display a little more useful. Since we can access the WinRT Calendar APIs from our .NET 5 Windows app, we can get the calendar appointments from the system. So let's create a little helper method to get, monitor and draw the calendar:


private Windows.ApplicationModel.Appointments.AppointmentStore? store;
private System.Collections.Generic.IList<Windows.ApplicationModel.Appointments.Appointment> events =
    new System.Collections.Generic.List<Windows.ApplicationModel.Appointments.Appointment>();

private async Task LoadCalendar()
{
    store = await Windows.ApplicationModel.Appointments.AppointmentManager.RequestStoreAsync(Windows.ApplicationModel.Appointments.AppointmentStoreAccessType.AllCalendarsReadOnly);
    await LoadAppointments();
    store.StoreChanged += (s, e) => LoadAppointments();
}
private async Task LoadAppointments()
{
    if (store is null)
        return;
    var ap = await store.FindAppointmentsAsync(DateTime.Now.Date, TimeSpan.FromDays(90));
    events = ap.Where(a => !a.IsCanceledMeeting).OrderBy(a => a.StartTime).ToList();
}

private void DrawCalendar(System.Drawing.Graphics g, int width, int height)
{
    float y = 0;
    var titleFont = new System.Drawing.Font("Arial", 12);
    var subtitleFont = new System.Drawing.Font("Arial", 10);
    float lineSpacing = 5;
    foreach (var a in events)
    {
        var time = a.StartTime.ToLocalTime();
        if (time + a.Duration < DateTimeOffset.Now)
        {
            _ = LoadAppointments();
            continue;
        }
        string timeString = a.AllDay ? time.ToString("d") : time.ToString();
        if (time < DateTimeOffset.Now.Date.AddDays(1)) //Within 24 hours
        {
            if (a.AllDay) timeString = time.Date <= DateTimeOffset.Now.Date ? "Today" : "Tomorrow";
            else timeString = time.ToString("HH:mm");
        }
        else if (time < DateTimeOffset.Now.AddDays(7)) // Add day of week
        {
            if (a.AllDay) timeString = time.ToString("ddd");
            else timeString = time.ToString("ddd HH:mm");
        }
        if (!a.AllDay && a.Duration.TotalMinutes >= 2) // End time
        {
            timeString += " - " + time.Add(a.Duration).ToString("HH:mm");
        }
        var color = System.Drawing.Brushes.White;
        if (!a.AllDay && a.StartTime <= DateTimeOffset.UtcNow && a.StartTime + a.Duration > DateTimeOffset.UtcNow)
            color = System.Drawing.Brushes.Green; // Active meetings are green
        g.DrawString(a.Subject, titleFont, color, new System.Drawing.PointF(0, y));
        y += titleFont.Size + lineSpacing;
        g.DrawString(timeString, subtitleFont, System.Drawing.Brushes.Gray, new System.Drawing.PointF(2, y));
        y += subtitleFont.Size + lineSpacing;
        if (y > height)
            break;
    }
}

Most of the code should be pretty self-explanatory, and most of just deals with writing a pretty time stamp for each appointment. Replace the bit of code that draws "Hello World" with this single one line:

     DrawImage(DrawCalendar);

And you should now see your display alternating between time of day, and you upcoming calendar appointments.

Conclusion

As you can tell, once the display and code is hooked up, it's pretty easy to start adding various cycling screens to your display, and you can add anything you'd like that fits your needs.

I'm using my Unifi client library to listen for camera and doorbell events and display camera motions, I connect to home assistant's REST API to get current temperature outside and draw a graph, so I know if weather is still heating up, or cooling down, and whether I should close or open the windows to save on AC. I have a smart meter, so I show current power consumption, which helps me be more aware of trying to save energy, and when a meeting is about to start, I start flashing the display with a count down (I'm WAY better now at joining meetings on time). There are lots and lots more options you could do, and I'd love to hear your ideas what this could be used for. I do hope in the long run, this could evolve into a configurable application with lots of customization and plugins, as well as varying display size.
On my list next is to use a 2.4" LED touch screen instead, to add a bit more space for stuff, as well as the ability to interact with the display directly.

Parts List

- WaveShare 1.5" OLED
- Adafruit has an FT232H USB-to-SPI board

Source code

Just want all the source code for this? Download it here

Using the C#/Win32 code generator to enhance your WinUI 3 app


With the new C#/WIn32 code generator it has become super easy to generate interop code for Windows APIs. Previously we had to go read the doc and figure out the proper C# signature, or browse pinvoke.net and hope it is covered there. We could also reference an external package like PInvoke.Kernel32, PInvoke.User32 etc. etc. but it would also pull in a lot more dependencies and APIs than you probably need.

Now with the code generator, it's as simple as adding the nuget package, and then add the method name you want to a "NativeMethods.txt" file, and you instantly have access to the method and any structs and enums you need. It also uses all the latest C#9 features for improved interop code.

You can read the full blogpost on this here: https://blogs.windows.com/windowsdeveloper/2021/01/21/making-win32-apis-more-accessible-to-more-languages/

 

Using the C# Win32 Interop Generator with WinUI 3

The latest WinUI 3 Preview 4 release does have several improvements around improving your window, but there is also a lot still missing. However most of those features can be added by obtaining the window's HWND handle and calling the native Win32 methods directly.

So let's set out to try and control our window using CsWin32.

 

First, we'll create a new WinUI Desktop project:

 

image

Next we'll add the Microsoft.Windows.CsWin32 NuGet package the project (not the package project!) to get the Win32 code generator. However, note that the current v0.1.319-beta version has some bugs that you'll see when WinUI is referenced. So instead we'll use the daily build which has been fixed (you can skip this if you're reading this and a new version has been published). Go edit the project file, and add the following to get the latest daily from the nuget server that hosts these builds: (also shout-out to Andrew Arnott for quickly fixing bugs as they were discovered)

  <PropertyGroup>
   <RestoreAdditionalProjectSources>https://pkgs.dev.azure.com/azure-public/vside/_packaging/winsdk/nuget/v3/index.json;$(RestoreAdditionalProjectSources)</RestoreAdditionalProjectSources>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.Windows.CsWin32" Version="0.1.370-beta">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

 

Next we'll add a new text file to the root of the project named NativeMethods.txt.

Screenshot 2021-02-19 100955

We're now set up to generate the required methods.


The first Win32 method we need is the ShowWindow method in User32.dll, so add the text ShowWindow to its own line in the NativeMethods.txt file.

Note that you should now be able to get auto completion and full intellisense documentation on the method Microsoft.Windows.Sdk.PInvoke.ShowWindow. Pretty cool huh?

Screenshot 2021-02-19 094339

If you use VS16.9+, you can also expand the Project Name -> Dependencies -> Analyzers -> Microsoft.Windows.CsWin32 -> Microsoft.Windows.CsWin32.SourceGenerator and see the code that gets generated.

Screenshot 2021-02-19 094156

None of this is coming from a big library. It's literally just an internal class directly compiled into your application. No external dependencies required, and if you're building a class library, no dependencies to force on your users either.

 

Controlling the WinUI Window

As you can see in the screenshot above, the first thing we need to do is provide an HWND handle. You can get this from the Window, but it's not obvious, as we need to cast it to an interface we define. Let's create a new static class called WindowExtensions to help us do this:

using System;
using System.Runtime.InteropServices;
using WinRT;

namespace WinUIWinEx
{
    public static class WindowExtensions
    {
        [ComImport]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        [Guid("EECDBF0E-BAE9-4CB6-A68E-9598E1CB57BB")]
        internal interface IWindowNative
        {
            IntPtr WindowHandle { get; }
        }

        public static IntPtr GetWindowHandle(this Microsoft.UI.Xaml.Window window)
            => window is null ? throw new ArgumentNullException(nameof(window)) : window.As<IWindowNative>().WindowHandle;
    }
}

 

Now we want to call the ShowWindow method. If you read the documentation for this method: https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-showwindow you'll see takes a number indicating the desired window state, like 0 for hide, 3 for maximize, 5 for show, 6 for minimize and 9 for restore.

        private static bool ShowWindow(Microsoft.UI.Xaml.Window window, int nCmdShow)
        {
            var hWnd = new Microsoft.Windows.Sdk.HWND(window.GetWindowHandle());
            return Microsoft.Windows.Sdk.PInvoke.ShowWindow(hWnd, nCmdShow);
        }

        public static bool MinimizeWindow(this Microsoft.UI.Xaml.Window window) => ShowWindow(window, 6);
        public static bool MaximizeWindow(this Microsoft.UI.Xaml.Window window) => ShowWindow(window, 3);
        public static bool HideWindow(this Microsoft.UI.Xaml.Window window) => ShowWindow(window, 0);
        public static bool ShowWindow(this Microsoft.UI.Xaml.Window window) => ShowWindow(window, 5);
        public static bool RestoreWindow(this Microsoft.UI.Xaml.Window window) => ShowWindow(window, 9);

 

Because these are all extension methods, this now enables us to make simple calls inside our Window class like this:

       this.MaximizeWindow();

  

The CsWin32 code generator just makes this really easy. I've taken all of this a few levels further and started a WinUIEx project on GitHub, where you can see the above code and much more, like setting your windows to be always-on-top or adding a tray icon to your app, so you can minimize to the tray.

You can find the repo on github here:

https://github.com/dotMorten/WinUIEx/

Building custom XAML control panels

One of my favorite XAML Control primitives is the Panel class. It's what drives Grid, StackPanel, Canvas and many many other controls that contains a set of other controls, and controls layout the children out in the view.

So in my second twitch stream, I walked through creating a custom panel that lays out controls in a grid-like manner, without having to do all the row and column definitions. It mainly focuses on the Arrange and Measure steps in the layout life-cycle, which applies to both WPF, UWP and WinUI (or even Silverlight for that matter ;-). And just for fun I used the latest WinUI 3.0 Alpha release (but it really doesn't matter as the concepts are the exact same - only namespaces differs). 




And please subscribe to my YouTube and Twitch channels!

Building your own version of WPF - Live edition

As a follow-up to my blogpost on compiling WPF back when it was still in preview, I tried something new: Live-streamed cloning, compiling and using a local build of WPF in a new app, and make some (evil) modifications to WPF. Even got to submit a PR to the WPF documentation while going though this.

I've been inspired a lot be many others who've started to live-code on Twitch. It's quite interesting to see their thought process and approaches to solving problems - things you never see in a polished 45min conference presentation where everything is prepped and (hopefully) nothing goes wrong. There's quite a lot to learn from seeing people using tools, shortcuts, tricks and tips to solve their problems.

This was a surprising amount of fun, and I'll definitely be doing this again (and hopefully fix some of the issues I had doing this live). I'm prepping some various level 200-400 topics on XAML development, like custom panel and control development, and hope you'll join me. All of these concepts will apply to both WPF, UWP and WinUI, but I figured let's live dangerously and use the latest WinUI Alpha (and we'll log any issues we find as we go).

You can subscribe to my channel at twitch.tv/dotMorten, and I'll be posting the videos after on youtube.com/dotMorten. 

Here's the recording from yesterday. Feedback and suggestions, as well as ideas for topics to cover would be highly appreciated.