Desktop, Cross-Platform and Game Development
XAML declares the UI as a tree of elements; {Binding Port} connects a control property to a property of the DataContext.
Concept
Windows Forms Drag-and-drop designer, event handlers in code-behind; quick internal tools
XAML UI, powerful data binding, styles, templates, vector graphics; the classic choice for engineering tools Modern Windows UI (Windows App SDK) Windows, macOS, Android, iOS
One XAML codebase for desktop and mobile (dotnet workload install maui) Avalonia UI (open source) Windows, Linux, macOS, WebAssembly
WPF-like XAML that also runs on Linux — popular for cross-platform tools Blazor Hybrid With MAUI/WPF/WinForms Razor web UI inside a desktop app XAML, Data Binding and MVVM
XAML declares the UI as a tree of elements; {Binding Port} connects a control property to a property of the DataContext.
MVVM (Model–View–ViewModel) separates:
Model — domain data and logic (ECUs, frames, test results).
View — XAML; no logic in code-behind.
ViewModel — state and commands for the view: implements INotifyPropertyChanged (so bindings update), exposes ObservableCollection<T> (so lists update) and ICommand properties (so buttons execute logic and enable/disable themselves via CanExecute).
Because the ViewModel is plain C#, it is unit-testable without a window. The CommunityToolkit.Mvvm package generates the boilerplate ([ObservableProperty], [RelayCommand]) with source generators.
Dependency properties (WPF/WinUI/MAUI's BindableProperty) are properties backed by the framework's property system — they support binding, styling, animation, inheritance and change notification. Custom controls declare them with DependencyProperty.Register.
UI threading: UI elements may only be touched from the UI thread. After await in an event handler the continuation returns to the UI thread automatically (synchronization context, Module 32); from background threads use Dispatcher.Invoke/InvokeAsync.
Architecture: views → ViewModels → application services (via DI, Module 45) → hardware/database adapters. Keep long-running I/O async so the UI never freezes..NET runs on Windows, Linux and macOS (x64, Arm64). Write portable code: Path.Combine, Environment.NewLine, OperatingSystem.IsWindows() checks for platform-specific features (serial port names, registry, P/Invoke), and publish per runtime identifier (dotnet publish -r linux-arm64 --self-contained) for devices such as a Raspberry Pi test controller.
C# is the scripting language of Unity (and Godot 4 via .NET, plus MonoGame/FNA frameworks):
Entities in the scene (car, obstacle, camera) Behaviours attached to objects (MonoBehaviour in Unity): physics body, renderer, controller script
Update() every frame (variable time), FixedUpdate() at a fixed physics step
Rigid bodies, colliders, forces — integrated by the engine Collisions, triggers, UI events, C# events between components
Avoid allocations per frame (GC pauses cause stutter), pool objects, cache component lookups
The same concepts appear in engineering: driving simulators, HIL plant models and 3D visualizations of sensor data.
Why It Matters
Many engineering tools are desktop applications: flashing tools, CAN monitors, calibration editors, end-of-line testers. MVVM keeps them testable, and cross-platform .NET lets the same tool run on a Linux bench controller.
Syntax
<TextBox Text="{Binding Port, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="Connect" Command="{Binding ConnectCommand}" />public string Port { get => _port; set { _port = value; PropertyChanged?.Invoke(this, new(nameof(Port))); } }
public static readonly DependencyProperty IsOnProperty = DependencyProperty.Register(nameof(IsOn), typeof(bool), typeof(LedIndicator));Example
53.1 A ViewModel exercised without a window 53.2 A WPF application: XAML view, ViewModel, command and a dependency property
53.3 Cross-platform checks and a game-style update loop
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
// A ViewModel is plain C#: WPF, WinUI, MAUI and Avalonia bind to the same INotifyPropertyChanged / ICommand contracts,
// so it can be exercised (and unit-tested) without any window.
var vm = new BenchMonitorViewModel();
vm.PropertyChanged += (_, e) => Console.WriteLine($" [binding] {e.PropertyName} changed -> UI re-reads it");
vm.Frames.CollectionChanged += (_, e) => Console.WriteLine($" [binding] Frames {e.Action}, count {vm.Frames.Count}");
Console.WriteLine($"Connect.CanExecute before entering a port: {vm.ConnectCommand.CanExecute(null)}");
vm.Port = "COM3";
Console.WriteLine($"Connect.CanExecute after entering a port: {vm.ConnectCommand.CanExecute(null)}");
vm.ConnectCommand.Execute(null);
vm.OnFrameReceived("18FEF100#0102030405060708");
vm.OnFrameReceived("0CF00400#FF7D7D0000000000");
Console.WriteLine($"Status = {vm.Status}, Title = {vm.Title}");
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected bool Set<T>(ref T field, T value, [CallerMemberName
…<Window x:Class="BenchMonitor.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:BenchMonitor"
Title="{Binding Title}" Height="300" Width="480">
<Window.DataContext>
<local:BenchMonitorViewModel />
</Window.DataContext>
<DockPanel Margin="12">
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<TextBlock Text="Port:" VerticalAlignment="Center" />
<TextBox Width="100" Margin="6,0" Text="{Binding Port, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="Connect" Command="{Binding ConnectCommand}" />
<TextBlock Margin="12,0" VerticalAlignment="Center" Text="{Binding Status}" />
<local:LedIndicator Width="14" Height="14" IsOn="{Binding IsConnected}" />
</StackPanel>
<ListBox Margin="0,8,0,0" ItemsSource="{Binding Frames}" FontFamily="Consolas" />
</DockPanel>
</Window>using System.Windows;
namespace BenchMonitor;
public partial class MainWindow : Window
{
public MainWindow() => InitializeComponent(); // code-behind stays empty in MVVM
}using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
namespace BenchMonitor;
public sealed class RelayCommand(Action execute, Func<bool> canExecute) : ICommand
{
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value; // WPF re-queries CanExecute automatically
remove => CommandManager.RequerySuggested -= value;
}
public bool CanExecute(object? parameter) => canExecute();
public void Execute(object? parameter) => execute();
}
public sealed class BenchMonitorViewModel : INotifyPropertyChanged
{
private string _port = "";
private string _status = "Disconnected";
public event PropertyChangedEventHandler? PropertyChanged;
public ObservableCollection<string> Frames { get; } = ["18FEF100#0102030405060708"];
public ICommand ConnectCommand { get; }
public BenchMonitorViewModel() => ConnectCommand = new RelayCommand(() => Status = "Connected", () => Port.Length > 0 && !IsConnected);
public string Port { get => _port; set
…using System.Runtime.InteropServices;
// Cross-platform: one codebase, runtime checks where behaviour must differ
Console.WriteLine($"OS: {(OperatingSystem.IsWindows() ? "Windows" : OperatingSystem.IsLinux() ? "Linux" : OperatingSystem.IsMacOS() ? "macOS" : "other")}, architecture {RuntimeInformation.OSArchitecture}, runtime identifier {RuntimeInformation.RuntimeIdentifier}");
Console.WriteLine($"framework: {RuntimeInformation.FrameworkDescription}");
Console.WriteLine($"path separator '{Path.DirectorySeparatorChar}', new line {(Environment.NewLine == "\r\n" ? "CRLF" : "LF")}");
string serialPort = OperatingSystem.IsWindows() ? "COM3" : "/dev/ttyUSB0";
Console.WriteLine($"default serial port for this OS: {serialPort}");
Console.WriteLine($"portable path: {Path.Combine("logs", "2026-09-30", "can.asc")}");
// ---- Game-loop concepts (as in Unity or MonoGame): objects, components, fixed-timestep update ----
var world = new World();
var car = world.Spawn("TestCar");
car.Add(new RigidBody { Velocity = 20 }); // m/s
car.Add(new Throttle { Acceleration = 3 }); // m/s^2
car.Add(new SpeedLimiter { MaxSpeed = 25 }); // update order matters: limit AFTER acceler
…Expected Result
The ViewModel raised exactly the notifications a WPF view would react to — including Title, a computed property that depends on Status:
The WPF project was created from the template, the files above were copied in, and it builds (a window can't be shown in this text, but the XAML compiled together with the ViewModel and the custom control):
The OS lines reflect the machine that ran the program. In the simulation the limiter component runs after the throttle in each fixed step, so the speed never exceeds 25 m/s — component update order is a real design decision in game engines too.
Connect.CanExecute before entering a port: False
[binding] Port changed -> UI re-reads it
Connect.CanExecute after entering a port: True
[binding] Status changed -> UI re-reads it
[binding] Title changed -> UI re-reads it
[binding] Frames Add, count 1
[binding] Frames Add, count 2
Status = Connected, Title = Bench Monitor - Connected> dotnet new wpf -n BenchMonitor -o BenchMonitor
The template "WPF Application" was created successfully.
Processing post-creation actions...
Restoring C:\demo\BenchMonitor\BenchMonitor.csproj:
Determining projects to restore...
Restored C:\demo\BenchMonitor\BenchMonitor.csproj (in 87 ms).
Restore succeeded.
> dotnet build BenchMonitor -nologo -v q
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:02.55OS: Windows, architecture X64, runtime identifier win-x64
framework: .NET 10.0.12
path separator '\', new line CRLF
default serial port for this OS: COM3
portable path: logs\2026-09-30\can.asc
frame 1: t=0.5s position 10.00 m, velocity 21.5 m/s
frame 2: t=1.0s position 20.75 m, velocity 23.0 m/s
frame 3: t=1.5s position 32.25 m, velocity 24.5 m/s
event: speed limiter engaged at 25.0 m/s
frame 4: t=2.0s position 44.50 m, velocity 25.0 m/s
frame 5: t=2.5s position 57.00 m, velocity 25.0 m/sCommon Mistakes
Business logic in code-behind event handlers — untestable.
Forgetting to raise PropertyChanged (or raising it with a wrong name) — the UI silently doesn't update.
Replacing an ObservableCollection instead of modifying it, or modifying it from a background thread.
Blocking the UI thread with synchronous I/O (.Result on a serial read).
Hard-coded \ paths and COM ports in "cross-platform" code.
Allocating in Update() every frame in games.
Best Practices
MVVM with DI; CommunityToolkit.Mvvm to remove boilerplate.
Async commands for I/O; report progress with IProgress<T>.
Unit-test ViewModels; keep views declarative.
Abstract platform-specific services behind interfaces.
Choose Avalonia or MAUI when Linux/mobile targets matter, WPF/WinUI for Windows-only tools.
Interview Relevance
Medium (high for desktop roles): "explain MVVM", "what does INotifyPropertyChanged do?", "what is a dependency property?", "how do you update the UI from a background thread?", "WPF vs WinForms vs MAUI?".
Practice Questions
Add a DisconnectCommand whose CanExecute is the inverse of ConnectCommand.
Rewrite the ViewModel with CommunityToolkit.Mvvm attributes.
Add an IProgress<int> to a simulated flash operation and show it in a ProgressBar.
Extend the game loop with a Brake component triggered by an obstacle event.
Quick Quiz
Q1. (Multiple choice) Which interface makes a ViewModel property change visible to bindings? A) ICommand B) INotifyPropertyChanged C) IDisposable D) IObservable<T> Answer: B.
Q2. (True/False) A ViewModel can be unit-tested without creating any window.
Answer: True (verified: the ViewModel ran in a console program).
Q3. (Predict the output) Connect.CanExecute before and after setting Port = "COM3".
Answer: False, then True (verified).
Q4. (Find the bug) Frames.Add(frame) is called from a serial-port background thread in a WPF app and throws.
Answer: Collections bound to the UI must be changed on the UI thread: Application.Current.Dispatcher.Invoke(() => Frames.Add(frame)), or marshal data via a channel to the UI thread.
Coding Challenge
Write a minimal ObservableObject base class with a Set helper, and a CounterViewModel with a Count property and an IsEven computed property that is raised when Count changes; print the notifications for three increments.
using System.ComponentModel;
using System.Runtime.CompilerServices;
var vm = new CounterViewModel();
vm.PropertyChanged += (_, e) => Console.WriteLine($"{e.PropertyName} changed (Count={vm.Count}, IsEven={vm.IsEven})");
for (int i = 0; i < 3; i++) vm.Increment();
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void Raise([CallerMemberName] string? name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
protected bool Set<T>(ref T field, T value, [CallerMemberName] string? name = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
Raise(name);
return true;
}
}
public sealed class CounterViewModel : ObservableObject
{
private int _count;
public int Count { get => _count; private set { if (Set(ref _count, value)) Raise(nameof(IsEven)); } }
public bool IsEven => Count % 2 == 0;
public void Increment() => Count++;
}