2026 · in progress
IAT Design (WPF rewrite)
A ground-up rewrite of a 600+ class WinForms IAT authoring client into a maintainable MVVM WPF application. Designer tabs are in place; deploy lands with the coordinated server release.
C#.NET 10WPFMVVMMediatRFluentValidationOPC Packaging

The problem
The original WinForms client shipped real value — researchers at Oxford and Princeton used it — but years of accretion made it expensive to change. God objects talked to the network, the disk, and the display. The product needed a second life without throwing away the domain.
Approach
- Split the solution: IAT.Core, IAT.ViewModels, IAT.Views, composition root in the exe.
- Domain models never open a file, a socket, or a window. Children hold Guids, not object graphs.
- Tabs for Blocks, Layout, Stimuli, Trials, Instructions, Surveys, and Deploy.
- Live previews locked to Layout.Interior aspect via Viewbox — the same calculator drives designer and administered test.
- Three instruction types (text, keyed, mock-item) with derived geometry and designer overrides.
- OPC packages embed images and JSON so a .iat file is the entire test.
- Production WebSocket client: UTF-8 text frames, persistent connections, exponential backoff, transaction completion without tearing down the pipe.
- FluentValidation errors surface in a banner, not a pile of dialogs.
Results
- Designer surface complete: stimuli, blocks, trials, three instruction types, surveys, layout editor, validation, save/open.
- Original client peak memory driven from ~1 GB to 55 MB; the rewrite keeps that discipline.
- Deploy tab maps ServerReport as a DTO — empty payloads cannot blank a populated UI.
- Source: github.com/mkjanda/IAT-Design-WPF (MIT). Coordinated server release expected October 2026.
IAT-Design-WPF on GitHubProduct site
From the source
The aggregate owns referential integrity
IAT.Core/Domain/IatTest.cs
public InstructionScreen? RemoveInstructionScreen(InstructionScreen screen)
{
if (screen is null) return null;
if (!InstructionScreens.Remove(screen))
return null;
_instructionCache.Remove(screen.Id);
foreach (var block in Blocks)
block.InstructionsIds.Remove(screen.Id);
return screen;
}
/// Reset in place so child ViewModels that hold this
/// singleton stay valid; ObservableCollections raise
/// CollectionChanged as items are removed.
public void Reset()
{
Id = Guid.NewGuid();
Name = "New IAT Test";
Stimuli.Clear();
Blocks.Clear();
Trials.Clear();
Keys.Clear();
InstructionScreens.Clear();
}Removing an instruction screen is a domain operation: drop it from the collection, drop the cache entry, and strip the Guid from every block. Children hold Ids, not object graphs. No network, no file I/O, no window.
Composition root
IAT Design WPF/App.xaml.cs
protected override void OnStartup(StartupEventArgs e)
{
var services = new ServiceCollection();
services.AddSingleton<TransactionState>();
services.AddMediatR(cfg =>
cfg.RegisterServicesFromAssembly(
typeof(TransactionSuccessHandler).Assembly));
services.AddSingleton<IWebSocketService, WebSocketService>();
services.AddSingleton<ILayoutCalculatorService, LayoutCalculatorService>();
services.AddSingleton<IProjectPackageService, ProjectPackageService>();
services.AddSingleton<ITestDeploymentService, TestDeploymentService>();
services.AddSingleton<IValidator<IatTest>, IatTestValidator>();
services.AddSingleton<IatTest>();
services.AddSingleton<LayoutViewModel>();
services.AddSingleton<InstructionManagerViewModel>();
services.AddSingleton<DeployManagerViewModel>();
services.AddSingleton<TestDesignerViewModel>();
Services = services.BuildServiceProvider();
new MainWindow().Show();
}One place to wire the graph. Domain singleton shared by every designer tab. Validators, export processors, and the WebSocket client are injected. ViewModels never new up infrastructure.
Text frames, UTF-8, no BOM
IAT.Core/Services/Network/WebSocketService.cs
public async Task SendMessage(object message)
{
await EnsureConnectedAndReceivingAsync();
await using var memStream = new MemoryStream();
var settings = new XmlWriterSettings
{
Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false),
OmitXmlDeclaration = false,
Indent = false
};
using (var writer = XmlWriter.Create(memStream, settings))
new XmlSerializer(message.GetType()).Serialize(writer, message);
await _sendLock.WaitAsync();
try
{
await _socket!.SendAsync(
new ArraySegment<byte>(memStream.ToArray()),
WebSocketMessageType.Text,
endOfMessage: true,
timeoutCts.Token);
}
finally { _sendLock.Release(); }
}
private async Task<bool> TryReconnectAsync(CancellationToken ct)
{
if (_intentionalClose) return false;
_reconnectAttempt++;
var delay = Math.Min(MaxBackoffSeconds,
(int)Math.Pow(2, Math.Min(_reconnectAttempt, 5)));
ConnectionState = WebSocketConnectionState.Reconnecting;
await Task.Delay(TimeSpan.FromSeconds(delay), ct);
await ConnectCoreAsync(ct);
return true;
}Java’s handler only implements handleTextMessage. Binary frames never arrive. XmlSerializer’s default UTF-16 is wrong for text frames. Send is semaphore-guarded; reconnect uses exponential backoff capped at 30s.
Derived layout geometry
IAT.Core/Services/LayoutCalculatorService.cs
public static Rect ComputeTextInstructionsRect(Rect interior)
{
const double pad = 15;
return new Rect(
interior.X + pad, interior.Y + pad,
Math.Max(0, interior.Width - 2 * pad),
Math.Max(0, interior.Height - 2 * pad));
}
public static Rect ComputeKeyedInstructionsRect(
Rect interior, Rect leftKey, Rect rightKey)
{
const double pad = 15;
var top = Math.Max(leftKey.Bottom, rightKey.Bottom) + pad;
var bottom = interior.Bottom - pad;
return new Rect(interior.X + pad, top,
Math.Max(0, interior.Width - 2 * pad),
Math.Max(0, bottom - top));
}
public static Rect ComputeMockItemInstructionsRect(
Rect interior, Rect errorMark, Rect continueInstructions)
{
var top = errorMark.Bottom;
var bottom = Math.Max(top, continueInstructions.Top);
return new Rect(0, top, Math.Max(0, interior.Width),
Math.Max(0, bottom - top));
}Text, keyed, mock-item, and continue regions are functions of the interior and key bottoms. The preview and the administered test share the same math. Designers may override; defaults still come from the calculator.
Map the DTO. Don’t bind it.
IAT.ViewModels/Controls/DeployManagerViewModel.cs
private void OnServerReportChanged(ServerReport report)
{
Application.Current.Dispatcher.Invoke(() =>
{
if (!_isActive) return;
ApplyServerReport(report);
LastSyncText = "just now";
});
}
private void ApplyServerReport(ServerReport report)
{
var hasIats = report.IATReport is { Count: > 0 };
var hasIdentity = !string.IsNullOrWhiteSpace(report.ContactFName)
|| !string.IsNullOrWhiteSpace(report.Organization);
// A mid-transaction reset must never blank the tab.
if (!hasIats && !hasIdentity && DeployedTests.Count > 0)
return;
AccountName = $"{report.ContactFName} {report.ContactLName}".Trim();
AdministrationsRemaining = report.NumAdministrations < 0
? "Unlimited"
: report.NumAdministrations.ToString();
}ServerReport is a serializable payload. The ViewModel applies it on the dispatcher and refuses to replace a populated list with an empty report. XAML never binds to TransactionState.