There are two good ways to do this, 1) a dialog service (easy, clean), and 2) view assisted. View assisted provides some neat features, but is usually not worth it.
DIALOG SERVICE
a) a dialog service interface like via constructor or some dependency container:
interface IDialogService
{
Task ShowDialogAsync(DialogViewModel dlgVm);
}
b) Your implementation of IDialogService should open a window (or inject some control into the active window), create a view corresponding to the name of the given dlgVm type (use container registration or convention or a ContentPresenter with type associated DataTemplates). ShowDialogAsync should create a TaskCompletionSource and return its .Task proptery. The DialogViewModel class itself needs an event you can invoke in the derived class when you want to close, and watch in the dialog view to actually close/hide the dialog and complete the TaskCompletionSource.
b) To use, simply call await this.DialogService.ShowDialog(myDlgVm) on your instance of some DialogViewModel-derived class. After await returns, look at properties you've added on your dialog VM to determine what happened; you don't even need a callback.
VIEW ASSISTED
This has your view listening to an event on the viewmodel. This could all be wrapped up into a Blend Behavior to avoid code behind and resource usage if you're so inclined (FMI, subclass the "Behavior" class to see a sort of Blendable attached property on steroids). For now, we'll do this manually on each view:
a) Create an OpenXXXXXDialogEvent with a custom payload (a DialogViewModel derived class).
b) Have the view subscribe to the event in its OnDataContextChanged event. Be sure to hide and unsubscribe if the old value != null and in the Window's Unloaded event.
c) When the event fires, have the view open your view, which might be in a resource on your page, or you could locate it by convention elsewhere (like in the the dialog service approach).
This approach is more flexible, but requires more work to use. I don't use it much. The one nice advantage are the ability to place the view physically inside a tab, for example. I have used an algorithm to place it in the current user control's bounds, or if not big enough, traverse up the visual tree until a big enough container is found.
This allows dialogs to be close to the place they're actually used, only dim the part of the app related to the current activity, and let the user move around within the app without having to manually push dialogs away, even have multiple quasi-modal dialogs open on different tabs or sub-views.