Creating a flexible custom WPF dialog

Standard
Share

In my current project, we had a need to create a custom WPF dialog box. Writing our own provides us the ability to easily skin the window with our application’s theme as well as gives us the ability to generate any buttons we wanted.

We wanted our custom dialog box to be able to:

  1. Display a message to the user
  2. Provide various button options (OK, OK/Cancel, Yes/No, Yes/No/Cancel, etc)
  3. Return the button clicked by the user
  4. Display an optional CheckBox
    • Display text next to the optional CheckBox
    • Accept a boolean property for the default IsChecked state of the optional CheckBox
    • Provide the IsChecked value of the optional CheckBox as an out parameter

PREVIEW
In this article, I’ll assume you’re familiar with the concepts of simple WPF binding, Value Converters, and primitive WPF window design (no fancy animations here!). I’ll walk you through the creation of a custom confirmation dialog box, from design of the box itself to a complete ViewModel to house the various data elements of the dialog. Finally, I’ll show you how to call this newly created dialog and how to obtain the dialog results – both the button pressed (as a return value) and the optional CheckBox value (as an out parameter). If you’re not familiar with Value Converters (classes that implement the IValueConverter interface) or RelayCommands (which implement the ICommand interface) – don’t worry. Just follow along, and I’ll explain as we go.

In this project, we’ll make use of the MVVM design pattern. I use a modified version of Josh Smith’s MVVM library. You can find my modified library here. This library provides both the INotifyPropertyChanged implementation we’ll need to get UI updates as well as the RelayCommands we need to link button clicks to ViewModel methods.

In the project we need to define:

  • An enum to contain all the necessary button combinations we want displayed
  • An enum to contain the single result value returned from the View

In the ViewModel:

  • A button options enum property to house the requested buttons to be displayed to the user
  • A result enum property to house the selected button chosen by the user
  • A string property to house the message displayed in the dialog
  • A string property to house the optional CheckBox caption
  • A boolean property to house the optional CheckBox checked status
  • An Action property to house our view’s Close action
  • Methods to handle the various button clicks
  • RelayCommand/ICommand pairs to handle the binding of View button clicks to ViewModel methods

And in the View (code-behind):

  • Define a close action in the view’s constructor
  • Create alternate view constructor(s) to handle various overloads
  • Create methods that display the dialog with a return value and an optional out value

In a nutshell, whenever we need to display this custom window, we’ll create an instance of the View (with optional parameters), which will in turn create an instance of the ViewModel. We’ll then display the View (with the default ShowDialog() method or a custom ShowCustomDialog() method) and obtain the results.

Let’s start by showing the code for the ViewModel (and enums) and I’ll explain in detail each line of code.

Complete ViewModel code:

using System;
using System.Windows.Controls;
using ProjectNamespace.MVVM;

public enum ConfirmationDialogResult
{
    OK,
    Cancel,
    Yes,
    No
}

public enum CustomConfirmationDialogButtonOptions
{
    OK,
    OKCancel,
    YesNo,
    YesNoCancel
}

namespace ProjectNamespace.ViewModels
{
    internal class ConfirmationViewModel : ViewModelBase
    {

        #region Properties

        private string message;
        public string Message
        {
            get { return message; }
            set { message = value; NotifyPropertyChanged(); }
        }
        
        /// <summary>
        /// Boolean result of optional checkbox
        /// </summary>
        private bool checkBoxValue;
        public bool CheckBoxValue
        {
            get { return checkBoxValue; }
            set { checkBoxValue = value; }
        }

        /// <summary>
        /// Checkbox caption
        /// </summary>
        private string checkBoxString = null;
        public string CheckBoxString
        {
            get { return checkBoxString; }
            set { checkBoxString = value; NotifyPropertyChanged(); }
        }

        /// <summary>
        /// Font Size
        /// </summary>
        private int fontSize;
        public int FontSize
        {
            get { return fontSize; }
            set { fontSize = value; NotifyPropertyChanged(); }
        }

        /// <summary>
        /// Button stack panel
        /// </summary>
        public StackPanel ButtonStackPanel;

        /// <summary>
        /// Results from dialog
        /// </summary>
        public ConfirmationDialogResult Result { get; set; }

        /// <summary>
        /// Close Action
        /// </summary>
        public Action CloseAction { get; set; }

        #endregion

        #region Commands

        private RelayCommand okCommand;
        public RelayCommand OKCommand
        {
            get
            {
                if (okCommand == null)
                {
                    okCommand = new RelayCommand(param => OKExecute());
                }
                return okCommand;
            }
        }

        private RelayCommand yesCommand;
        public RelayCommand YesCommand
        {
            get
            {
                if (yesCommand == null)
                {
                    yesCommand = new RelayCommand(param => YesExecute());
                }
                return yesCommand;
            }
        }

        private RelayCommand noCommand;
        public RelayCommand NoCommand
        {
            get
            {
                if (noCommand == null)
                {
                    noCommand = new RelayCommand(param => NoExecute());
                }
                return noCommand;
            }
        }

        private RelayCommand cancelCommand;
        public RelayCommand CancelCommand
        {
            get
            {
                if (cancelCommand == null)
                {
                    cancelCommand = new RelayCommand(param => CancelExecute());
                }
                return cancelCommand;
            }
        }

        #endregion

        #region Constructors

        public ConfirmationViewModel(StackPanel buttonPanel)
        {
            Message = "Are you sure?";
            FontSize = 32;
            BuildButtonList(buttonPanel, CustomConfirmationDialogButtonOptions.OK);
        }

        /// <summary>
        /// Message
        /// </summary>
        /// <param Name="message"></param>
        public ConfirmationViewModel(StackPanel buttonPanel, string message) : this(buttonPanel)
        {
            Message = message;
            BuildButtonList(buttonPanel, CustomConfirmationDialogButtonOptions.OK);
        }

        /// <summary>
        /// Message, button options
        /// </summary>
        /// <param Name="message"></param>
        /// <param Name="options"></param>
        public ConfirmationViewModel(StackPanel buttonPanel, string message, CustomConfirmationDialogButtonOptions options) : this(buttonPanel, message)
        {
            BuildButtonList(buttonPanel, options);
        }

        /// <summary>
        /// Message, button options, optional checkbox content string, default checkbox boolean
        /// </summary>
        /// <param name="message"></param>
        /// <param name="options"></param>
        /// <param name="optionalCheckBoxString"></param>
        /// <param name="defaultCheckBoxValue"></param>
        public ConfirmationViewModel(StackPanel buttonPanel, string message, CustomConfirmationDialogButtonOptions options, string optionalCheckBoxString, bool defaultCheckBoxValue) : this(buttonPanel, message, options)
        {
            BuildButtonList(buttonPanel, options);
            CheckBoxString = optionalCheckBoxString;
            CheckBoxValue = defaultCheckBoxValue;
        }

        /// <summary>
        /// Message, button options, font size
        /// </summary>
        /// <param Name="message"></param>
        /// <param Name="options"></param>
        /// <param Name="fontSize"></param>
        public ConfirmationViewModel(StackPanel buttonPanel, string message, CustomConfirmationDialogButtonOptions options, int fontSize) : this(buttonPanel, message, options)
        {
            FontSize = fontSize;
        }

        #endregion

        #region Methods

        /// <summary>
        /// OK - Execute
        /// Executed once employee confirms OK
        /// </summary>
        internal void OKExecute()
        {
            // Dialog result
            Result = ConfirmationDialogResult.OK;
            CloseAction();
        }

        /// <summary>
        /// Yes - Execute
        /// </summary>
        internal void YesExecute()
        { 
            // Dialog result
            Result = ConfirmationDialogResult.Yes;
            CloseAction();
        }

        /// <summary>
        /// No - execute
        /// </summary>
        internal void NoExecute()
        { 
            // Dialog result
            Result = ConfirmationDialogResult.No;
            CloseAction();
        }

        /// <summary>
        /// Cancel - Execute
        /// Executed once employee confirms Cancel
        /// </summary>
        internal void CancelExecute()
        {
            // Dialog result
            Result = ConfirmationDialogResult.Cancel;
            CloseAction();
        }

        public void BuildButtonList(StackPanel buttonPanel, CustomConfirmationDialogButtonOptions options)
        {
            buttonPanel.Children.Clear();

            Button button;
            int btnMinWidth = 75;
            switch (options)
            { 
                case CustomConfirmationDialogButtonOptions.OK:
                    button = new Button();
                    button.Content = "OK";
                    button.Command = OKCommand;
                    button.MinWidth = btnMinWidth;
                    button.Width = Double.NaN;
                    buttonPanel.Children.Add(button);
                    break;

                case CustomConfirmationDialogButtonOptions.OKCancel:
                    button = new Button();
                    button.Content = "OK";
                    button.Command = OKCommand;
                    button.MinWidth = btnMinWidth;
                    button.Width = Double.NaN;
                    buttonPanel.Children.Add(button);

                    button = new Button();
                    button.Content = "Cancel";
                    button.Command = CancelCommand;
                    button.MinWidth = btnMinWidth;
                    button.Width = Double.NaN;
                    buttonPanel.Children.Add(button);
                    break;

                case CustomConfirmationDialogButtonOptions.YesNo:
                    button = new Button();
                    button.Content = "Yes";
                    button.Command = YesCommand;
                    button.MinWidth = btnMinWidth;
                    button.Width = Double.NaN;
                    buttonPanel.Children.Add(button);

                    button = new Button();
                    button.Content = "No";
                    button.Command = NoCommand;
                    button.MinWidth = btnMinWidth;
                    button.Width = Double.NaN;
                    buttonPanel.Children.Add(button);
                    break;

                case CustomConfirmationDialogButtonOptions.YesNoCancel:
                    button = new Button();
                    button.Content = "Yes";
                    button.Command = YesCommand;
                    button.MinWidth = btnMinWidth;
                    button.Width = Double.NaN;
                    buttonPanel.Children.Add(button);

                    button = new Button();
                    button.Content = "No";
                    button.Command = NoCommand;
                    button.MinWidth = btnMinWidth;
                    button.Width = Double.NaN;
                    buttonPanel.Children.Add(button);

                    button = new Button();
                    button.Content = "Cancel";
                    button.Command = CancelCommand;
                    button.MinWidth = btnMinWidth;
                    button.Width = Double.NaN;
                    buttonPanel.Children.Add(button);

                    break;

                default:
                    break;
            }
        }

        #endregion

    }
}

ViewModel Breakdown
In lines #1-3, we’re simply declaring our usings statements.

using System;
using System.Windows.Controls;
using ProjectNamespace.MVVM;

Lines #5-19 define our two enums, ConfirmationDialogResult and CustomConfirmationDialogButtonOptions. These enums define a type and valid values for each type. For example, the valid values for ConfirmationDialogResult are OK, Cancel, Yes, and No.

    public enum ConfirmationDialogResult
    {
        OK,
        Cancel,
        Yes,
        No
    }
 
    public enum CustomConfirmationDialogButtonOptions
    {
        OK,
        OKCancel,
        YesNo,
        YesNoCancel
    }

Line #23 starts the class for our ViewModel. Note that the class inherits from ViewModelBase, which will provide us the RelayCommands necessary to bind our buttons to methods on the ViewModel.

    internal class ConfirmationViewModel : ViewModelBase

Lines #28-33 provide the private and public field/property pair that will house the string to be displayed in the dialog box.

        private string message;
        public string Message
        {
            get { return message; }
            set { message = value; }
        }

Lines #38-43 provide the private and public field/property pair that will house the boolean value to which the IsChecked property of the optional CheckBox will be bound.

        private bool checkBoxValue;
        public bool CheckBoxValue
        {
            get { return checkBoxValue; }
            set { checkBoxValue = value; }
        }

Lines #48-53 provide the private and public field/property pair that will house the string to which the Content property of the optional CheckBox content will be bound.

        private string checkBoxString = null;
        public string CheckBoxString
        {
            get { return checkBoxString; }
            set { checkBoxString = value; }
        }

Lines #58-63 provide the private and public field/property pair that will house the font size to which the TextElement.FontSize property of the window will be bound.

        private int fontSize;
        public int FontSize
        {
            get { return fontSize; }
            set { fontSize = value; }
        }

Line #68 provides a StackPanel member on the ViewModel. You’ll see later on the description of the code for the View how the View’s reference to the StackPanel gets passed in to the ViewModel as a parameter. This StackPanel will house the various buttons that will be displayed on the View. Because the buttons are created dynamically by the ViewModel, we’re passing the StackPanel into the ViewModel.

        public StackPanel ButtonStackPanel;

On a side note, there are various ways to handle the above situation – we might have alternatively added a List<Button> property to the ViewModel, then bound the StackPanel’s children to the contents of the List<Button> on the ViewModel. Back to the ViewModel explanation:

Line #73 provides the property that will house the result of the dialog. That is to say, we’ll use this property after the dialog has been closed by the user to determine which displayed button the user clicked.

        public ConfirmationDialogResult Result { get; set; }

Line #78 provides the property that will house the Close action of the dialog. One of the issues many people run into when writing MVVM code is determining how to close a View from a method in the ViewModel. I prefer to use an Action property like the one shown here. You’ll see later in the description of the View’s constructor how we define this Action property.

        public Action CloseAction { get; set; }

Lines #84-134 define the RelayCommand/ICommand field/property pairs that will relay our buttons’ Click events to methods on our ViewModel. I’ll explain the first in detail, but note that a RelayCommand/ICommand pair exists for each method that should be bound to a button.

        private RelayCommand okCommand;
        public RelayCommand OKCommand
        {
            get
            {
                if (okCommand == null)
                {
                    okCommand = new RelayCommand(param => OKExecute());
                }
                return okCommand;
            }
        }

Line #84 defines the private RelayCommand okCommand. Note that this is never defined until it is referenced in the get accessor of the public ICommand property OKCommand. FYI, RelayCommands are ICommands. That is to say, RelayCommands are objects that implement the ICommand interface, and can therefore be referred to as ICommand objects.

Line #85 defines the public ICommand OKCommand. You’ll see in lines #87-94 that a reference to OKCommand will do the following:

  1. Check to see if okCommand is null. okCommand will initiate as a null value and remain null until it is accessed via the OKCommand’s get accessor.
  2. If okCommand is null, okCommand will be defined as a new RelayCommand, which in this case will redirect to the OKExecute() method on our ViewModel.
  3. Return the value stored in okCommand.

Again, lines #97-134 simply define RelayCommands for the other three possible button click handlers we need (Cancel, Yes, No).

Lines #140-190 define five overloads of the ViewModel constructor. This gives us the flexibility to define the necessary values at the time of ViewModel construction. You’ll later see how these match up to different calls to the ViewModel from the View.

Lines #140-145 define the primary constructor. Note I avoided the use of the term default constructor, as the primary constructor accepts a parameter (of type StackPanel).

        public ConfirmationViewModel(StackPanel buttonPanel)
        {
            Message = "Are you sure?";
            FontSize = 32;
            BuildButtonList(buttonPanel, CustomConfirmationDialogButtonOptions.OK);
        }
  • Line #142 sets a value of “Are you sure?” to the Message property on the ViewModel.
  • Line #143 sets a value of 32 to the FontSize property on the ViewModel.
  • Line #144 calls a a BuildButtonList method and passes to it the StackPanel buttonPanel it received as a parameter, along with a CustomConfirmationDialogButtonOption value of CustomConfirmationDialogButtonOption.OK.

Lines #151-155 contain the first overload of the ViewModel constructor.

        public ConfirmationViewModel(StackPanel buttonPanel, string message) : this(buttonPanel)
        {
            Message = message;
        }

This constructor makes use of constructor chaining through the appending of ” : this(parameterName)” to the end of the constructor signature.

In line 151, you can see this constructor accepts two parameters: a StackPanel (just as the primary constructor did), and a string. This constructor will first pass the buttonPanel to the primary constructor and execute that constructor. Then, it will continue with the code inside the block of this constructor. Therefore, the code flow executed when this overload of the ViewModel constructor is called goes something like this:

  1. ConfirmationViewModel(StackPanel buttonPanel, string message)
  2. ConfirmationViewModel(StackPanel buttonPanel) is executed, including all code contained within
    1. Message property set to “Are you sure?”
    2. FontSize property set to 32
    3. BuildButtonList(buttonPanel, CustomConfirmationDialogButtonOptions.OK) method is called
  3. Message property set to the value of the message parameter

The constructor at line #162 expands our flexibility by accepting an additional parameter of type CustomConfirmationDialogButtonOptions. You’ll recall we defined this type earlier as an enum. This parameter will allow us to determine which buttons we want displayed in our dialog box. Again constructor makes use of chaining. Calling this constructor results in the following code flow:

ConfirmationViewModel(Stackpanel buttonPanel, string message, CustomConfirmationDialogButtonOptions options)

  1. ConfirmationViewModel(Stackpanel buttonPanel, string message)
    1. ConfirmationViewModel(Stackpanel buttonPanel)
      1. Message property set to “Are you sure?”
      2. FontSize property set to 32
      3. BuildButtonList(buttonPanel, CustomConfirmationDialogButtonOptions.OK) method is called
    2. Message property set to the value of the message parameter
  2. BuildButtonList(buttonPanel, options) method is called
        public ConfirmationViewModel(StackPanel buttonPanel, string message, CustomConfirmationDialogButtonOptions options) : this(buttonPanel, message)
        {
            BuildButtonList(buttonPanel, options);
        }

The constructor found at line #174 takes it a step further. It first calls the previous constructor (which calls the previous constructor, which calls the previous constructor…), then sets the following values:

  1. The CheckBoxString string property is set to the value of the optionalCheckBoxString parameter
  2. The CheckBoxValue boolean property is set to the value of the defaultCheckBoxValue parameter
        public ConfirmationViewModel(StackPanel buttonPanel, string message, CustomConfirmationDialogButtonOptions options, string optionalCheckBoxString, bool defaultCheckBoxValue) : this(buttonPanel, message, options)
        {
            CheckBoxString = optionalCheckBoxString;
            CheckBoxValue = defaultCheckBoxValue;
        }

The constructor found at line #187 is a slight variation. It accepts StackPanel, string, and CustomConfirmationDialogButtonOptions parameters just like the overload found at line #162. However, it also accepts an integer fontSize parameter that also sets the FontSize property to the value of the fontSize parameter.

        public ConfirmationViewModel(StackPanel buttonPanel, string message, CustomConfirmationDialogButtonOptions options, int fontSize) : this(buttonPanel, message, options)
        {
            FontSize = fontSize;
        }

Lines #200-314 define the methods for our ViewModel. There are five methods – four of them simply set the ViewModel’s Result property then call the CloseAction() Action (still to be defined by our View). The fifth method is the BuildButtonList method called from our ViewModel constructors.

I’ll explain one of the four Result-setting methods and the BuildButtonList method below.

Line #200 defines our OKExecute() method, which will eventually be executed when the user clicks on an ‘OK’ button.

        internal void OKExecute()
        {
            // Dialog result
            Result = ConfirmationDialogResult.OK;
            CloseAction();
        }

Line #200 simply defines the method. Line #203 sets the ViewModel’s Result property to the value ConfirmationDialogResult.OK. Line #204 calls the CloseAction() Action, which will close the View.

Again, lines #210-236 define the other three methods to be executed when the user clicks ‘Yes’, ‘No’ or ‘Cancel’.

The BuildButtonList method on line #238 accepts a StackPanel parameter and a CustomConfirmationDialogButtonOptions parameter. Basically this method evaluates the requested CustomConfirmationDialogButtonOptions value, builds the buttons to be displayed, and places them in the supplied Stackpanel parameter.

        public void BuildButtonList(StackPanel buttonPanel, CustomConfirmationDialogButtonOptions options)
        {
            buttonPanel.Children.Clear();
 
            Button button;
            int btnMinWidth = 75;
            switch (options)
            {
                case CustomConfirmationDialogButtonOptions.OK:
                    button = new Button();
                    button.Content = "OK";
                    button.Command = OKCommand;
                    button.MinWidth = btnMinWidth;
                    button.Width = Double.NaN;
                    buttonPanel.Children.Add(button);
                    break;
 
                case CustomConfirmationDialogButtonOptions.OKCancel:
                    button = new Button();
                    button.Content = "OK";
                    button.Command = OKCommand;
                    button.MinWidth = btnMinWidth;
                    button.Width = Double.NaN;
                    buttonPanel.Children.Add(button);
 
                    button = new Button();
                    button.Content = "Cancel";
                    button.Command = CancelCommand;
                    button.MinWidth = btnMinWidth;
                    button.Width = Double.NaN;
                    buttonPanel.Children.Add(button);
                    break;

The method starts in line #240 by clearing the StackPanel’s Children collection. This is necessary due to the use of the constructor chaining that sometimes ran this method twice. We need to ensure that each time this method is run, any items contained in the StackPanel are removed – otherwise we’d possibly end up with ‘OK’, ‘Cancel’, ‘OK’ buttons when we only wanted ‘OK’ and ‘Cancel’.

Line #242 simply defines a button variable that we’ll use to represent the button being created throughout the remainder of the method. Line #243 defines an integer value that will be used to set our buttons’ MinWidth property.

Line #244 starts a switch statement that evaluates the options parameter. If you’re not familiar with the switch/case flow control mechanism, it works similar to numerous if statements. Basically, the system evaluates the value provided to the switch method as a parameter, then runs the code in the matching case code block if found.

In line #244, the system evaluates the value provided in the options parameter, then executes the code in the case block that matches the parameter. Assume that the options variable has a value of CustomConfirmationDialogButtonOptions.OK. If this is the case, then the code block in lines #246-253 will be executed. Note that a case block ends with a “break;” statement.

Assuming options has a value of CustomConfirmationDialogButtonOptions.OK, then execution will move to line #247.

  • Line #247 sets the button variable to a new Button() object.
  • Line #248 sets the button’s Content property to “OK”. This is the text that will be displayed in the button.
  • Line #249 sets the button’s Command property to “OKCommand”. This is the ICommand property on the ViewModel that points to the OKExecute() method. This is what “connect” the click of the button to the method on the ViewModel.
  • Line #250 sets the button’s MinWidth property to the value defined in line #243.
  • Line #251 sets the button’s Width property to Double.NaN.
  • Line #252 adds the button to the StackPanel’s Children property.
  • Line #253 will break out of the switch statement.

This is the code that builds the ‘OK’ button, sets some of its visual properties, and most importantly binds the button click to the appropriate OKCommand ICommand property on the ViewModel.

You’ll note lines #255-269 represent the code block to be executed if options has a value of CustomConfirmationDialogButtonOptions.OKCancel. Lines #256-261 are identical to #247-252, building the ‘OK’ button.

Lines #263-269 are also identical, save two exceptions; line #264 sets the button’s Content property to “Cancel”, and line #265 sets the button’s Command property to the ViewModel’s ICommand property CancelExecute. This builds the ‘Cancel’ button. Line #269 breaks out of the switch statement.

Lines #271-309 follow suit, building the ‘Yes’/’No’ buttons or the ‘Yes’/’No’/’Cancel’ buttons as determined by the value of the options parameter.

Now let’s look at the complete code for the View, after which we’ll go over each line in detail.

Complete View Code (code-behind)

using System;
using System.Windows;
using System.Windows.Controls;
using ProjectNamespace.ViewModels;

namespace ProjectNamespace.Views
{
    /// <summary>
    /// Interaction logic for Confirmation.xaml
    /// </summary>
    public partial class ConfirmationView : Window
    {
        private ConfirmationViewModel cvm;

        // Handle of Button StackPanel UI Control to dynamically build button list
        private StackPanel buttonStackPanel
        {
            get
            {
                return (StackPanel)MainGrid.FindName("ButtonStackPanel");
            }
        }
        
        /// <summary>
        /// Confirmation - Constructor
        /// </summary>
        public ConfirmationView()
        {
            InitializeComponent();
            cvm = new ConfirmationViewModel(buttonStackPanel);            
            this.DataContext = cvm;
            this.Owner = Application.MainWindow;

            // Close Handler
            if (cvm.CloseAction == null)
            {
                cvm.CloseAction = new Action(() => this.Close());
            }
        }

        /// <summary>
        /// Confirmation - alternate constructor
        /// </summary>
        public ConfirmationView(Window owner) : this()
        {
            this.Owner = owner;
        }

        /// <summary>
        /// Confirmation - alternate constructor
        /// </summary>
        /// <param Name="customMsg"></param>
        public ConfirmationView(string customMsg)
        {
            InitializeComponent();
            cvm = new ConfirmationViewModel(buttonStackPanel, customMsg);
            this.DataContext = cvm;
            this.Owner = Application.MainWindow;

            // Close Handler
            if (cvm.CloseAction == null)
            {
                cvm.CloseAction = new Action(() => this.Close());
            }
        }

        public ConfirmationView(string customMsg, bool autoResize) : this(customMsg)
        {
            if (autoResize)
            {
                this.Height = Double.NaN;
                this.Width = Double.NaN;
            }
        }

        /// <summary>
        /// Confirmation - alternate constructor
        /// </summary>
        /// <param Name="customMsg"></param>
        public ConfirmationView(string customMsg, Window owner) : this(customMsg)
        {
            this.Owner = owner;
        }

        /// <summary>
        /// ConfirmationView - alternate constructor
        /// </summary>
        /// <param Name="customMsg"></param>
        /// <param Name="options"></param>
        public ConfirmationView(string customMsg, CustomConfirmationDialogButtonOptions options)
        {
            InitializeComponent();
            cvm = new ConfirmationViewModel(buttonStackPanel, customMsg, options);
            this.DataContext = cvm;
            this.Owner = Application.MainWindow;

            // Close Handler
            if (cvm.CloseAction == null)
            {
                cvm.CloseAction = new Action(() => this.Close());
            }
        }

        /// <summary>
        /// ConfirmationView - alternate constructor
        /// </summary>
        /// <param Name="customMsg"></param>
        /// <param Name="options"></param>
        public ConfirmationView(string customMsg, CustomConfirmationDialogButtonOptions options, Window owner) : this(customMsg, options)
        {
            this.Owner = owner;
        }

        /// <summary>
        /// ConfirmationView - alternate constructor
        /// </summary>
        /// <param Name="customMsg"></param>
        /// <param Name="options"></param>
        /// <param Name="fontSize"></param>
        public ConfirmationView(string customMsg, CustomConfirmationDialogButtonOptions options, int fontSize)
        {
            InitializeComponent();
            cvm = new ConfirmationViewModel(buttonStackPanel, customMsg, options, fontSize);
            this.DataContext = cvm;
            this.Owner = Application.MainWindow;

            // Close Handler
            cvm.CloseAction = new Action(() => this.Close());
        }

        /// <summary>
        /// ConfirmationView - alternate constructor
        /// </summary>
        /// <param Name="customMsg"></param>
        /// <param Name="options"></param>
        /// <param Name="fontSize"></param>
        public ConfirmationView(string customMsg, CustomConfirmationDialogButtonOptions options, int fontSize, Window owner) : this(customMsg, options, fontSize)
        {
            this.Owner = owner;
        }

        /// <summary>
        /// ConfirmationView - alternate constructor
        /// </summary>
        /// <param name="customMsg"></param>
        /// <param name="options"></param>
        /// <param name="optionalCheckBoxString"></param>
        /// <param name="defaultCheckBoxValue"></param>
        public ConfirmationView(string customMsg, CustomConfirmationDialogButtonOptions options, string optionalCheckBoxString, bool defaultCheckBoxValue)
        {
            InitializeComponent();
            cvm = new ConfirmationViewModel(buttonStackPanel, customMsg, options, optionalCheckBoxString, defaultCheckBoxValue);
            this.DataContext = cvm;
            this.Owner = Application.MainWindow;
            this.Height = Double.NaN;

            // Close Handler
            cvm.CloseAction = new Action(() => this.Close());
        }

        /// <summary>
        /// ConfirmationView - alternate constructor
        /// </summary>
        /// <param name="customMsg"></param>
        /// <param name="options"></param>
        /// <param name="optionalCheckBoxString"></param>
        /// <param name="defaultCheckBoxValue"></param>
        public ConfirmationView(string customMsg, CustomConfirmationDialogButtonOptions options, string optionalCheckBoxString, bool defaultCheckBoxValue, Window owner) : this(customMsg, options, optionalCheckBoxString, defaultCheckBoxValue)
        {
            this.Owner = owner;
        }

        /// <summary>
        /// Shows the confirmation dialog, returns result
        /// </summary>
        /// <returns></returns>
        public ConfirmationDialogResult ShowCustomDialog()
        {
            this.ShowDialog();
            return cvm.Result;
        }

        /// <summary>
        /// Shows the confirmation dialog, returns result with optional checkbox out value
        /// </summary>
        /// <param name="checkBoxValue"></param>
        /// <returns></returns>
        public ConfirmationDialogResult ShowCustomDialog(out bool checkBoxValue)
        {
            this.ShowDialog();
            checkBoxValue = cvm.CheckBoxValue;
            return cvm.Result;
        }
    }
}

Lines #1-4 define the necessary usings for our dialog.

using System;
using System.Windows;
using System.Windows.Controls;
using ProjectNamespace.ViewModels;

Line #13 defines a field to house a reference to the ViewModel.

        private ConfirmationViewModel cvm;

Lines #16-22 define a StackPanel property that is a reference to the StackPanel on the View that houses the buttons.

        private StackPanel buttonStackPanel
        {
            get
            {
                return (StackPanel)MainGrid.FindName("ButtonStackPanel");
            }
        }

This simply contains a get accessor that searches the XAML of our Window for a VisualElement named “ButtonStackPanel”, casts it to a StackPanel object and returns it. As you’ll soon see, this StackPanel will be passed into the ViewModel.

If not specified,

  • the string displayed to the user is “Are you sure?”
  • the button option displayed to the user is an “OK” button
  • the dialog will be displayed centered over the applications main window
  • no optional CheckBox will be displayed

Lines #27-171 represent the various constructors for our View (a default constructor and ten overloads). Some of the various signatures of our View accept the following collections of parameters:

  • No parameters (default constructor)
  • Window (sets the owner of the dialog box, allowing control over where on screen the dialog is displayed
  • String (the message to be displayed to the user
  • String (the message to be displayed to the user) and a boolean to determine if the dialog should be auto-sized
  • String (the message to be displayed to the user) and a Window to determine where on screen the dialog is displayed
  • String (the message to be displayed to the user) and an enum indicating which buttons should be displayed (OK, OKCancel, YesNo, YesNoCancel)
  • String (the message to be displayed to the user), an enum indicating which buttons should be displayed, and a Window to determine where on screen the dialog is displayed
  • String (the message to be displayed to the user), an enum indicating which buttons should be displayed, a string to be displayed next to a CheckBox, and a bool representing the optional CheckBox’s default IsChecked value

When possible, we’ve made use of constructor chaining (as previously explained in the ViewModel section).

In lines #27-39 we have the default constructor. Let’s look at this code:

        public ConfirmationView()
        {
            InitializeComponent();
            cvm = new ConfirmationViewModel(buttonStackPanel);           
            this.DataContext = cvm;
            this.Owner = Application.MainWindow;
 
            // Close Handler
            if (cvm.CloseAction == null)
            {
                cvm.CloseAction = new Action(() => this.Close());
            }
        }

Line #29 calls the InitializeComponent() method. Line #30 sets the View field cvm to a new instance of the ViewModel (using the VM’s primary constructor), passing it a handle to the StackPanel for button composition. At line #31, we’re setting the newly created instance of the ViewModel as the DataContext of the View. This allows the many properties stored on the ViewModel to be displayed by bound controls in the View.

In line #32 we’re setting the application’s main window as the owner of this instance of the View. That allows us to specify that this View should open centered (specified in the XAML below) over the owner when displayed.

In lines #35-38, we’re defining the CloseAction Action property on the ViewModel. First, we check to see if the CloseAction is null, then we’re setting it to a new Action if it is null. This Action is the delegate () => this.Close(), which tells the system anytime it sees CloseAction() called, it should otherwise execute the code “this.Close()”. And since this.Close() is being defined from the View, this refers to the View and so it executes the View’s Close() method.

The various other overloads of the View constructor simply allow us to define other properties of the ViewModel at the instantiation of the View. I won’t get into all the details here, as it’s pretty straightforward to figure out what the different overloads do.

Instead, we’ll now focus on the two ShowCustomDialog() methods. First, understand that the View inherits from Window. As such, it inherits Window’s ShowDialog() method, which simply displays the View, but doesn’t give easy access to the ViewModel’s values after the user closes the View.

To make it easy to retrieve the Result property of the ViewModel, I created a ShowCustomDialog() method.

        public ConfirmationDialogResult ShowCustomDialog()
        {
            this.ShowDialog();
            return cvm.Result;
        }

As you can see, in line #177 we declare the method’s return type as ConfirmationDialogResult. So in line #179 we simply call the View’s inherited method ShowDialog(), and in line #180 we return the value from the ViewModel’s Result property.

The overload of ShowCustomDialog() accepts a single boolean out parameter. Out parameters allow a function to return more than a single value.

public ConfirmationDialogResult ShowCustomDialog(out bool checkBoxValue)
        {
            this.ShowDialog();
            checkBoxValue = cvm.CheckBoxValue;
            return cvm.Result;
        }

In line #188, we’re accepting an out boolean parameter called checkBoxValue. This parameter allows us to set a value to the variable, and have that value passed back to the calling method (similar to a function’s return value). The only difference between this overload and the first ShowCustomDialog() method mentioned above is that I’m setting the ViewModel’s CheckBoxValue value to the out parameter checkBoxValue. You’ll see in the XAML section that CheckBoxValue will be bound to the IsChecked property of a CheckBox, easily allowing you to obtain the IsChecked value of the CheckBox after the View has been closed.

Let’s now take a look at the XAML for the window, and we’ll follow that with an explanation of how to implement the View and obtain its results.

Complete View XAML

<Window x:Class="ProjectNamespace.Views.ConfirmationView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:ProjectNamespace.Converters"
        Height="275" Width="375"
        Title="Confirmation"
        ShowInTaskbar="False"
        ResizeMode="NoResize"
        Topmost="True"
        WindowStartupLocation="CenterOwner"
        WindowStyle="None"
        BorderBrush="{DynamicResource DefaultedBorderBrush}"
        BorderThickness="10">
    <Window.Resources>
        <local:NullToCollapsedConverter x:Key="NullToCollapsedConverter"/>
    </Window.Resources>
    <Grid x:Name="MainGrid">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="10*"/>
            <ColumnDefinition Width="260*"/>
            <ColumnDefinition Width="10*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="10*"/>
            <RowDefinition Height="180*"/>
            <RowDefinition Height="75*"/>
            <RowDefinition Height="20*"/>
            <RowDefinition Height="10*"/>
        </Grid.RowDefinitions>

        <ScrollViewer Grid.Row="1" Grid.Column="1" VerticalScrollBarVisibility="Auto">
            <Viewbox Name="vb" Grid.Row="1" Grid.Column="1" Stretch="Uniform">
                <TextBlock Text="{Binding Message}" FontSize="{Binding FontSize}"
                       TextWrapping="Wrap"
                       TextAlignment="Center" />
        </Viewbox>
        </ScrollViewer>

        <StackPanel Name="ButtonStackPanel"
                    Grid.Row="2" Grid.Column="1"
                    HorizontalAlignment="Center"
                    Orientation="Horizontal"/>

        <CheckBox Grid.Row="3" Grid.Column="1" HorizontalContentAlignment="Center" HorizontalAlignment="Center" VerticalContentAlignment="Center" VerticalAlignment="Center"
                  IsChecked="{Binding CheckBoxValue}"
                  Visibility="{Binding CheckBoxString, Converter={StaticResource NullToCollapsedConverter}}"
                  Content="{Binding CheckBoxString}"/>
    </Grid>
</Window>

The XAML is pretty straightforward, so I won’t go through it line by line. I will, however, explain each binding as well as a referenced IValueConverter.

Basically, the Window consists of a grid that contains a TextBlock, a StackPanel, a CheckBox and an IValueConverter.

Lines #14-16 build the View’s reference to the NullToCollapsedConverter IValueConverter (code shown later). This particular converter allows us to bind a View element’s Visible property to a property that might return null (in our case, a string), and convert that null/not null value to a Visibility value of Visibility.Collapsed or Visibility.Visible.

The grid contains three columns and five rows. The first and last columns and the first and last rows are simply used as a way to give the Window elements some white space between the elements and the edge of the Window. Since the View’s Grid only contains three columns, all of the View’s visual elements appear in the second Grid column. Grid rows and columns are numbered starting with 0, so Grid.Column = “1” actually means the element appears in the Grid’s second column.

Lines #33-35 renders a TextBlock, which has two of its properties bound to ViewModel properties. The TextBlock.Text property is bound to the Message property, and the TextBlock.FontSize property is bound to the ViewModel’s FontSize property.

Lines #39-42 renders a StackPanel. You’ll see no binding in the XAML, as this is the StackPanel that the View’s constructor obtained by name and passed to the ViewModel. The ViewModel then populated this StackPanel’s Children collection with buttons as defined by the optionally provided CustomConfirmationDialogButtonOption value.

Lines #44-47 create the optional CheckBox. You’ll see that its Content property is bound to the ViewModel’s CheckBoxString property, and its IsChecked property is bound to the ViewModel’s CheckBoxValue property. Also, this CheckBox references a NullToCollapsedConverter IValueConverter, which allows us to bind its Visibility property to any nullable property on the ViewModel. I’ve chosen to bind the Visibility property to the same string property as its Content property – CheckBoxString. This way, if the View constructor that accepts an optionalCheckBoxString string as a parameter is called, then the ViewModel’s CheckBoxString property is set (changing from a null value to the string value). The View’s CheckBox’s Visibility property, bound to this string with an IValueConverter, is then set to Visibility.Visible, allowing us to see the CheckBox. If no View constructor accepting the optionalCheckBoxString parameter is used, then the ViewModel’s CheckBoxString property remains null, and the View’s CheckBox’s Visibility property, using the NullToCollapsedConverter IValueConverter, is set to Visibility.Collapsed, preventing it from being displayed in the View.

Let’s walk through a few different ways of implementing this View. Suppose in another part of our app we want to prompt the user with a message, and get an answer from the user. We could do something like the following:

...
Views.ConfirmationView cv = new ConfirmationView("Are you sure you wish to terminate the employee?", CustomConfirmationDialogButtonOption.YesNo);
var result = cv.ShowCustomDialog();
if(result == ConfirmationDialogResult.Yes)
{
    // todo: Enter code here to execute if user selected "Yes" 
} 

In line #2 of our example, the system creates a new instance of the ConfirmationView and passes it two parameters – a string message to be displayed to the user, and an enum representing which button options to present to the user. Part of the View’s construction (this actually calls the constructor found on line #90 of the View code-behind) then passes the pertinent values over to a new instance of the ViewModel and sets that ViewModel as the DataContext for the View. Line #3 then calls the View’s ShowCustomDialog() function, which in turn simply shows the View. Once the View closes, it will return the ViewModel’s Result property. Line #3 also stores this returned value into a variable called result. Once result has been defined (the View has closed), we can act on the value of result.

Alternately, if we construct the View using the optionalCheckBoxString constructor, then we can display the View using the overload of ShowCustomDialog() that accepts an out parameter. This will give us easy access to the result of the CheckBox upon the View closing. For example:

...
public bool PromptAgain { get; set; }
...
if(PromptAgain == null || PromptAgain == true)
{
    Views.ConfirmationView cv = new ConfirmationView("Do you wish to delete the selected item?",         CustomConfirmationDialogButtonOptions.YesNo, "Do not ask me again", false);
    bool doNotAskAgain = false;
    ConfirmationDialogResult result = cv.ShowCustomDialog(out doNotAskAgain);
    PromptAgain = !doNotAskAgain;
    if(result == ConfirmationDialogResult.No)
    {
        return;
    }
}

// todo: enter delete item code here

In this possible code scenario, line #2 defines a boolean property PromptAgain. Line #4 checks this boolean property for a null (never been set) value or a true value. If PromptAgain is null or true, then line #6 creates an instance of the View with the following parameters:

  • string “Do you wish to delete the selected item?” – the message to display to the user
  • CustomConfirmationDialogButtonOptions.YesNo – show only “Yes” and “No” buttons
  • string “Do not ask me again” – the content of an optional CheckBox to be displayed
  • bool false – the default IsChecked value of the optional CheckBox

Line #7 defines a boolean that represents both the default CheckBox IsChecked value and will house the CheckBox’s IsChecked value when the View closes. It does this through the use of the ShowCustomDialog(out askAgain) overload used in line #8. When the View closes, line #9 then sets the PromptAgain boolean to the opposite of the doNotAskAgain variable. That is to say, if the user did not click “Do not ask me again” (doNotAskAgain = false), then they should be prompted the next time around, which means the PromptAgain property should be set to true. Conversely, if they did click “Do not ask me again” (doNotAskAgain = true), then the property PromptAgain should be set to false.

When the View is displayed to the user in line #8, assume the user checks the box next to “Do not ask me again” then clicks the “Yes” button. The out value of doNotAskAgain is true (because the user clicked the box), and PromptAgain should therefore be set to false – or more simply the opposite of true. Line #10 then evaluates the return value of the View (which should be ConfirmationDialogResult.Yes, because the user clicked the ‘Yes’ button). Code flow should then evaluate the if statement on line #10 and skip the code because the if statement evaluates to false.

The next time this bit of code is hit (barring any other changes in PromptAgain), the user shouldn’t be prompted with this View because the PromptAgain value has been set to false.

Complete NullToCollapsedConverter Code

using System;
using System.Windows.Data;

namespace ProjectNamespace.Converters
{
    class NullToCollapsedConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value == null)
            {
                return System.Windows.Visibility.Collapsed;
            }
            else
            {
                return System.Windows.Visibility.Visible;
            }
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

Conclusion
This concludes the explanation of a custom WPF dialog box. I’ve shown you how to create a ViewModel with various constructor overloads, related View constructors (with related overloads), and explained the XAML behind the View.

Addendum
If you want to expand this dialog abilities with additional button options, you’d have to make the following changes:

  1. Expand CustomConfirmationDialogButtonOptions enum with other button combinations (for example, how about an “AcceptRefuse” option?
  2. Expand ConfirmationDialogResult enum to handle additional button options (add “Accept” and “Refuse” as enum entries
  3. Add the necessary ViewModel methods to handle the newly optional button clicks – such as an AcceptExecute() method and a RefuseExecute() method
  4. Add the appropriate RelayCommand/ICommand pairs to the ViewModel allowing our new buttons to be bound to the appropriate methods (continuing our example, we’d need an AcceptCommand and RefuseCommand set of RelayCommand/ICommand pairs
  5. Finally, modify the ViewModel’s BuildButtonList() method to handle the creation of these newly desired buttons
  6. Addendum #2
    It occurred to me shortly after publishing this article that the ViewModel could be made significantly simpler by employing the button’s Tag property and the button’s CommandParameter property. You see, Buttons have a Tag property of type object, which allows us to assign any object to the property – including an Enum value.

    Since all of our ButtonExecute methods (OKExecute, CancelExecute, etc) simply set the ViewModel’s Result property to the appropriate value and then closed the View, we can have all of our buttons bound to the same RelayCommand and execute the same method. The difference is that we’ll build the Button with the Tag property set as the appropriate related Enum value, then pass the Button’s Tag property in as a parameter to the bound Command.

    Take a look at the following code:

            private RelayCommand buttonCommand;
            public RelayCommand ButtonCommand
            {
                get
                {
                    if (buttonCommand == null)
                    {
                        buttonCommand = new RelayCommand(param => ButtonExecute((ConfirmationDialogResult)param));
                    }
                    return buttonCommand;
                }
            }
    
            internal void ButtonExecute(ConfirmationDialogResult result)
            {
                Result = result;
                CloseAction();
            }
    

    and

            public bool BuildButtonList(StackPanel buttonPanel, CustomConfirmationDialogButtonOptions options)
            {
                buttonPanel.Children.Clear();
    
                Button button;
                int btnMinWidth = 75;
                switch (options)
                { 
                    case CustomConfirmationDialogButtonOptions.OK:
                        button = new Button();
                        button.Tag = ConfirmationDialogResult.OK;
                        button.Content = "OK";
                        button.Command = ButtonCommand;
                        button.CommandParameter = button.Tag;
                        button.MinWidth = btnMinWidth;
                        button.Width = Double.NaN;
                        buttonPanel.Children.Add(button);
                        break;
    

    You can see in the “generic” ButtonCommand RelayCommand, we’re now passing a ConfirmationDialogResult parameter to the ButtonExecute method. We’re getting this value from the Button’s Tag property – which is set to an enum value when the button is built, and which is passed as a parameter via the button’s CommandParameter property.

    You can see in the ButtonExecute method we’re simply setting the ViewModel’s Result property to the value passed in as a parameter.

    This significantly simplifies expanding this control, as it removes the need to build extra RelayCommand field/property pairs as well as their related methods.

2 thoughts on “Creating a flexible custom WPF dialog

Leave a Reply

Your email address will not be published. Required fields are marked *