The dialogs are shown by a MetroWindow, but a view model has no business knowing about windows. IDialogCoordinator bridges that: the view model asks the coordinator for a dialog and passes itself along as the context, and the coordinator works out which window that context belongs to.

Two pieces are involved:

  • IDialogCoordinator — the service your view model calls. Inject it, so the view model stays testable.
  • DialogParticipation — an attached property that tells the coordinator which window a given context belongs to. Without it the coordinator cannot resolve a window and throws.

1. Register the view model

Set DialogParticipation.Register on the window and bind it to the view model. {Binding} binds the DataContext itself, which is usually what you want:

<mah:MetroWindow x:Class="SimpleApp.MainWindow"
                 xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
                 mah:DialogParticipation.Register="{Binding}">

</mah:MetroWindow>

You do not have to register on the window itself. The coordinator resolves the window with Window.GetWindow, so registering on a UserControl inside a MetroWindow works too — useful when each view brings its own view model.

2. Give the view model a coordinator

DialogCoordinator.Instance is the ready-made implementation. Pass it in rather than reaching for it inside the view model, and tests can substitute their own:

using MahApps.Metro.Controls;
using MahApps.Metro.Controls.Dialogs;

namespace SimpleApp
{
    public partial class MainWindow : MetroWindow
    {
        public MainWindow()
        {
            this.InitializeComponent();

            // Set the DataContext for your View
            this.DataContext = new MainWindowViewModel(DialogCoordinator.Instance);
        }
    }
}

With a DI container you would register IDialogCoordinator against DialogCoordinator.Instance once and let the container do the rest.

3. Show dialogs

Every method takes the context as its first parameter — that is the object you registered in step 1, normally the view model itself. With several windows open this is what puts the dialog on the right one.

The methods below return Task so callers can await them, which suits the async command types most MVVM frameworks provide. Wiring one straight to a plain ICommand, which cannot await, would push you towards async void and its swallowed exceptions.

using MahApps.Metro.Controls.Dialogs;

namespace SimpleApp
{
    public class MainWindowViewModel
    {
        private readonly IDialogCoordinator dialogCoordinator;

        public MainWindowViewModel(IDialogCoordinator dialogCoordinator)
        {
            this.dialogCoordinator = dialogCoordinator;
        }

        public async Task ShowMessage()
        {
            await this.dialogCoordinator.ShowMessageAsync(this, "Message Title", "Bar");
        }

        public async Task ShowProgress()
        {
            var controller = await this.dialogCoordinator.ShowProgressAsync(
                this,
                "Wait",
                "Waiting for the Answer to the Ultimate Question of Life, The Universe, and Everything...");

            controller.SetIndeterminate();

            // Do your work...
            var result = await Task.Run(...);

            await controller.CloseAsync();
        }
    }
}

Unregister views that come and go

In 2.4.11 a registration that is never removed keeps the view model and the window alive for the lifetime of the process.

DialogParticipation keeps its registrations in a static dictionary that maps the context to the element it was registered on. Both are held by a strong reference, and an entry is only removed when the attached property changes. Closing a window does not change it.

For a main window that lives as long as the application this makes no difference. For a dialog window or a view that is opened repeatedly it does: every instance leaves its view model behind, and memory grows with each round.

Clear the registration when the view goes away:

public partial class MyDialogWindow : MetroWindow
{
    public MyDialogWindow()
    {
        this.InitializeComponent();
        this.Closed += (sender, e) => DialogParticipation.SetRegister(this, null);
    }
}

Setting the property to null removes the old value from the dictionary and puts nothing back, which is exactly what is wanted. For a UserControl that is swapped in and out, do the same from Unloaded.

On develop this is nothing you have to remember any more, which is #4573: the registrations live in a ConditionalWeakTable, so one lasts exactly as long as the context it was made for and a window that is closed takes its entry with it. Clearing the property by hand still works, and it is still the way to take a registration back while the view and the view model are both still around.

Views that move into a window of their own

A view can end up in a window that did not exist when it was registered. A tab torn out of a Dragablz TabablzControl is the usual way this happens. The registration itself carries over, because it points at the element rather than at the window, and the coordinator works out the window each time it is asked. What matters is the kind of window the view lands in: Dragablz opens a plain Window for a torn off tab unless it is told otherwise, and asking for a dialog from there gives you Context is not inside a MetroWindow.

That window is yours to make, through the IInterTabClient on the InterTabController:

public class MetroInterTabClient : IInterTabClient
{
    public INewTabHost<Window> GetNewHost(IInterTabClient interTabClient, object partition, TabablzControl source)
    {
        var tabs = new TabablzControl
                   {
                       InterTabController = new InterTabController { InterTabClient = interTabClient }
                   };

        var window = new MetroWindow { Width = 800, Height = 600, Content = tabs };

        return new NewTabHost<Window>(window, tabs);
    }

    public TabEmptiedResponse TabEmptiedHandler(TabablzControl tabControl, Window window)
    {
        return TabEmptiedResponse.CloseWindowOrLayoutBranch;
    }
}

With that in place a dialog opens in whichever window the tab is sitting in at the time, the one it started in as well as the one it was pulled into. The same goes for any other window a view is moved into, a docking layout among them: make it a MetroWindow and the coordinator finds it. This is #4424.

When it does not work

The coordinator throws instead of failing quietly, and the two messages are worth recognising:

Message Cause
Context is not registered. Nothing called DialogParticipation.Register for this context — the attached property is missing, or the object passed as context is not the one that was registered.
Context is not inside a MetroWindow. The element you registered sits in a plain Window. The dialogs are drawn by MetroWindow, so the containing window has to be one. That includes a window the view was moved into after it was registered, see above.

The first one also appears when the DataContext is replaced after the binding was evaluated: the registration then still points at the previous object, while the view model calling the coordinator is the new one.