A Deep-Dive with Real Chrona Code Examples

Many WPF applications begin with direct MessageBox.Show(...) calls.

At first, this feels completely reasonable. A save confirmation here, an error dialog there, a warning before deletion. The implementation is fast, obvious, and immediately functional.

As the application grows, however, this becomes an architectural problem.

Once MessageBox.Show(...) is scattered across ViewModels, services, and workflow logic, UI behavior becomes tightly coupled to application behavior. Every feature decides independently how dialogs should work. Every ViewModel depends directly on WPF dialog primitives. Every future redesign requires widespread refactoring.

Chrona takes a different approach.

Instead of coupling feature code directly to WPF dialogs, the application routes all dialog interaction through a layered abstraction:

Feature Code
    ↓
IDialogService
    ↓
IMessageDialogHost
    ↓
CustomDialogHost
    ↓
CustomMessageDialogWindow

This separation is architecturally strong because the business and workflow layers no longer depend on concrete UI rendering details.

The result is a dialog system that is:

  • replaceable
  • testable
  • design-consistent
  • future-proof
  • independent from direct WPF primitives

That matters far more in production systems than most MVVM tutorials acknowledge.


The Real Problem with MessageBox.Show

A direct message box call looks harmless:

MessageBox.Show(
    "Entry saved successfully.",
    "Information",
    MessageBoxButton.OK,
    MessageBoxImage.Information);

The problem is not the individual call.

The problem is architectural spread.

Once dozens or hundreds of these calls exist across a desktop application, several issues emerge:

ViewModels become tied to WPF types.
Dialog design becomes inconsistent.
Testing becomes harder.
Modal behavior becomes fragmented.
Future redesigns require mass refactoring.

Even worse, the application logic starts speaking in framework-specific terminology.

A ViewModel should not need to know what MessageBoxButton.YesNoCancel means. The ViewModel should only express intent:

I need a confirmation.
I need to show an error.
I need to notify the user.

How that intent is rendered visually should be infrastructure behavior.

Chrona separates exactly these concerns.


Replacing WPF Types with Application Semantics

The first important architectural step in Chrona is removing WPF dialog types from the application-facing API.

Instead of exposing MessageBoxButton, MessageBoxImage, or MessageBoxResult, the application defines its own dialog semantics.

namespace Chrona.Services.App.Interfaces;

public enum DialogButtons
{
    Ok,
    OkCancel,
    YesNo,
    YesNoCancel
}
namespace Chrona.Services.App.Interfaces;

public enum DialogIcon
{
    None,
    Information,
    Warning,
    Error,
    Question
}
namespace Chrona.Services.App.Interfaces;

public enum DialogResult
{
    None,
    Ok,
    Cancel,
    Yes,
    No
}

This is an extremely important architectural move.

The ViewModel and service layers now speak in application language instead of WPF language.

That creates major long-term flexibility.

The application no longer depends on:

MessageBoxButton
MessageBoxImage
MessageBoxResult

which means the UI framework can evolve independently from the application workflows.

This may sound subtle, but it is one of the strongest decoupling decisions in the entire dialog architecture.


High-Level Feature API Through IDialogService

The feature-facing API remains intentionally high-level and workflow-oriented.

namespace Chrona.Services.App.Interfaces;

public interface IDialogService
{
    DialogResult ShowDialog(
        string message,
        string title,
        DialogButtons buttons = DialogButtons.Ok,
        DialogIcon icon = DialogIcon.Information);

    Task<DialogResult> ShowDialogAsync(
        string message,
        string title,
        DialogButtons buttons = DialogButtons.Ok,
        DialogIcon icon = DialogIcon.Information);

    bool Confirm(
        string message,
        string title,
        DialogIcon icon = DialogIcon.Question);

    string? ShowOpenFileDialog(
        string filter,
        string title);

    string? ShowSaveFileDialog(
        string filter,
        string defaultFileName);

    // Toast notifications
    void ShowToast(string message, DialogIcon icon);

    void ShowSuccess(
        string message,
        IReadOnlyList<ToastActionOption>? actions = null);
}

This is good service design because the call sites remain readable and intention-driven.

Feature code does not need to know:

  • whether the dialog is modal
  • whether a custom overlay is used
  • whether a native WPF window is used
  • whether notifications appear as toasts
  • whether dialogs are queued internally

The ViewModel simply expresses intent.

That keeps workflow logic clean.


IMessageDialogHost: The Critical Architectural Boundary

The actual decoupling point is IMessageDialogHost.

namespace Chrona.Services.App.Interfaces;

public interface IMessageDialogHost
{
    DialogResult ShowDialog(
        string message,
        string title,
        DialogButtons buttons,
        DialogIcon icon);
}

This interface is the true boundary between:

application interaction policy

and:

concrete UI rendering

Everything above this interface remains stable.

Everything below it becomes replaceable.

That separation gives the architecture significant flexibility.

The application can later replace:

  • native WPF windows
  • MahApps dialogs
  • Fluent overlays
  • custom modal systems
  • accessibility-enhanced dialogs
  • web-style popup layers

without rewriting feature logic.

That is exactly what good desktop architecture should enable.


DialogService as a Policy Layer

Chrona’s DialogService does not render dialogs directly.

Instead, it acts as an interaction-policy layer.

public class DialogService : IDialogService
{
    private readonly IMessageDialogHost _messageDialogHost;
    private readonly IToastService _toastService;

    public DialogService(
        IToastService toastService,
        IMessageDialogHost messageDialogHost)
    {
        _toastService = toastService;
        _messageDialogHost = messageDialogHost;
    }

    public DialogResult ShowDialog(
        string message,
        string title,
        DialogButtons buttons = DialogButtons.Ok,
        DialogIcon icon = DialogIcon.Information)
    {
        // Yes/No questions remain modal
        if (buttons == DialogButtons.YesNo ||
            buttons == DialogButtons.YesNoCancel)
        {
            return _messageDialogHost.ShowDialog(
                message,
                title,
                buttons,
                icon);
        }

        // Simple informational messages use toasts
        ShowToast(message, icon);

        return DialogResult.Ok;
    }
}

This is a very strong pattern.

The service owns interaction policy:

When should interaction be modal?
When should interaction be non-blocking?

But it does not own rendering.

Rendering belongs to the dialog host.

That separation keeps responsibilities extremely clean.


Modal vs Non-Modal UX Policy

Chrona distinguishes between two fundamentally different interaction categories.

Decision workflows remain modal:

Yes/No
Yes/No/Cancel

because the user must explicitly decide something before continuing.

Informational messages become non-blocking toast notifications:

Save successful
Export completed
Operation finished

This dramatically improves UX quality.

Users are interrupted only when interruption is actually necessary.

Many desktop applications overuse modal dialogs and unintentionally create interaction fatigue.

Chrona avoids that by centralizing interaction policy.


CustomDialogHost: UI Rendering Infrastructure

The actual UI rendering happens inside CustomDialogHost.

public class CustomDialogHost : IMessageDialogHost
{
    public DialogResult ShowDialog(
        string message,
        string title,
        DialogButtons buttons,
        DialogIcon icon)
    {
        if (Application.Current?.Dispatcher is not null &&
            !Application.Current.Dispatcher.CheckAccess())
        {
            return Application.Current.Dispatcher.Invoke(
                () => ShowDialogCore(
                    message,
                    title,
                    buttons,
                    icon));
        }

        return ShowDialogCore(
            message,
            title,
            buttons,
            icon);
    }

    private static DialogResult ShowDialogCore(
        string message,
        string title,
        DialogButtons buttons,
        DialogIcon icon)
    {
        var dialog =
            new CustomMessageDialogWindow(
                title,
                message,
                buttons,
                icon);

        if (Application.Current?.MainWindow is
            { IsLoaded: true } mainWindow &&
            mainWindow.IsVisible)
        {
            dialog.Owner = mainWindow;
        }

        dialog.ShowDialog();

        return dialog.DialogOutcome;
    }
}

Several strong implementation details are visible here.

The host handles UI-thread marshaling automatically.

The host assigns dialog ownership to the main window.

The host returns application-level DialogResult values rather than WPF-native types.

The host isolates all rendering-specific behavior from application workflows.

This is infrastructure code in the best sense: centralized, replaceable, and invisible to feature logic.


Building a Dialog That Feels Native to the App

Chrona’s custom dialog window is fully integrated into the application design system.

The dialog uses existing dynamic resources:

SurfaceBrush
DividerBrush
PrimaryBrush
PrimaryLightBrush

which makes the dialog visually consistent with the rest of the application.

The shell itself is highly customized:

<Window
    WindowStartupLocation="CenterOwner"
    ResizeMode="NoResize"
    WindowStyle="None"
    ShowInTaskbar="False"
    Background="Transparent"
    AllowsTransparency="True"
    SizeToContent="WidthAndHeight"
    MinWidth="460"
    MaxWidth="680">

This is important because native MessageBox windows often break visual consistency in modern WPF applications.

Custom dialogs allow:

  • branded UX
  • theme consistency
  • dark mode support
  • consistent typography
  • accessibility improvements
  • animation support
  • richer layouts

without changing feature logic.


Visual Structure and Theming

The dialog uses application-level brushes and effects:

<Border
    Background="{DynamicResource SurfaceBrush}"
    BorderBrush="{DynamicResource DividerBrush}"
    BorderThickness="1"
    CornerRadius="12">

along with a shadow effect:

<DropShadowEffect
    BlurRadius="24"
    ShadowDepth="4"
    Opacity="0.22"
    Color="Black" />

The header includes an icon badge and title region:

<Border
    Background="{DynamicResource BackgroundBrush}"
    BorderBrush="{DynamicResource DividerBrush}"
    BorderThickness="0,0,0,1">

while the footer uses application button styles:

<Button
    Style="{DynamicResource SecondaryButtonStyle}" />

<Button
    Style="{DynamicResource PrimaryButtonStyle}" />

This creates a dialog experience that feels like a natural extension of the application rather than an external operating-system popup.


Mapping Application Semantics to UX

The code-behind translates dialog semantics into actual visuals.

Icon configuration is centralized:

private void ConfigureIcon(DialogIcon icon)
{
    var (kind, badgeKey, iconKey) = icon switch
    {
        DialogIcon.Error =>
            (PackIconMaterialKind.AlertCircle,
             "ErrorBrush",
             "OnPrimaryTextBrush"),

        DialogIcon.Warning =>
            (PackIconMaterialKind.AlertOutline,
             "WarningBrush",
             "OnPrimaryTextBrush"),

        DialogIcon.Question =>
            (PackIconMaterialKind.HelpCircleOutline,
             "PrimaryLightBrush",
             "PrimaryBrush"),

        DialogIcon.Information =>
            (PackIconMaterialKind.InformationOutline,
             "PrimaryLightBrush",
             "PrimaryBrush"),

        _ =>
            (PackIconMaterialKind.InformationOutline,
             "PrimaryLightBrush",
             "PrimaryBrush")
    };

    IconGlyph.Kind = kind;

    IconBadge.Background =
        ResolveBrush(badgeKey, Brushes.LightGray);

    IconGlyph.Foreground =
        ResolveBrush(iconKey, Brushes.DodgerBlue);
}

The dialog window does not expose raw icon implementation details to the application layer. It simply maps semantic intent:

Error
Warning
Information
Question

into actual UI styling.

That keeps the application semantics stable while allowing the visual implementation to evolve independently.


Button Configuration and Localization

Button configuration follows the same pattern.

private void ConfigureButtons(DialogButtons buttons)
{
    SecondaryButton.Visibility = Visibility.Collapsed;
    TertiaryButton.Visibility = Visibility.Collapsed;

    switch (buttons)
    {
        case DialogButtons.Ok:
            PrimaryButton.Content = "OK";
            PrimaryButton.IsDefault = true;
            SecondaryButton.IsCancel = false;
            break;

        case DialogButtons.YesNo:
            PrimaryButton.Content = "Ja";
            SecondaryButton.Content = "Nein";
            SecondaryButton.Visibility = Visibility.Visible;
            PrimaryButton.IsDefault = true;
            SecondaryButton.IsCancel = true;
            break;
    }
}

This is important because localization and UX policy remain centralized.

If button wording changes later, the application does not need to update dozens of dialog call sites.


Dialog Outcome Mapping

The button click handlers map visual actions back into application semantics:

private void PrimaryButton_Click(
    object sender,
    RoutedEventArgs e)
{
    DialogOutcome =
        PrimaryButton.Content?.ToString() switch
        {
            "Ja" => AppDialogResult.Yes,
            _ => AppDialogResult.Ok
        };

    DialogResult = true;

    Close();
}

and:

private void SecondaryButton_Click(
    object sender,
    RoutedEventArgs e)
{
    DialogOutcome =
        SecondaryButton.Content?.ToString() switch
        {
            "Nein" => AppDialogResult.No,
            _ => AppDialogResult.Cancel
        };
}

Again, the application layer never sees WPF-native dialog primitives.

Everything flows through application-defined semantics.


One DI Registration Changes the Entire Application

The actual dialog-system swap happens in one place:

services.AddSingleton<IMessageDialogHost, CustomDialogHost>();

services.AddSingleton<IDialogService, DialogService>();

Previously, this could have pointed to a different implementation:

WpfMessageDialogHost

Now it points to:

CustomDialogHost

Every call site remains unchanged.

That is the real value of the abstraction.


Global Error Handling Also Benefits

Chrona even routes exception handling through the same host abstraction.

public class ExceptionHandler
{
    private readonly IMessageDialogHost _messageDialogHost;

    private readonly ILogger<ExceptionHandler>? _logger;

    public ExceptionHandler(
        IMessageDialogHost messageDialogHost,
        ILogger<ExceptionHandler>? logger = null)
    {
        _messageDialogHost = messageDialogHost;
        _logger = logger;
    }
}

The safe wrapper is particularly strong:

private DialogResult SafeShowDialog(
    string message,
    string title,
    DialogButtons buttons,
    DialogIcon icon)
{
    try
    {
        return _messageDialogHost.ShowDialog(
            message,
            title,
            buttons,
            icon);
    }
    catch
    {
        // Fallback if UI is unavailable
        return DialogResult.None;
    }
}

Even critical error dialogs remain inside the same architectural system.

That creates consistency across the entire application.


Why This Architecture Is Valuable in Real Projects

This dialog architecture provides several long-term advantages.

Design consistency improves because all dialogs follow the same visual system.

Replaceability improves because rendering can evolve independently from workflows.

Maintainability improves because ViewModels remain free of WPF-specific dialog details.

Scalability improves because richer dialog types can be added cleanly:

detail sections
danger-style confirmations
checkbox confirmations
stacktrace expansions
multi-action dialogs

Testability improves because both IDialogService and IMessageDialogHost can be mocked or faked during tests.

Most importantly, the application now owns its dialog semantics instead of delegating them to framework primitives.

That is a major architectural improvement.


Possible Future Enhancements

Chrona’s architecture already provides a strong foundation, but several natural extensions would fit cleanly into the current design.

A DialogSeverity abstraction could support specialized destructive-action styling.

Technical-detail sections could display stack traces or expandable error diagnostics.

Animations such as fade or scale transitions could improve perceived UX quality.

An async dialog-host API could support queued or non-modal workflows later.

Visual regression screenshots could ensure dialog consistency across releases.

The important point is that the architecture already supports these future evolutions cleanly because rendering and workflow semantics are separated properly.

Von admin