Search code examples
c#wpfmvvmmvvm-lightmahapps.metro

Wait for MahApps Metro Dialog to return result


I have a ViewModel sending a message (using MVVM Light Messenger) to the View to show a Metro Dialog as follows:

In ViewModel, I call this code from the DialogBox class:

DialogBox.ShowDialogBox(
                        (result) => { DialogResult(result); },
                        "Dialog Title",
                        "Dialog Message",
                        MessageDialogStyle.AffirmativeAndNegative
                        );

This is the DialogBox Class which deals with sending the message to the View:

public class DialogBox
{
    public Action<MessageDialogResult> dialogResultCallBack { get; set; }
    public string dialogTitle;
    public string dialogText;
    public MessageDialogStyle dialogStyle;
    public string okButtonText;
    public string cancelButtonText;

    public DialogBox(Action<MessageDialogResult> _dialogResultCallBack, string _dialogTitle, string _dialogText, MessageDialogStyle _dialogStyle, string _okButtonText, string _cancelButtonText)
    {
        dialogResultCallBack = _dialogResultCallBack;
        dialogTitle = _dialogTitle;
        dialogText = _dialogText;
        dialogStyle = _dialogStyle;
        okButtonText = _okButtonText;
        cancelButtonText = _cancelButtonText;
    }


    public static void ShowDialogBox(Action<MessageDialogResult> _dialogResultCallBack, string _dialogTitle, string _dialogText,
        MessageDialogStyle _dialogStyle, string _affirmativeButtonText = "OK", string _negativeButtonText = "Cancel")
    {
        Messenger.Default.Send(new DialogBox(
                       _dialogResultCallBack,
                       _dialogTitle,
                       _dialogText,
                       _dialogStyle,
                       _affirmativeButtonText,
                       _negativeButtonText), GlobalResources.MessengerTokens.dialogTokenMainWindow);
    }
}

The View codebehind has the following code to receive the message:

Messenger.Default.Register<DialogBox>(this, GlobalResources.MessengerTokens.dialogTokenMainWindow, dialogData =>
            {
                ShowMessageDialog(dialogData);
            });

And the ShowMessageDialog deals with showing the actual dialog. These all work fine.

Currently, when the user has selected either Affirmative/Negative, the result is returned and triggers an action call to DialogResult(result) in the ViewModel as seen in the topmost code snippet.

private void DialogResult(MessageDialogResult result)
    {
        if (result == MessageDialogResult.Affirmative)
        {
             //deal with the situation
        }
        else
        {
            //deal with the situation
        }
    }

I would actually like to wait for the result straight away after calling the DialogBox.ShowDialogBox() method in the ViewModel. The current approach is causing the code to jump to a separate method call rather than being able to deal with the result straight away. To illustrate this briefly,

if(condition)
{
  DialogBox.ShowDialogBox(...);

  //Needs some sort of await method to wait for results here

  if(result == MessageDialogResult.Affirmative)
  {
         //do stuff
  }
  else
  {
        //do stuff
  }
 }

I have seen some sample code at least on WinForms that waiting for a result is easier (using codebehind and without MVVM) by doing something like:

if (MessageBox.Show("Title", "Message", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning) == System.Windows.Forms.DialogResult.OK)

Could there be a similar approach I could take for my current situation? Thanks for any suggestions and sorry if my question was too long.


Solution

  • I think there is a better way of doing what you are doing. To make this more MVVM, this is what I do... Firstly, I use Caliburn Micro to handle my MVVM stuff and MEF. So first we have two interfaces:

    internal interface IDialogViewModel
    {
        event EventHandler Closed;
    }
    

    and the following interface will help you get results for your dialog

    public interface IDialogManager
    {
        /// <summary>
        /// Show a dialog that performs as Task with generic return type. 
        /// </summary>
        /// <typeparam name="TResult">The result to be returned from the dialog task.</typeparam>
        /// <param name="viewModel">The DialogViewModel type to be displayed.</param>
        /// <returns>The Task to be awaited.</returns>
        Task<TResult> ShowDialog<TResult>(DialogViewModel<TResult> viewModel);
    
        /// <summary>
        /// Show a dialog that performs as Task.
        /// </summary>
        /// <param name="viewModel">The result to be returned from the dialog task.</param>
        /// <returns>The Task to be awaited.</returns>
        Task ShowDialog(DialogViewModel viewModel);
    }
    

    The implementation of these interfaces are

    /// <summary>
    /// DialogViewModel class which should be inherited for all view 
    /// model that want to be displayed as metro dialogs.
    /// </summary>
    public abstract class DialogViewModel : Screen, IDialogViewModel
    {
        private readonly TaskCompletionSource<int> tcs;
        internal Task Task { get { return tcs.Task; } }
    
        /// <summary>
        /// Deafult constructor.
        /// </summary>
        protected DialogViewModel()
        {
            tcs = new TaskCompletionSource<int>();
        }
    
        /// <summary>
        /// Close the dialog.
        /// </summary>
        protected void Close()
        {
            tcs.SetResult(0);
            var handler = Closed;
            if (handler != null)
                handler(this, EventArgs.Empty);
        }
    
        /// <summary>
        /// Closed event.
        /// </summary>
        public event EventHandler Closed;
    }
    
    /// <summary>
    /// DialogViewModel class which should be inherited for all view 
    /// model that want to be displayed as metro dialogs that can return a 
    /// specific result.
    /// </summary>
    public abstract class DialogViewModel<TResult> : Screen, IDialogViewModel
    {
        private readonly TaskCompletionSource<TResult> tcs;
        internal Task<TResult> Task { get { return tcs.Task; } }
    
        /// <summary>
        /// Deafult constructor.
        /// </summary>
        protected DialogViewModel()
        {
            tcs = new TaskCompletionSource<TResult>();
        }
    
        /// <summary>
        /// Close the dialog.
        /// </summary>
        protected void Close(TResult result)
        {
            tcs.SetResult(result);
            var handler = Closed;
            if (handler != null)
                handler(this, EventArgs.Empty);
        }
    
        /// <summary>
        /// Closed event.
        /// </summary>
        public event EventHandler Closed;
    }
    

    and the manager class is

    /// <summary>
    /// The DialogManager that can be used to show Views as Metro modal dialogs.
    /// Import IDialogManager to any view model that needs to show a metro message 
    /// box.
    /// </summary>
    [Export(typeof(IDialogManager))]
    public class DialogManager : IDialogManager
    {
        /// <summary>
        /// Show the required dialog.
        /// </summary>
        /// <param name="viewModel">The view model ascociated with the view.</param>
        public async Task ShowDialog(DialogViewModel viewModel)
        {
            // Locate the ascociated view.
            var viewType = ViewLocator.LocateTypeForModelType(viewModel.GetType(), null, null);
            var dialog = (BaseMetroDialog)Activator.CreateInstance(viewType);
            if (dialog == null)
            {
                throw new InvalidOperationException(
                    String.Format("The view {0} belonging to view model {1} " +
                                      "does not inherit from {2}",
                                      viewType,
                                      viewModel.GetType(),
                                      typeof(BaseMetroDialog)));
            }
            dialog.DataContext = viewModel;
    
            // Show the metro window.
            MetroWindow firstMetroWindow = 
                Application.Current.Windows.OfType<MetroWindow>().First();
            await firstMetroWindow.ShowMetroDialogAsync(dialog);
            await viewModel.Task;
            await firstMetroWindow.HideMetroDialogAsync(dialog);
        }
    
        /// <summary>
        /// Show the required dialog.
        /// </summary>
        /// <typeparam name="TResult">The type of result to return.</typeparam>
        /// <param name="viewModel">The view model ascociated with the view.</param>
        public async Task<TResult> ShowDialog<TResult>(DialogViewModel<TResult> viewModel)
        {
            // Locate the ascociated view.
            var viewType = ViewLocator.LocateTypeForModelType(viewModel.GetType(), null, null);
            var dialog = (BaseMetroDialog)Activator.CreateInstance(viewType);
            if (dialog == null)
            {
                throw new InvalidOperationException(
                    String.Format("The view {0} belonging to view model {1} " +
                                      "does not inherit from {2}",
                                      viewType,
                                      viewModel.GetType(),
                                      typeof(BaseMetroDialog)));
            }
            dialog.DataContext = viewModel;
    
            // Show the metro window.
            MetroWindow firstMetroWindow = Application.Current.Windows.OfType<MetroWindow>().First();
            await firstMetroWindow.ShowMetroDialogAsync(dialog);
            TResult result = await viewModel.Task;
            await firstMetroWindow.HideMetroDialogAsync(dialog);
            return result;
        }
    }
    

    We also have the message box settings

    /// <summary>
    /// Class that holds the settings for message box dialogs.
    /// </summary>
    public class MessageBoxSettings
    {
        /// <summary>
        /// Default constructor.
        /// </summary>
        public MessageBoxSettings()
        {
            this.AffirmativeButtonText = "OK";
            this.NegativeButtonText = "Cancel";
            this.MessageDialogStyle = MessageDialogStyle.AffirmativeAndNegative;
            this.MetroColorDialogScheme = MetroDialogColorScheme.Theme;
            this.Animation = false;
        }
    
        /// <summary>
        /// Sets the button styles to use.
        /// Default is 'OK' and 'Cancel'.
        /// </summary>
        public MessageDialogStyle MessageDialogStyle { get; set; }
    
        /// <summary>
        /// The color sheme to use for the dialog.
        /// </summary>
        public MetroDialogColorScheme MetroColorDialogScheme { get; set; }
    
        /// <summary>
        /// Affirmative button text. Default OK.
        /// </summary>
        public string AffirmativeButtonText { get; set; }
    
        /// <summary>
        /// The negative button text to use.
        /// </summary>
        public string NegativeButtonText { get; set; }
    
        /// <summary>
        /// First auxillary button text.
        /// </summary>
        public string FirstAuxillaryButtonText { get; set; }
    
        /// <summary>
        /// Second auxillary button text.
        /// </summary>
        public string SecondAuxiliaryButtonText { get; set; }
    
        /// <summary>
        /// Show animation on the dialog.
        /// </summary>
        public bool Animation { get; set; }
    }
    

    Now the view and view model that actually use the code above are

    /// <summary>
    /// View model for the message box view.
    /// </summary>
    public class MessageBoxViewModel : DialogViewModel<MessageDialogResult>
    {
        private MessageBoxSettings settings;
    
        #region Initialisation.
        /// <summary>
        /// Null.
        /// </summary>
        public MessageBoxViewModel() { }
    
        /// <summary>
        /// Default constructor.
        /// </summary>
        /// <param name="title">The title of the message box dialog.</param>
        /// <param name="message">The message to display in the message box.</param>
        public MessageBoxViewModel(string title, string message)
        {
            this.Title = title;
            this.DialogBody = message;
            if (this.settings == null)
                this.settings = new MessageBoxSettings();
            SetDialogVisuals();
        }
    
        /// <summary>
        /// Overloaded.
        /// </summary>
        /// <param name="title">The title of the message box dialog.</param>
        /// <param name="message">The message to display in the message box.</param>
        /// <param name="settings">MessageBoxSettings for the dialog.</param>
        public MessageBoxViewModel(string title, string message, MessageBoxSettings settings)
        {
            this.Title = title;
            this.DialogBody = message;
            this.settings = settings;
            SetDialogVisuals();
        }
        #endregion // Initialisation.
    
        #region Class Methods.
        /// <summary>
        /// Set the dialog visuals based on the MessageBoxSettings.
        /// </summary>
        private void SetDialogVisuals()
        {
            // Set dialog button visibility.
            switch (settings.MessageDialogStyle)
            {
                case MessageDialogStyle.Affirmative:
                    this.AffirmativeButtonVisibility = Visibility.Visible;
                    this.NegativeButtonVisibility = Visibility.Collapsed;
                    this.FirstAuxillaryButtonVisibility = Visibility.Collapsed;
                    this.SecondAuxillaryButtonVisibility = Visibility.Collapsed;
                    break;
                case MessageDialogStyle.AffirmativeAndNegative:
                    this.AffirmativeButtonVisibility = Visibility.Visible;
                    this.NegativeButtonVisibility = Visibility.Visible;
                    this.FirstAuxillaryButtonVisibility = Visibility.Collapsed;
                    this.SecondAuxillaryButtonVisibility = Visibility.Collapsed;
                    break;
                case MessageDialogStyle.AffirmativeAndNegativeAndDoubleAuxiliary:
                    this.AffirmativeButtonVisibility = Visibility.Visible;
                    this.NegativeButtonVisibility = Visibility.Visible;
                    this.FirstAuxillaryButtonVisibility = Visibility.Visible;
                    this.SecondAuxillaryButtonVisibility = Visibility.Visible;
                    break;
                case MessageDialogStyle.AffirmativeAndNegativeAndSingleAuxiliary:
                    this.AffirmativeButtonVisibility = Visibility.Visible;
                    this.NegativeButtonVisibility = Visibility.Visible;
                    this.FirstAuxillaryButtonVisibility = Visibility.Visible;
                    this.SecondAuxillaryButtonVisibility = Visibility.Collapsed;
                    break;
                default:
                    break;
            }
    
            // Set the button text.
            this.AffirmativeButtonText = settings.AffirmativeButtonText;
            this.NegativeButtonText = settings.NegativeButtonText;
            this.FirstAuxillaryButtonText = settings.FirstAuxillaryButtonText;
            this.SecondAuxiliaryButtonText = settings.SecondAuxiliaryButtonText;
    
            // Color scheme.
            string name = MahApps.Metro.ThemeManager.DetectAppStyle(Application.Current).Item2.Name;
            this.Background = settings.MetroColorDialogScheme == MetroDialogColorScheme.Theme ?
                MahApps.Metro.ThemeManager.Accents
                    .Where(a => a.Name.CompareNoCase(name))
                    .First().Resources["HighlightBrush"] as SolidColorBrush :
                new SolidColorBrush(System.Windows.Media.Colors.White);
        }
    
        /// <summary>
        /// Handles the button click events for the affermative button.
        /// </summary>
        public void AffirmativeButtonClick()
        {
            Close(MessageDialogResult.Affirmative);
        }
    
        /// <summary>
        /// Handles the button click events for the negative button.
        /// </summary>
        public void NegativeButtonClick()
        {
            Close(MessageDialogResult.Negative);
        }
    
        /// <summary>
        /// Handles the button click events for the first auxillary button.
        /// </summary>
        public void FirstAuxillaryButtonClick()
        {
            Close(MessageDialogResult.FirstAuxiliary);
        }
    
        /// <summary>
        /// Handles the button click events for the second auxillary button.
        /// </summary>
        public void SecondAuxillaryButtonClick()
        {
            Close(MessageDialogResult.SecondAuxiliary);
        }
        #endregion // Class Methods.
    
        #region Properties.
        /// <summary>
        /// Hold the dialog title.
        /// </summary>
        private string title;
        public string Title
        {
            get { return title; }
            set
            {
                if (title == value)
                    return;
                title = value;
                NotifyOfPropertyChange(() => Title);
            }
        }
    
        /// <summary>
        /// Hold the text for the dialog body.
        /// </summary>
        private string dialogBody;
        public string DialogBody
        {
            get { return dialogBody; }
            set
            {
                if (dialogBody == value)
                    return;
                dialogBody = value;
                NotifyOfPropertyChange(() => DialogBody);
            }
        }
    
        /// <summary>
        /// Sets the button styles to use.
        /// Default is 'OK' and 'Cancel'.
        /// </summary>
        private MessageDialogStyle messageDialogStyle =
            MessageDialogStyle.AffirmativeAndNegative;
        public MessageDialogStyle MessageDialogStyle
        {
            get { return messageDialogStyle; }
            set
            {
                if (messageDialogStyle == value)
                    return;
                messageDialogStyle = value;
                NotifyOfPropertyChange(() => MessageDialogStyle);
            }
        }
    
        /// <summary>
        /// The color sheme to use for the dialog.
        /// </summary>
        private SolidColorBrush background;
        public SolidColorBrush Background
        {
            get { return background; }
            set
            {
                if (background == value)
                    return;
                background = value;
                NotifyOfPropertyChange(() => Background);
            }
        }
    
        /// <summary>
        /// Affirmative button text. Default OK.
        /// </summary>
        private string affirmativeButtonText = "OK";
        public string AffirmativeButtonText
        {
            get { return affirmativeButtonText; }
            set
            {
                if (affirmativeButtonText == value)
                    return;
                affirmativeButtonText = value;
                NotifyOfPropertyChange(() => AffirmativeButtonText);
            }
        }
    
        /// <summary>
        /// Visibility for the default affirmative button.
        /// </summary>
        private Visibility affirmativeButtonVisibility = Visibility.Visible;
        public Visibility AffirmativeButtonVisibility
        {
            get { return affirmativeButtonVisibility = Visibility.Visible; }
            set
            {
                if (affirmativeButtonVisibility == value)
                    return;
                affirmativeButtonVisibility = value;
                NotifyOfPropertyChange(() => AffirmativeButtonVisibility);
            }
        }
    
        /// <summary>
        /// The negative button text to use.
        /// </summary>
        private string negativeButtonText = "Cancel";
        public string NegativeButtonText
        {
            get { return negativeButtonText; }
            set
            {
                if (negativeButtonText == value)
                    return;
                negativeButtonText = value;
                NotifyOfPropertyChange(() => NegativeButtonText);
            }
        }
    
        /// <summary>
        /// Visibility for the default negative button.
        /// </summary>
        private Visibility negativeButtonVisibility = Visibility.Visible;
        public Visibility NegativeButtonVisibility
        {
            get { return negativeButtonVisibility; }
            set
            {
                if (negativeButtonVisibility == value)
                    return;
                negativeButtonVisibility = value;
                NotifyOfPropertyChange(() => NegativeButtonVisibility);
            }
        }
    
        /// <summary>
        /// First auxillary button text.
        /// </summary>
        private string firstAuxillaryButtonText;
        public string FirstAuxillaryButtonText
        {
            get { return firstAuxillaryButtonText; }
            set
            {
                if (firstAuxillaryButtonText == value)
                    return;
                firstAuxillaryButtonText = value;
                NotifyOfPropertyChange(() => FirstAuxillaryButtonText);
            }
        }
    
        /// <summary>
        /// First auxillary button visibility.
        /// </summary>
        private Visibility firstAuxillaryButtonVisibility = Visibility.Collapsed;
        public Visibility FirstAuxillaryButtonVisibility
        {
            get { return firstAuxillaryButtonVisibility; }
            set
            {
                if (firstAuxillaryButtonVisibility == value)
                    return;
                firstAuxillaryButtonVisibility = value;
                NotifyOfPropertyChange(() => FirstAuxillaryButtonVisibility);
            }
        }
    
        /// <summary>
        /// Second auxillary button text.
        /// </summary>
        private string secondAuxiliaryButtonText;
        public string SecondAuxiliaryButtonText
        {
            get { return secondAuxiliaryButtonText; }
            set
            {
                if (secondAuxiliaryButtonText == value)
                    return;
                secondAuxiliaryButtonText = value;
                NotifyOfPropertyChange(() => SecondAuxiliaryButtonText);
            }
        }
    
        /// <summary>
        /// Second auxillary button visibility.
        /// </summary>
        private Visibility secondAuxillaryButtonVisibility = Visibility.Collapsed;
        public Visibility SecondAuxillaryButtonVisibility
        {
            get { return secondAuxillaryButtonVisibility; }
            set
            {
                if (secondAuxillaryButtonVisibility == value)
                    return;
                secondAuxillaryButtonVisibility = value;
                NotifyOfPropertyChange(() => SecondAuxillaryButtonVisibility);
            }
        }
        #endregion // Properties.
    }
    

    The view is

    <MahAppsDialogs:CustomDialog 
            x:Class="GambitFramework.Core.MessageBox.MessageBoxView"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:Caliburn="http://www.caliburnproject.org"
            xmlns:MahApps="http://metro.mahapps.com/winfx/xaml/controls"
            xmlns:Local="clr-namespace:GambitFramework.Core.MessageBox"
            xmlns:Converters="clr-namespace:GambitFramework.Core.Converters;assembly=GambitFramework"
            xmlns:MahAppsDialogs="clr-namespace:MahApps.Metro.Controls.Dialogs;assembly=MahApps.Metro"
            Title="{Binding Title}">
        <MahAppsDialogs:CustomDialog.Content>
            <TextBlock Text="{Binding DialogBody}"
                          Margin="0,5,0,0"
                          TextWrapping="Wrap"/>
        </MahAppsDialogs:CustomDialog.Content>
        <MahAppsDialogs:CustomDialog.DialogBottom>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="25*" />
                    <ColumnDefinition Width="50*" />
                    <ColumnDefinition Width="25*" />
                </Grid.ColumnDefinitions>
                <StackPanel Grid.Column="1"
                                Orientation="Horizontal" 
                                HorizontalAlignment="Right"
                                Margin="0,0,0,0">
                    <Button x:Name="AffirmativeButton" 
                              Content="{Binding AffirmativeButtonText}"
                              Visibility="{Binding AffirmativeButtonVisibility}"
                              Style="{StaticResource AccentedSquareButtonStyle}"
                              Caliburn:Message.Attach="[Event Click] = [Action AffirmativeButtonClick()]"
                              MinWidth="75"
                              Padding="15,0"
                              Margin="5,10,0,5"/>
                    <Button x:Name="NegativeButton" 
                              Content="{Binding NegativeButtonText}"
                              Visibility="{Binding NegativeButtonVisibility}" 
                              Caliburn:Message.Attach="[Event Click] = [Action NegativeButtonClick()]"
                              MinWidth="75"
                              Padding="15,0"
                              Margin="10,10,0,5"/>
                    <Button x:Name="FirstAuxiliaryButton" 
                              Content="{Binding FirstAuxillaryButtonText}"
                              Visibility="{Binding FirstAuxillaryButtonVisibility}"
                              Caliburn:Message.Attach="[Event Click] = [Action FirstAuxillaryButtonClick()]"
                              MinWidth="75"
                              Padding="15,0"
                              Margin="5,10,0,5"/>
                    <Button x:Name="SecondAuxiliaryButton" 
                              Content="{Binding SecondAuxiliaryButtonText}"
                              Visibility="{Binding SecondAuxillaryButtonVisibility}"
                              Caliburn:Message.Attach="[Event Click] = [Action SecondAuxillaryButtonClick()]"
                              MinWidth="75"
                              Padding="15,0"
                              Margin="5,10,0,5"/>
                </StackPanel>
            </Grid>
        </MahAppsDialogs:CustomDialog.DialogBottom>
    </MahAppsDialogs:CustomDialog>
    

    This view has an empty code behind. This code can then be used as follows

    MessageBoxSettings settings = new MessageBoxSettings()
    {
        MessageDialogStyle = MessageDialogStyle.AffirmativeAndNegative,
        MetroColorDialogScheme = MetroDialogColorScheme.Accented,
        AffirmativeButtonText = "Delete",
        NegativeButtonText = "Cancel"
    };
    string message = String.Format(
        "Are you sure you want to delete back test \"{0}\" {1}",
        SelectedBackTest.Name,
        SelectedBackTest.LastRun == null ? 
            String.Empty : 
            String.Format("which was late run on {0:G}?", SelectedBackTest.LastRun));
    MessageDialogResult r = await dialogManager
        .ShowDialog<MessageDialogResult>(
            new MessageBoxViewModel("Confirm Delete", message, settings));
    if (r == MessageDialogResult.Affirmative)
    {
        ...
    }
    

    I hope this helps.