I would like to build a generic/re-usable modal dialog that I can use in our WPF (MVVM) - WCF LOB application.
I have a Views and associated ViewModels that I would like to display using dialogs. Bindings between Views and ViewModels are done using Type-targeted DataTemplates.
Here are some requirements that I have been able to draft:
- I prefer this to be based on a Window instead of using Adorners and controls that act like a modal dialog.
- It should get its minimum size from the content.
- It should center on the owner window.
- The window must not show the Minimize and Maximize buttons.
- It should get its title from the content.
What is the best way to do this?
I usually deal with this by injecting this interface into the appropriate ViewModels:
public interface IWindow
{
void Close();
IWindow CreateChild(object viewModel);
void Show();
bool? ShowDialog();
}
This allows the ViewModels to spaw child windows and show them modally on modeless.
A reusable implementation of IWindow is this:
public class WindowAdapter : IWindow
{
private readonly Window wpfWindow;
public WindowAdapter(Window wpfWindow)
{
if (wpfWindow == null)
{
throw new ArgumentNullException("window");
}
this.wpfWindow = wpfWindow;
}
#region IWindow Members
public virtual void Close()
{
this.wpfWindow.Close();
}
public virtual IWindow CreateChild(object viewModel)
{
var cw = new ContentWindow();
cw.Owner = this.wpfWindow;
cw.DataContext = viewModel;
WindowAdapter.ConfigureBehavior(cw);
return new WindowAdapter(cw);
}
public virtual void Show()
{
this.wpfWindow.Show();
}
public virtual bool? ShowDialog()
{
return this.wpfWindow.ShowDialog();
}
#endregion
protected Window WpfWindow
{
get { return this.wpfWindow; }
}
private static void ConfigureBehavior(ContentWindow cw)
{
cw.WindowStartupLocation = WindowStartupLocation.CenterOwner;
cw.CommandBindings.Add(new CommandBinding(PresentationCommands.Accept, (sender, e) => cw.DialogResult = true));
}
}
You can use this Window as a reusable host window. There's no code-behind:
<Window x:Class="Ploeh.Samples.ProductManagement.WpfClient.ContentWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:self="clr-namespace:Ploeh.Samples.ProductManagement.WpfClient"
xmlns:pm="clr-namespace:Ploeh.Samples.ProductManagement.PresentationLogic.Wpf;assembly=Ploeh.Samples.ProductManagement.PresentationLogic.Wpf"
Title="{Binding Path=Title}"
Height="300"
Width="300"
MinHeight="300"
MinWidth="300" >
<Window.Resources>
<DataTemplate DataType="{x:Type pm:ProductEditorViewModel}">
<self:ProductEditorControl />
</DataTemplate>
</Window.Resources>
<ContentControl Content="{Binding}" />
</Window>
You can read more about this (as well as download the full code sample) in my book.
I'm answering my own question to help others find all answers I struggled to find in one place. What above seems like a straight forward problem, actually presents multiple problems that I hope to answer sufficiently below.
Here goes.
Your WPF window that will serve as the generic dialog can look something like this:
<Window x:Class="Example.ModalDialogView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ex="clr-namespace:Example"
Title="{Binding Path=mDialogWindowTitle}"
ShowInTaskbar="False"
WindowStartupLocation="CenterOwner"
WindowStyle="SingleBorderWindow"
SizeToContent="WidthAndHeight"
ex:WindowCustomizer.CanMaximize="False"
ex:WindowCustomizer.CanMinimize="False"
>
<DockPanel Margin="3">
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" FlowDirection="RightToLeft">
<Button Content="Cancel" IsCancel="True" Margin="3"/>
<Button Content="OK" IsDefault="True" Margin="3" Click="Button_Click" />
</StackPanel>
<ContentPresenter Name="WindowContent" Content="{Binding}"/>
</DockPanel>
</Window>
Following MVVM, the right way to show a dialog is through a mediator. To use a mediator, you typically require some service locator as well. For mediator specific details, look here.
The solution I settled on involved implementing an IDialogService interface that is resolved through a simple static ServiceLocator. This excellent codeproject article has the details on that. Take note of this message in the article forum. This solution also solves the problem of discovering the owner window via the ViewModel instance.
Using this interface, you can call IDialogService.ShowDialog(ownerViewModel, dialogViewModel). For now, I'm calling this from the owner ViewModel, meaning I have hard references between my ViewModels. If you use aggregated events, you will probably call this from a conductor.
Setting the minimum size on the View that will eventually be displayed in the dialog doesn't automatically set the minimum size of the dialog. Also, since the logical tree in the dialog contains the ViewModel, you can't just bind to the WindowContent element's properties. This question has an answer with my solution.
The answer I mention above also includes code that centers the window on the owner.
Finally, disabling the minimize and maximize buttons is something WPF can't natively do. The most elegant solution IMHO is using this.