All work

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

IAT Design WPF instruction screen editor with live preview

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

csharp
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

csharp
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

csharp
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

csharp
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

csharp
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.