Dialogs In WPF (MVVM)

There are a lot of different articles on this topic and a lot of different solutions, simple ones based on static helper or service classes and more complex ones based on WPF dependency properties. All of them have their own pros and cons. I will not discuss about advantages and disadvantages of various solutions, but I wish to tell you about the solution I like more. Probably it has its own positive and negative points as well, I hope we will know about them during feedback. I will illustrate the solution in a step-by-step manner from the very basics of MVVM to complexity of dependency injection. In the narration I will not give any definitions, I hope you will find them in various sources if you enjoy the solution.
 
Part 1 MVVM

Step 1. To start our work let’s create a new WPF application project and save it with name WpfApplication1.

Step 2. We will follow MVVM (Model - View - ViewModel) pattern to develop our simple project. At this step let’s organize project structure and add new folders: Models, ViewModels and Views.

Step 3. Move MainWindow.xaml to Views folder and edit namespaces in xaml and code behind files taking new placement into consideration:

  • MainWindow.xaml: x:Class="WpfApplication1.Views.MainWindow";
  • MainWindow.xaml.cs: namespace WpfApplication1.Views.

Step 4. Set StartupUri property in App.xaml to "Views/MainWindow.xaml" and run application to check it still works.

Step 5. To implement commands logic we have to use a RelayCommand class, you can download it and save in project root folder. Include RelayCommand.cs in project and change namespace to WpfApplication1.

Step 6. We will execute RelayCommand with parameter further, for this purpose make some changes:

  • replace all instances of “Action execute” with “Action<object> execute”;
  • replace “this.execute();” with “this.execute(parameter);” in public void Execute(object parameter) method.

or find any other implementation for RelayCommand with parameters.

Step 7. At Steps 7-11 we will create a view model for MainWindow of our project. Add new public class in ViewModels folder and name it MainWindowViewModel.

Step 8. Add using of System.Windows.Input namespace to MainWindowViewModel.cs:

  1. using System.Windows.Input;

Step 9. Add private field openDialogCommand and corresponding public property:

  1. private ICommand openDialogCommand = null;  
  2. public ICommand OpenDialogCommand  
  3. {  
  4.     get { return this.openDialogCommand; }  
  5.     set { this.openDialogCommand = value; }  

Step 10. Add private void method OnOpenDialog:

  1. private void OnOpenDialog(object parameter)  
  2. {  
  3.   

Step 11. Add parameterless constructor for MainWindowViewModel and initialize private field openDialogComand through RelayCommand instance:

  1. public MainWindowViewModel()  
  2. {  
  3.     this.openDialogCommand = new RelayCommand(OnOpenDialog);  

Step 12. At steps 12-16 we will associate main window with its view model. Build project. Open App.xaml and add namespace for ViewModels folder:

  1. xmlns:viewModels="clr-namespace:WpfApplication1.ViewModels"   

Step 13. Add resource for MainWindowViewModel to Application.Resources section:

  1. <viewModels:MainWindowViewModel x:Key="MainWindowViewModel" />   

Step 14. Open MainWindow.xaml and declare DataContext property for Window node (along with Title, Height and Width properties):

  1. DataContext="{StaticResource ResourceKey=MainWindowViewModel}"   

Step 15. Add button to Grid node in markup of MainWindow.xaml and specify a Command property through binding to OpenDialogCommand property of data context:

  1. <Button Content="Button" ... Command="{Binding OpenDialogCommand}" />   

Step 16. Put a breakpoint at the open curly brace of OnOpenDialog method in MainWindowViewModel and run the project. Click button and notice the project’s flow stopped at the breakpoint.

PART 2. DIALOG STRUCTURE

Step 17. In this part we will create a base for a modal dialog window. Add a new folder Dialogs to project tree and two subfolders - DialogService and DialogYesNo.

Step 18. All our dialogs must return a result of choice made by user. Add a new item DialogResultEnum.cs to DialogService folder, replace class with enum and define structure with possible dialog result values, for example:

  1. public enum DialogResult  
  2. {  
  3.     Undefined,  
  4.     Yes,  
  5.     No  

Step 19. Add a new Window (WPF) to DialogService folder and name it DialogWindow.

Step 20. Add a property WindowStartupLocation to Window node and replace Grid section in DialogWindow markup with ContentPresenter node:

  1. <Window x:Class="WpfApplication1.Dialogs.DialogService.DialogWindow"  
  2.         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
  3.         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
  4.         Title="DialogWindow" Height="300" Width="300"  
  5.         WindowStartupLocation="CenterScreen">  
  6.      <ContentPresenter x:Name="ContentPresenter" Content="{Binding}"></ContentPresenter>  
  7. </Window>   

Step 21. Add a new public static class DialogService.cs to DialogService folder and define public static method OpenDialog which returns DialogResult:

  1. public static DialogResult OpenDialog()  
  2. {  
  3.     DialogWindow win = new DialogWindow();  
  4.     win.ShowDialog();  
  5.     return DialogResult.Undefined;  

Step 22. Get back to MainWindowViewModel, and modify OnOpenDialog method:

  1. private void OnOpenDialog(object parameter)  
  2. {  
  3.     Dialogs.DialogService.DialogResult result =  
  4.         Dialogs.DialogService.DialogService.OpenDialog();  

Step 23. Run project, put a breakpoint at closing curly brace in OnOpenDialog method and click button in main window. Dialog window should appear and result variable should have a DialogResult.Undefined value after closing the dialog.

PART 3. DIALOG VIEW

Step 24. In this part we will add a view for our modal dialog and define some logic. Add new public abstract class DialogViewModelBase to DialogService folder:

  1. public abstract class DialogViewModelBase  
  2. {  
  3.   

Step 25. Add new user control (WPF) to DialogYesNo folder and name it DialogYesNoView, place two buttons as a content of the control:

  1. <UserControl x:Class="WpfApplication1.Dialogs.DialogYesNo.DialogYesNoView"  
  2.              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
  3.              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
  4.              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"  
  5.              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"  
  6.              mc:Ignorable="d"  
  7.              d:DesignHeight="100" d:DesignWidth="300">  
  8.     <Grid Margin="4">  
  9.         <Button Content="Yes" HorizontalAlignment="Left" Margin="135,70,0,0" VerticalAlignment="Top" Width="75"/>  
  10.         <Button Content="No" HorizontalAlignment="Left" Margin="215,70,0,0" VerticalAlignment="Top" Width="75"/>  
  11.     </Grid>  
  12. </UserControl>   

Step 26. Add a new class to DialogYesNo folder and name it DialogYesNoViewModel, make DialogYesNoViewModel derived from DialogViewModelBase.

Step 27. Add two commands to process Yes and No buttons click (similarly for button at main window, see Steps 8-9):

  1. class DialogYesNoViewModel : DialogViewModelBase  
  2. {  
  3.     private ICommand yesCommand = null;  
  4.     public ICommand YesCommand  
  5.     {  
  6.         get { return yesCommand; }  
  7.         set { yesCommand = value; }  
  8.     }  
  9.   
  10.     private ICommand noCommand = null;  
  11.     public ICommand NoCommand  
  12.     {  
  13.         get { return noCommand; }  
  14.         set { noCommand = value; }  
  15.     }  
  16.   
  17.     public DialogYesNoViewModel()  
  18.     {  
  19.         this.yesCommand = new RelayCommand(OnYesClicked);  
  20.         this.noCommand = new RelayCommand(OnNoClicked);  
  21.     }  
  22.   
  23.     private void OnYesClicked(object parameter)  
  24.     {  
  25.   
  26.     }  
  27.   
  28.     private void OnNoClicked(object parameter)  
  29.     {  
  30.   
  31.     }  

Step 28. Modify DialogYesNoView and add Command properties to Yes and No buttons. Pay attention to CommandParameter property - we will pass a reference to the hosting window as a parameter to commands:

  1. <Button Content="Yes" HorizontalAlignment="Left" Margin="135,70,0,0" VerticalAlignment="Top" Width="75"   
  2.         Command="{Binding YesCommand}"   
  3.         CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=Window}}"/>  
  4. <Button Content="No" HorizontalAlignment="Left" Margin="215,70,0,0" VerticalAlignment="Top" Width="75"   
  5.         Command="{Binding NoCommand}"  
  6.         CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=Window}}"/>   

Step 29. Get back to App.xaml and add namespace for DialogYesNo folder:

  1. xmlns:dialogYesNo="clr-namespace:WpfApplication1.Dialogs.DialogYesNo"   

Step 30. Append an Application.Resources section with a resource for data template to associate DialogYesNoViewModel with DialogYesNoView:

  1. <DataTemplate DataType="{x:Type dialogYesNo:DialogYesNoViewModel}">  
  2.     <dialogYesNo:DialogYesNoView></dialogYesNo:DialogYesNoView>  
  3. </DataTemplate>   

Step 31. Get back to DialogWindow.xaml and add SizeToContent property to Window node along with WindowStartupLocation property:

  1. SizeToContent="WidthAndHeight"   

Step 32. Modify signature of OpenDialog static method in DialogService, let the method accept a parameter of type DialogViewModelBase and define win variable’s DataContext property:

  1. public static DialogResult OpenDialog(DialogViewModelBase vm)  
  2. {  
  3.     DialogWindow win = new DialogWindow();  
  4.     win.DataContext = vm;  
  5.     win.ShowDialog();  
  6.     return DialogResult.Undefined;  

Step 33. Get back to MainWindowViewModel to modify call of DialogService.OpenDialog(). Create instance of DialogViewModelBase instantiated with DialogYesNoViewModel and pass it as an argument to DialogService.OpenDialog():

  1. private void OnOpenDialog(object parameter)  
  2. {  
  3.     Dialogs.DialogService.DialogViewModelBase vm =  
  4.         new Dialogs.DialogYesNo.DialogYesNoViewModel();  
  5.     Dialogs.DialogService.DialogResult result =  
  6.         Dialogs.DialogService.DialogService.OpenDialog(vm);  

Now we have a modal YesNo dialog with empty commands to process buttons click, and the dialog still returns DialogResult.Undefined value after closing.

PART 4. DIALOG RESULT

Step 34. Get back to DialogViewModelBase, add public property UserDialogResult of type DialogResult:

  1. public DialogResult UserDialogResult  
  2. {  
  3.     get;  
  4.     private set;  

Step 35. Add void method CloseDialogWithResult to close dialog window:

  1. public void CloseDialogWithResult(Window dialog, DialogResult result)  
  2. {  
  3.     this.UserDialogResult = result;  
  4.     if (dialog != null)  
  5.         dialog.DialogResult = true;  

Step 36. Get back to DialogYesNoViewModel and define methods OnYesClicked and OnNoClicked:

  1. private void OnYesClicked(object parameter)  
  2. {  
  3.     this.CloseDialogWithResult(parameter as WindowDialogResult.Yes);  
  4. }  
  5.   
  6. private void OnNoClicked(object parameter)  
  7. {  
  8.     this.CloseDialogWithResult(parameter as WindowDialogResult.No);  

Step 37. Modify OpenDialog method in DialogService - add result variable and set its value to UserDialogResult property of hosting window data context:

  1. public static DialogResult OpenDialog(DialogViewModelBase vm)  
  2. {  
  3.     DialogWindow win = new DialogWindow();  
  4.     win.DataContext = vm;  
  5.     win.ShowDialog();  
  6.     DialogResult result =  
  7.         (win.DataContext as DialogViewModelBase).UserDialogResult;  
  8.     return result;  

At this moment our dialog returns a result of type DialogResult. Put a breakpoint to a closing curly brace in OnOpenDialog method in MainWindowViewModel, run project and click a button. Modal YesNo dialog will open. Click Yes or No button, dialog will be closed. Project flow will stop at break point and result variable will have a value, depend on which button was clicked.

PART 5. IMPROVEMENT

Step 38. Add Message property of type string and constructor with parameter to DialogViewModelBase:

  1. public string Message  
  2. {  
  3.     get;  
  4.     private set;  
  5. }  
  6.   
  7. public DialogViewModelBase(string message)  
  8. {  
  9.     this.Message = message;  

Step 39. Modify constructor in DialogYesNoViewModel:

  1. public DialogYesNoViewModel(string message)  
  2.     : base(message)  
  3. {  
  4.     this.yesCommand = new RelayCommand(OnYesClicked);  
  5.     this.noCommand = new RelayCommand(OnNoClicked);  

Step 40. Add string argument when instantiate variable of type DialogViewModelBase with DialogYesNoViewModel in OnOpenDialog method in MainWindowViewModel:

  1. private void OnOpenDialog(object parameter)  
  2. {  
  3.     Dialogs.DialogService.DialogViewModelBase vm =  
  4.         new Dialogs.DialogYesNo.DialogYesNoViewModel("Question");  
  5.     Dialogs.DialogService.DialogResult result =  
  6.         Dialogs.DialogService.DialogService.OpenDialog(vm);  

Step 41. Add a Label element to DialogYesNoView.xaml and set its Content property connected with Message property of data context:

  1. <Label Content="{Binding Message}" .../>   

Step 42. Modify signature of OpenDialog method in DialogService and add second parameter of type Window, set Owner property of DialogWindow instance to parameter value:

  1. public static DialogResult OpenDialog(DialogViewModelBase vm, Window owner)  
  2. {  
  3.     DialogWindow win = new DialogWindow();  
  4.     if(owner != null)  
  5.         win.Owner = owner;  
  6.     win.DataContext = vm;  
  7.     win.ShowDialog();  
  8.     DialogResult result =  
  9.         (win.DataContext as DialogViewModelBase).UserDialogResult;  
  10.     return result;  

Step 43. Add second argument to call OpenDialog method from OnOpenDialog method in MainWindowViewModel:

  1. private void OnOpenDialog(object parameter)  
  2. {  
  3.     Dialogs.DialogService.DialogViewModelBase vm =  
  4.         new Dialogs.DialogYesNo.DialogYesNoViewModel("Question");  
  5.     Dialogs.DialogService.DialogResult result =  
  6.         Dialogs.DialogService.DialogService.OpenDialog(vm, parameter as Window);  

Step 44. Add CommandParameter property to Button element in MainWindow.xaml:

  1. <Button Content="Button" ... Command="{Binding OpenDialogCommand}"  
  2.         CommandParameter="{Binding RelativeSource={RelativeSource AncestorType=Window}}"/>   

Step 45. Change WindowStartupLocation property to “CenterOwner” in DialogWindow.xaml.

PART 6. DEPENDENCY INJECTION

Step 46. Create subfolder DialogFacade in Dialogs folder.

Step 47. Add public interface IDialogFacade:

  1. public interface IDialogFacade  
  2. {  
  3.     DialogResult ShowDialogYesNo(string message, Window owner);  

Step 48. Add class DialogFacade, implement IDialogFacade and add private ShowDialog method by copying content of method from DialogService:

  1. public class DialogFacade : IDialogFacade  
  2. {  
  3.     public DialogFacade()  
  4.     {  
  5.   
  6.     }  
  7.   
  8.     public DialogResult ShowDialogYesNo(string message, Window owner)  
  9.     {  
  10.         DialogViewModelBase vm = new DialogYesNo.DialogYesNoViewModel(message);  
  11.         return this.ShowDialog(vm, owner);  
  12.     }  
  13.   
  14.     private DialogResult ShowDialog(DialogViewModelBase vm, Window owner)  
  15.     {  
  16.         DialogWindow win = new DialogWindow();  
  17.         if (owner != null)  
  18.             win.Owner = owner;  
  19.         win.DataContext = vm;  
  20.         win.ShowDialog();  
  21.         DialogResult result =  
  22.             (win.DataContext as DialogViewModelBase).UserDialogResult;  
  23.         return result;  
  24.     }  

Step 49. Install Ninject Nuget package.

Step 50. Add IoC subfolder to project root folder.

Step 51. Add new class and name it Module, add two usings to work with Ninject:

  1. using Ninject;  
  2. using Ninject.Modules; 

Step 52. Derive Module class from NinjectModule abstract class and implement it:

  1. class Module : NinjectModule  
  2. {  
  3.     public override void Load()  
  4.     {  
  5.         Bind<IDialogFacade>().To<DialogFacade>().InSingletonScope();  
  6.         Bind<MainWindowViewModel>().ToSelf();  
  7.     }  

Step 53. Add new class Container and implement access to Ninject kernel using singleton pattern:

  1. public class Container  
  2. {  
  3.     public IKernel Kernel  
  4.     {  
  5.         get;  
  6.         private set;  
  7.     }  
  8.   
  9.     private static volatile Container instance = null;  
  10.     private static object syncRoot = new Object();  
  11.   
  12.     private Container()  
  13.     {  
  14.         this.Kernel = new Ninject.StandardKernel();  
  15.         this.Kernel.Load(new Module());  
  16.     }  
  17.   
  18.     public static Container Instance  
  19.     {  
  20.         get  
  21.         {  
  22.             if (instance == null)  
  23.             {  
  24.                 lock (syncRoot)  
  25.                 {  
  26.                     if (instance == null)  
  27.                         instance = new Container();  
  28.                 }  
  29.             }  
  30.             return instance;  
  31.         }  
  32.     }  

Step 54. Add private field named dialogFacade to MainWindowViewModel and edit constructor for accepting IDialogFacade parameter:

  1. public class MainWindowViewModel  
  2. {  
  3.     private IDialogFacade dialogFacade = null;  
  4.     ...  
  5.     public MainWindowViewModel(IDialogFacade dialogFacade)  
  6.     {  
  7.         this.dialogFacade = dialogFacade;  
  8.         this.openDialogCommand = new RelayCommand(OnOpenDialog);  
  9.     }  
  10.     ...  

Step 55. Add a new class ViewModelLocator to ViewModels folder:

  1. using Ninject;  
  2.   
  3. public class ViewModelLocator  
  4. {  
  5.     private MainWindowViewModel mainWindowViewModel = null;  
  6.     public MainWindowViewModel MainWindowViewModel  
  7.     {  
  8.         get { return mainWindowViewModel; }  
  9.         set { mainWindowViewModel = value; }  
  10.     }  
  11.   
  12.     public ViewModelLocator()  
  13.     {  
  14.         this.mainWindowViewModel =  
  15.             IoC.Container.Instance.Kernel.Get<MainWindowViewModel>();  
  16.     }  

Step 56. Remove MainVindowViewModel resource and add ViewModelLocator resource to App.xaml:

  1. <Application x:Class="WpfApplication2.App"  
  2.              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
  3.              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
  4.              xmlns:viewModels="clr-namespace:WpfApplication2.ViewModels"  
  5.              xmlns:dialogYesNo="clr-namespace:WpfApplication2.Dialogs.DialogYesNo"  
  6.              StartupUri="Views/MainWindow.xaml">  
  7.     <Application.Resources>  
  8.         <viewModels:ViewModelLocator x:Key="ViewModelLocator" />  
  9.         <DataTemplate DataType="{x:Type dialogYesNo:DialogYesNoViewModel}">  
  10.             <dialogYesNo:DialogYesNoView></dialogYesNo:DialogYesNoView>  
  11.         </DataTemplate>  
  12.     </Application.Resources>  
  13. </Application>   

Step 57. Change DataContext property in MainWindow.xaml:

  1. DataContext="{Binding MainWindowViewModel, Source={StaticResource ResourceKey=ViewModelLocator}}"   

Step 58. Call ShowDialogYesNo method of dialogFacade object from OnOpenDialog method in MainWindowViewModel:

  1. private void OnOpenDialog(object parameter)  
  2. {  
  3.     Dialogs.DialogService.DialogResult result =  
  4.         this.dialogFacade.ShowDialogYesNo("Question", parameter as Window);  

Read more articles on WPF: