Up Development guide

Dialogs and Message Boxes

Last modified on November 28, 2012 01:47

The Cocktail DialogManager provides a convenient way to handle user prompts and dialog boxes.


Introduction

Message boxes and dialog boxes are commonly found in any given application. To display a message box, it's very tempting to use the standard MessageBox class. Problem with that is you can't style it and it just feels out of place, but as a developer you also don't want to spend your time on creating views and view models for message boxes and dialog boxes.

In addition, a user prompt is actually an asynchronous operation in particular in Silverlight where there is no such thing as a modal dialog box. The application has to wait for the user to respond and then continue with the current workflow. In more technical terms, a user prompt is often part of a larger coroutine with asynchronous operations before and after. 

Meet the DialogManager

Cocktail provides a nice little service that deals with displaying user prompts in general. The service is called the DialogManager. The DialogManager is made up of two parts. The IDialogManager interface and the default DialogManager implementation.

During start-up of the application, the bootstrapper ensures that the DialogManager gets added to the composition container. 

Displaying a message prompt

The simplest use case of the DialogManager is to display a message with some standard buttons for the user to respond. The following example displays a message box asking the user "Are you sure?" with a Yes and a No button.

C#
[Export]
public class MainPageViewModel
{
   private readonly IDialogManager _dialogManager;

    [ImportingConstructor]
   public MainPageViewModel(IDialogManager dialogManager)
    {
        _dialogManager = dialogManager;
    }

   public void ShowPrompt()
    {
        _dialogManager.ShowMessageAsync("Are you sure?", DialogButtons.YesNo);
    }
}

So far so good. In response to a bound action in the UI, this ViewModel displays the said message box and by clicking Yes or No, the user can dismiss the message. That's nice, but not so useful at the moment. As a developer you probably need to wait for the user's response and then continue one way or the other depending on whether the response was Yes or No.

So, let's turn this into a coroutine that waits for the user's response and displays another message based on the response.

C#
[Export]
public class MainPageViewModel
{
   private readonly IDialogManager _dialogManager;

    [ImportingConstructor]
   public MainPageViewModel(IDialogManager dialogManager)
    {
        _dialogManager = dialogManager;
    }

   public IEnumerable<IResult> ShowPrompt()
    {
        DialogOperationResult<DialogResult> asyncOp;
       yield return asyncOp = _dialogManager.ShowMessageAsync("Are you sure?", DialogButtons.YesNo);

       if (asyncOp.DialogResult == DialogResult.Yes)
            _dialogManager.ShowMessageAsync("Good to know!", DialogButtons.Ok);

       if (asyncOp.DialogResult == DialogResult.No)
            _dialogManager.ShowMessageAsync("Too bad!", DialogButtons.Ok);
    }
}

That's better. Now let's say we really only have additional logic if the user says Yes. If the user says No, we effectively just want to cancel the coroutine. For this purpose, the DialogManager allows for the developer to designate one button as the cancel button. At the same time you specify another button as the default button. 

Note: DialogResult.Ok and DialogResult.Cancel are implicit dedicated default and cancel buttons respectively.

Doing this has another effect. The default button is automatically associated with hitting Enter on the keyboard and the cancel button with hitting ESC.

C#
[Export]
public class MainPageViewModel
{
   private readonly IDialogManager _dialogManager;

    [ImportingConstructor]
   public MainPageViewModel(IDialogManager dialogManager)
    {
        _dialogManager = dialogManager;
    }

   public IEnumerable<IResult> ShowPrompt()
    {
       yield return
            _dialogManager.ShowMessageAsync("Are you sure?", DialogResult.Yes, DialogResult.No,    
                                            DialogButtons.YesNo);

        _dialogManager.ShowMessageAsync("Good to know!", DialogButtons.Ok);
    }
}

Now we no longer have to check what button the user clicked on. The coroutine will only continue past the "yield return" if the user clicked Yes. If the user clicks No. The coroutine will be terminated.

Customizing buttons

Cocktail provides a list of commonly used buttons as part of the DialogButtons class. This list of buttons is modeled after the standard MessageBox.

If those buttons are not sufficient, a developer can define their own buttons. Any arbitrary object can be a button. The object becomes the content of the XAML button and as long as XAML knows how to render it everything is good. The easiest object type for a custom button is obviously a simple string. The following example displays a message box with a Save and Cancel button and displays the string associated with the button clicked by the user.

C#
[Export]
public class MainPageViewModel
{
   private readonly IDialogManager _dialogManager;

    [ImportingConstructor]
   public MainPageViewModel(IDialogManager dialogManager)
    {
        _dialogManager = dialogManager;
    }

   public IEnumerable<IResult> ShowPrompt()
    {
        DialogOperationResult<string> asyncOp;
       yield return asyncOp = _dialogManager.ShowMessageAsync("Are you sure?", new[] {"Save", "Cancel"});

       yield return
            _dialogManager.ShowMessageAsync(string.Format("The user clicked: {0}", asyncOp.DialogResult),
                                            DialogButtons.Ok);
    }
}
As with the out-of-box buttons, the developer can specify the default and cancel buttons.

Displaying a dialog box

Displaying a dialog box is largely the same as displaying a simple message, except that the caller provides their own ViewModel and View for the content. Everything you learned about customizing the buttons etc. applies to showing a dialog box in exactly the same manner.

The following example demonstrates a simple dialog box that asks the user for their name and then displays a message. Notice that we don't have to do anything to handle the cancel button. As mentioned earlier, DialogResult.Cancel will implicitly become the cancel button and as a result automatically cancel the coroutine, so that the second "yield return" will only be executed if the user clicks Ok.

XAML
<UserControl x:Class="DialogManagerSamples.NamePromptView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             Width="Auto"
             Height="Auto">

    <Grid x:Name="LayoutRoot">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="Auto" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <TextBlock Grid.ColumnSpan="2" Margin="10" Text="Hello Stranger, what is your name?" />
        <TextBlock Grid.Row="1" Margin="10" VerticalAlignment="Center" Text="Firstname:" />
        <TextBlock Grid.Row="2" Margin="10" VerticalAlignment="Center" Text="Lastname:" />
        <TextBox x:Name="FirstName" Grid.Row="1" Grid.Column="1" Width="200" Margin="10" />
        <TextBox x:Name="LastName" Grid.Row="2" Grid.Column="1" Width="200" Margin="10" />
    </Grid>
</UserControl>
C#
[Export]
public class NamePromptViewModel : Screen
{
   private string _firstName;
   private string _lastName;

   public string FirstName
    {
       get { return _firstName; }
       set
        {
            _firstName = value;
            NotifyOfPropertyChange(() => FirstName);
        }
    }

   public string LastName
    {
       get { return _lastName; }
       set
        {
            _lastName = value;
            NotifyOfPropertyChange(() => LastName);
        }
    }
}

[Export]
public class MainPageViewModel
{
   private readonly IDialogManager _dialogManager;
   private readonly NamePromptViewModel _namePrompt;

    [ImportingConstructor]
   public MainPageViewModel(IDialogManager dialogManager, NamePromptViewModel namePrompt)
    {
        _dialogManager = dialogManager;
        _namePrompt = namePrompt;
    }

   public IEnumerable<IResult> AskForName()
    {
       yield return _dialogManager.ShowDialogAsync(_namePrompt, DialogButtons.OkCancel);

       yield return
            _dialogManager.ShowMessageAsync(
               string.Format("Nice to meet you {0} {1}", _namePrompt.FirstName, _namePrompt.LastName),
                DialogButtons.Ok);
    }
}

Accessing the dialog host

In the above example, it would be nice if we could disable the Ok button as long as at least one of the text boxes is empty. Instead of throwing an error message if the user didn't type anything, it makes for a much nicer user experience if we don't even let the user click Ok, until they have entered all the required data.

Cocktail provides access to the buttons via the dialog host. The dialog host can be obtained from the DialogHost extension method.

The following example demonstrates how to use the DialogHost method to enable and disable the Ok button.

C#
[Export]
public class NamePromptViewModel : Screen
{
   private string _firstName;
   private string _lastName;
   private DialogButton _okButton;

   private void OnCompleteChanged()
    {
       if (_okButton != null)
            _okButton.Enabled = IsComplete;
    }

   public string FirstName
    {
       get { return _firstName; }
       set
        {
            _firstName = value;
            NotifyOfPropertyChange(() => FirstName);
            OnCompleteChanged();
        }
    }

   public string LastName
    {
       get { return _lastName; }
       set
        {
            _lastName = value;
            NotifyOfPropertyChange(() => LastName);
            OnCompleteChanged();
        }
    }

   public bool IsComplete
    {
       get { return !string.IsNullOrWhiteSpace(FirstName) && !string.IsNullOrWhiteSpace(LastName); }
    }

   protected override void OnActivate()
    {
       base.OnActivate();
        _okButton = this.DialogHost().GetButton(DialogResult.Ok);
        _okButton.Enabled = IsComplete;
    }
}

Custom commands (new feature in v1.1)

As of version 1.1, the DialogManager supports the use of custom UI commands making it infinitely more useful for many scenarios not covered by a simple list of buttons that close the dialog.

Custom commands come in the form of the DialogUICommand<T> class and its associated interfaces. Additional overloads for ShowDialogAsync and ShowMessageAsync accept a list of commands. By default, each command still closes the dialog or message box with the associated DialogResult, but the developer can fully control the desired behavior by handling the command's Invoked event. 

The following example shows the Login popup from TempHire taking advantage of custom commands. In the ShowAsync method it sets up the "Login" and "Close" command in case of WPF and then displays itself through the DialogManager. The login command's Invoked handler first cancels the command. Canceling the command will instruct the DialogManager not to proceed with closing the dialog, while we asynchronously handle the login. Once the asynchronous login is successful, it instructs the dialog host to close.

In order to avoid potential memory leaks, all Invoked handlers will automatically be removed once the dialog host closes.
C#
[Export]
public class LoginViewModel : Screen
{
   private readonly IAuthenticationService _authenticationService;
   private readonly IDialogManager _dialogManager;
   private readonly IGlobalCache _globalCache;
   private string _failureMessage;
   private IDialogUICommand<DialogResult> _loginCommand;
   private string _password;
   private string _username;
   private bool _cacheLoaded;

    [ImportingConstructor]
   public LoginViewModel(IAuthenticationService authenticationService, IDialogManager dialogManager,
                            [Import(AllowDefault = true)] IGlobalCache globalCache)
    {
      /// Snip for clarity
   }

   public IBusyWatcher Busy { get; private set; }

   public string Username
    {
       get { return _username; }
       set
        {
            _username = value;
            NotifyOfPropertyChange(() => Username);
            UpdateCommands();
        }
    }

   public string Password
    {
       get { return _password; }
       set
        {
            _password = value;
            NotifyOfPropertyChange(() => Password);
            UpdateCommands();
        }
    }

   public string FailureMessage
    {
       get { return _failureMessage; }
       set
        {
            _failureMessage = value;
            NotifyOfPropertyChange(() => FailureMessage);
            NotifyOfPropertyChange(() => FailureMessageVisible);
        }
    }

   public bool FailureMessageVisible
    {
       get { return !string.IsNullOrWhiteSpace(_failureMessage); }
    }

   private bool CanLogin
    {
       get { return !string.IsNullOrWhiteSpace(Username) && !string.IsNullOrWhiteSpace(Password); }
    }

   private IEnumerable<IResult> LoginAsync()
    {
       using (Busy.GetTicket())
        {
            FailureMessage = "";

            var credential = new LoginCredential(Username, Password, null);
           // Clear username and password fields
           Username = null;
            Password = null;

            OperationResult operation;
           yield return operation = _authenticationService.LoginAsync(credential).ContinueOnError();

           if (_authenticationService.IsLoggedIn)
            {
               if (_globalCache != null)
                {
                   yield return operation = _globalCache.LoadAsync().ContinueOnError();

                    _cacheLoaded = operation.CompletedSuccessfully;
                   if (operation.HasError)
                        FailureMessage = "Failed to load global entity cache. Try again!";
                }
            }

           if (operation.HasError)
                FailureMessage = operation.Error.Message;
        }
    }

   public DialogOperationResult<DialogResult> ShowAsync()
    {
        var commands = new List<IDialogUICommand<DialogResult>>();
        _loginCommand = new DialogUICommand<DialogResult>("Login", DialogResult.Ok, true);
        _loginCommand.Invoked += (sender, args) =>
        {
            args.Cancel(); // Cancel command, we'll take it from here.

            LoginAsync().ToSequentialResult().Execute(
                _ =>
                    {
                       if (_authenticationService.IsLoggedIn && _cacheLoaded)
                            args.DialogHost.TryClose(_loginCommand.DialogResult);
                    });
        };
        commands.Add(_loginCommand);

#if !SILVERLIGHT
       var closeCommand = new DialogUICommand<DialogResult>("Close", DialogResult.Cancel, false, true);
        commands.Add(closeCommand);
#endif

        UpdateCommands();
       return _dialogManager.ShowDialogAsync(commands, this);
    }

   private void UpdateCommands()
    {
        _loginCommand.Enabled = CanLogin;
    }
}

Customizing the dialog host

If the standard look and feel of a dialog or message box doesn't fit with the overall look of your application, Cocktail allows for the complete customization of the dialog host. 

To start customizing the dialog host, we first need to create our own ViewModel and subclass DialogHostBase. Cocktail will automatically discover this ViewModel and use it instead of the default.

C#
public class CustomDialogViewModel : DialogHostBase
{
        
}

Once we have our own ViewModel, we can create a corresponding View. The following simple Silverlight example uses red bold text for the buttons instead of the standard look. 

XAML
<controls:ChildWindow x:Class="CustomDialog.CustomDialogView"
                      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                      xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
                      xmlns:cal="http://www.caliburnproject.org">
    <Grid x:Name="LayoutRoot" MinWidth="166" Margin="2">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <ContentControl x:Name="ActiveItem" Width="Auto" Height="Auto" HorizontalContentAlignment="Stretch"
                        VerticalContentAlignment="Stretch" IsTabStop="False" TabIndex="0" />
        <ItemsControl x:Name="DialogButtons" Grid.Row="1" IsEnabled="{Binding ActionsEnabled}" IsTabStop="False">
            <ItemsControl.ItemsPanel>
                <ItemsPanelTemplate>
                    <StackPanel HorizontalAlignment="Right" Orientation="Horizontal" />
                </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                   <!-- Customized buttons -->
                    <Button Width="75" Height="23" Margin="5" cal:Message.Attach="Close($dataContext)"
                            Content="{Binding Content}" IsEnabled="{Binding Enabled}"
                            Foreground="Red" FontWeight="Bold" />
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
    </Grid>
</controls:ChildWindow>
Tags:
Created by DevForce on April 04, 2012 09:13

This wiki is licensed under a Creative Commons 2.0 license. XWiki Enterprise 3.2 - Documentation. Copyright © 2015 IdeaBlade